这次我们看看阿里人是如何蹂躏CPU的

这次我们看看阿里人是如何蹂躏CPU的

在前面的系列文章中皮皮就介绍过如何使得CPU、I/O满载,如何实现网络延迟、丢包和中断,如何模拟线程、进程以及机器假死。如果对此不清楚的同学可以在文末的【相关系列】中查看相应的文章。

本文介绍是阿里巴巴混沌工程工具ChaosBlade中的CPU满载的故障模拟的实现方式。之前看的是Netflix的Simain Army中的实现,看上去一目了然,通过java的形式调用一下模拟脚本,脚本的内容如下:

#!/bin/bash

cat << EOF > /tmp/infiniteburn.sh
#!/bin/bash
while true;
do openssl speed;
done
EOF

for i in {1..32}
do
nohup /bin/bash /tmp/infiniteburn.sh &
done

通过调用openssl speed来模拟CPU满负荷运作,这里的方式还比较粗暴,直接开了32个线程来执行openssl speed这种CPU密集型计算程序。如果CPU的内核数大于32,那么就需要修改这里的硬编码脚本了。

通过翻看阿里巴巴混沌工程工具ChaosBlade的代码(golang project),发现它对于CPU的蹂躏方式要细腻很多。比如CPU的内核个数使用runtime.NumCPU()来获取。当然了还会有一些其它的附加细节,这个下面会详述。

我们先来简单的了解一下ChaosBlade对于这一块的实现布局。在ChaosBlade中,我们实现故障(类似cpu 100%、I/O 100%、网络中断等)注入的入口是通过blade命令。例如:使得CPU满载负荷,那么可以使用./blade create cpu fullload来实现。

其实ChaosBlade中的blade命令只是采用Cobra封装的CLI入口,其内部实现是调用bin/目录下的chaos_burncpu程序。

ChaosBlades(releases版)的下载地址为:https://github.com/chaosblade-io/chaosblade/releases。解压之后的目录结构为:

hidden@hidden:~/chaos/chaosblade-0.2.0$ tree
.
├── bin
│   ├── chaos_burncpu
│   ├── chaos_burnio
│   ├── chaos_changedns
│   ├── chaos_delaynetwork
│   ├── chaos_dropnetwork
│   ├── chaos_filldisk
│   ├── chaos_killprocess
│   ├── chaos_lossnetwork
│   ├── chaos_stopprocess
│   ├── cplus-chaosblade.spec.yaml
│   ├── jvm.spec.yaml
│   └── tools.jar
├── blade
├── chaosblade.dat
└── lib --<snip>

可以看到bin/目录下处理chaos_burncpu之外还有很多chaos_形式的程序,比如chaos_burnio是让I/O满载负荷。这些chaos_形式的程序的使用方式都是大同小异的。

使用指南

我们这里不使用ChaosBlade提供的blade命令,因为这只是外部的一层封装,使用这个无法使我们能够透彻的理解内部的实现。我们这里使用bin/chaos_burncpu来演示一下具体的用法。调用方式如下:

bin/chaos_burncpu --start

这里命令可以让当前机器的CPU满载负荷。取消CPU满载负荷可以使用如下的命令:

bin/chaos_burncpu --stop

bin/chaos_burncpu命令还可以通过--cpu-count来指定cpu中需要满载负荷的内核个数,示例如下:

# 指定需要满载负荷的cpu的内核个数为4
bin/chaos_burncpu --start --cpu-count 4

假设现在测试所使用的机器的cpu共有4个内核,那么我们让其中3个内核满载,效果如何呢?首先运行sar -u 1 100命令来监测cpu的使用情况,然后运行:

bin/chaos_burncpu --start --cpu-count 3

可以在持续运行sar命令的shell终端中看到cpu的%idle数值变成了25%左右:

02:21:35 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
02:21:44 PM all 73.95 0.00 1.24 0.00 0.00 24.81

我们还可以指定让某个cpu内核满载,比如下面的示例中让内核编号为1的满载:

bin/chaos_burncpu --start --cpu-list 1

sar命令中还可以通过—P参数查看指定编号的内核的使用情况,比如使用sar -u -P 1 1 100来指定编号为1的cpu内核的使用情况:

hidden@hidden:~$ sar -u -P 1 1 100
Linux 4.4.0-33.bm.1-amd64 (n224-008-172) 08/15/2019 _x86_64_ (4 CPU)

