這次我們看看阿里的人是如何蹂躪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滿載負荷的過程也比較簡單粗暴,總共分為兩步:
- 使用ps -ef | grep … 命令找出chaos_burncpu的pid。
- 使用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工具,所以本文就不多做贅述啦。