02:45:19 PM CPU %user %nice %system %iowait %steal %idle
02:45:20 PM 1 98.00 0.00 2.00 0.00 0.00 0.00
02:45:21 PM 1 98.99 0.00 1.01 0.00 0.00 0.00

可以看到这个内核已经满载。至于怎么实现挂载单个CPU内核的在下面会有详细的介绍。

我们再来通过-P 0来看一下编号为0的cpu内核的使用情况:

02:47:32 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
02:47:33 PM 0 1.00 0.00 2.00 0.00 0.00 97.00
02:47:34 PM 0 0.00 0.00 0.00 0.00 0.00 100.00

可以看到这个内核还是处于空闲状态(%idle接近100%)。

原理

CPU满载

chaos_burncpu中实现CPU满载负荷的逻辑其实相当简单,通过程序让CPU一直运作即可。代码如下:

func burnCpu() {
runtime.GOMAXPROCS(cpuCount)

for i := 0; i < cpuCount; i++ {
go func() {
for {
for i := 0; i < 2147483647; i++ {
}
runtime.Gosched() //让出CPU时间片
}
}()
}
select {} // wait forever
}

读者可以自己比较一下这个和Simain Army中的openssl speed的区别。

关闭

关闭CPU满载负荷的过程也比较简单粗暴,总共分为两步:

  1. 使用ps -ef | grep ... 命令找出chaos_burncpu的pid。
  2. 使用kill -9 pid命令干掉它。

指定内核满载

我们在上面就了解到通过--cpu-count可以指定CPU满载的内核个数,通过--cpu-list可以指定内核满载。ChaosBlade相比于Simian Army中的细腻之处也就体现在这里。

--cpu-count的功能很好实现,在上面的func burnCpu()函数中的cpuCount就是--cpu-count所指定的值。

--cpu-list的功能比较复杂,总共分为3步:

  • 第一步:执行nohup bin/chaos_burncpu --nohup --cpu-count 1 --cpu-processor [cpu内核编号] > /dev/null 2>&1 &。假设我们要指定编号为1的内核满载,那么对应的命令即为:nohup bin/chaos_burncpu --nohup --cpu-count 1 --cpu-processor 1 > /dev/null 2>&1 &。其实这个也只是个烟雾弹,实际上还是调用原本的bin/chaos_burncpu --start --cpu-count 1而已,只不过这里多指定了一个cpu-processor的信息。
  • 第二步:执行ps -ef | grep ... 命令找出对应的pid。
  • 第三步:将进程pid绑定到编号为cpu-processor的内核上。那么这一步怎么操作呢?我们先来看一下CPU Affinity。

CPU Affinity

基本概念

CPU affinity (亲和力/亲和性)是一种调度属性(scheduler property), 它可以将一个进程”绑定” 到一个或一组CPU上。

将进程与CPU绑定,最直观的好处就是减少cpu之间的cache同步和切换,提高了cpu cache的命中率,提高代码的效率。

从CPU架构上,NUMA拥有独立的本地内存,节点之间可以通过互换模块做连接和信息交互,因此每个CPU可以访问整个系统的内存,但是访问远地内存访问效率大大降低,绑定CPU操作对此类系统运行速度会有较大提升,UMA架构下,多CPU通过系统总线访问存储模块。不难看出,NUMA使用CPU绑定时,每个核心可以更专注地处理一件事情,资源体系被充分使用,减少了同步的损耗。

表示方法

CPU affinity 使用位掩码(bitmask)表示, 每一位都表示一个CPU, 置1表示”绑定”。最低位表示第一个逻辑CPU, 最高位表示最后一个逻辑CPU。CPU affinity典型的表示方法是使用16进制,具体如下:

0x00000001
is processor #0

0x00000003
is processors #0 and #1

0xFFFFFFFF
is all processors (#0 through #31)

taskset命令

taskset命名用于获取或者设定CPU affinity。

# 命令行形式
Usage: taskset [options] [mask | cpu-list] [pid|cmd [args...]]

PARAMETER
    mask : cpu亲和性,当没有-c选项时, 其值前无论有没有0x标记都是16进制的,
        当有-c选项时,其值是十进制的.
    command : 命令或者可执行程序
    arg : command的参数
    pid : 进程ID,可以通过ps/top/pidof等命令获取


OPTIONS
    -a, --all-tasks (旧版本中没有这个选项)
        这个选项涉及到了linux中TID的概念,他会将一个进程中所有的TID都执行一次CPU亲和性设置.
        TID就是Thread ID,他和POSIX中pthread_t表示的线程ID完全不是同一个东西.
        Linux中的POSIX线程库实现的线程其实也是一个进程(LWP),这个TID就是这个线程的真实PID.
-p, --pid
操作已存在的PID,而不是加载一个新的程序
-c, --cpu-list
声明CPU的亲和力使用数字表示而不是用位掩码表示. 例如 0,5,7,9-11.
-h, --help
display usage information and exit
-V, --version
output version information and exit
USAGE
    1) 使用指定的CPU亲和性运行一个新程序
      taskset [-c] mask command [arg]...
        举例:使用CPU0运行ls命令显示/etc/init.d下的所有内容
          taskset -c 0 ls -al /etc/init.d/
    2) 显示已经运行的进程的CPU亲和性
      taskset -p pid
        举例:查看init进程(PID=1)的CPU亲和性
          taskset -p 1
    3) 改变已经运行进程的CPU亲和力
     taskset -p[c] mask pid
        举例:打开2个终端,在第一个终端运行top命令,第二个终端中
          首先运行:[~]# ps -eo pid,args,psr | grep top #获取top命令的pid和其所运行的CPU号
          其次运行:[~]# taskset -cp 新的CPU号 pid #更改top命令运行的CPU号
          最后运行:[~]# ps -eo pid,args,psr | grep top #查看是否更改成功
PERMISSIONS
一个用户要设定一个进程的CPU亲和性,如果目标进程是该用户的,则可以设置,如果是其他用户的,则会设置失败,提示 Operation not permitted.当然root用户没有任何限制.
任何用户都可以获取任意一个进程的CPU亲和性.

应用taskset

下面我们就来详细实践一下CPU指定内核满载的过程。

首先我们让某个内核满载,这里我们还并未指定哪一个内核(对应前面所说第一步):

bin/chaos_burncpu --start --cpu-count 1

第二步,我们找到这个进程的pid:

hidden@hidden:~$ ps -ef | grep chaos_burncpu
zhuzhon+ 572792 490371 99 18:20 pts/0 00:00:14 bin/chaos_burncpu --nohup --cpu-count 1 --cpu-processor 1
zhuzhon+ 572860 551590 0 18:20 pts/3 00:00:00 grep chaos_burncpu

此时,我们查看pid=572792的进程的亲和力为f(即二进制的1111,也就是CPU内核编号0-3),也就是说CPU中的4个内核都有可能运行这个满载程序。

hidden@hidden:~$ taskset -p 572792
pid 572792's current affinity mask: f

hidden@hidden:~$ taskset -c -p 572792
pid 572792's current affinity list: 0-3

上面第一步中,指定某个单独的内核满载的实际效果应该时每个内核都会有一定的时间处于满载状态。对此有疑问的同学可以通过sar -u -P [cpu-processor] 1 1000来验证一下。

第三步,我们指定编号为0的内核满负荷:

hidden@hidden:~$ taskset -cp 0 572792
pid 572792's current affinity list: 0-3
pid 572792's new affinity list: 0

此时我们可以通过sar -u -P [cpu-processor] 1 1000命令来检测4个内核的各个使用情况。不出意外的话,内核编号为0的检测结果应该和下面的类似:

hidden@hidden:~$ sar -u -P 0 1 1000
Linux 4.4.0-33.bm.1-amd64 (n224-008-172) 08/15/2019 _x86_64_ (4 CPU)

06:22:08 PM CPU %user %nice %system %iowait %steal %idle
06:38:46 PM 0 100.00 0.00 0.00 0.00 0.00 0.00
06:38:47 PM 0 100.00 0.00 0.00 0.00 0.00 0.00
06:38:48 PM 0 100.00 0.00 0.00 0.00 0.00 0.00

而其他内核的%idle应该都接近在100%。

总结

本文不仅介绍了如何“蹂躏”CPU,还附带了一个知识点就是CPU affinity,程序开发者比机器更懂程序,如果用好它,可以有意想不到的效果。还有,在之前的《看我如何作死 | 将CPU、IO打爆》中不仅介绍了如何使得CPU满载,还介绍了I/O如何满载,这里不妨透漏一下,阿里和Netflix对于I/O满载的模拟都是使用的linux dd工具,所以本文就不多做赘述啦。

参考资料及衍生读物

  1. cpu亲和性绑定
  2. Linux中CPU亲和性(affinity)
  3. https://github.com/chaosblade-io/chaosblade

欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×