1.從平均負載談起
我們每次發現線上系統變慢時,第一件事往往都會使用top或者uptime命令查看cpu的負載以及占用率,比如top命令會有下面的結果:
top - 15:51:39 up 84 days, 1:24, 4 users, load average: 0.20, 0.22, 0.18
Tasks: 165 total, 1 running, 163 sleeping, 1 stopped, 0 zombie
%Cpu(s): 2.6 us, 0.8 sy, 0.0 ni, 95.8 id, 0.4 wa, 0.0 hi, 0.4 si, 0.0 st
MiB Mem : 31775.4 total, 404.9 free, 16313.4 used, 15057.1 buff/cache
MiB Swap: 8192.0 total, 8101.0 free, 91.0 used. 14823.6 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
411122 libvirt+ 20 0 10.5g 8.1g 5072 S 4.7 25.9 6232:25 qemu-system-x86
272695 libvirt+ 20 0 10.5g 8.0g 5148 S 4.3 25.8 3154:25 qemu-system-x86
712260 root 20 0 2162060 246380 184740 S 2.3 0.8 547:03.34 clickhouse-serv
42 root 25 5 0 0 0 S 1.7 0.0 1017:25 ksmd
272710 root 20 0 0 0 0 S 0.3 0.0 53:57.73 kvm-pit/272695
810400 root 20 0 9252 3840 3088 R 0.3 0.0 0:00.04 top
1 root 20 0 169092 13184 8460 S 0.0 0.0 2:19.67 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:01.44 kthreadd
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_par_gp
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H-kblockd
9 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
10 root 20 0 0 0 0 S 0.0 0.0 1:09.54 ksoftirqd/0
11 root 20 0 0 0 0 I 0.0 0.0 63:12.44 rcu_sched
但是,你真的知道這里每列輸出的含義嗎?
就拿第一行輸出來說,前面幾項我們都非常熟悉:
15:51:39 // 當前時間
up 84 days, 1:24, // 系統運行的時間
4 users // 當前登錄的用戶數
而后面load average之后的3個數字,則依次是過去1分鍾、5分鍾、15分鍾的平均負載。
平均負載這個詞對我們來說可能既熟悉又陌生,熟悉是因為我們每天工作中都會提到這個詞,但是陌生在於是否能夠理解它背后的含義?有人可能會說平均負載是單位時間內cpu的使用率,其實這樣的理解是錯誤的,我們可以通過命令man uptime來查看其中對於平均負載的解釋。
簡單來說,平均負載(Load Average)就是指單位時間內,系統處於可運行狀態或不可中斷狀態的平均進程數量,可以理解為平均活躍過的進程數量,這玩意和cpu使用率並沒有直接的關系,那么什么是進程的可運行狀態和不可中斷狀態呢?
可運行狀態是指進程正在使用CPU或者正在等待CPU的狀態,也就是進程的運行態和就緒態,我們用ps命令看到進程的狀態為R或者R+這樣;而不可中斷狀態的進程則是正處於內核態執行流程中的進程,並且這些流程是不可被打斷的,最常見的就是等待硬件設備的I/O響應,也就是用ps命令看到的D狀態,這也是操作系統對硬件的一種保護機制避免不一致的狀態出現;除了這兩個關鍵狀態外,還有S表示休眠狀態,以及Z表示僵屍進程等。
總的來說,linux中常見的進程狀態有下面這幾種:
D uninterruptible sleep,不可中斷狀態,一般就是I/O狀態,隨着I/O狀態的變化會導致CPU負載的變化
R running or runnable,運行或可運行,包括就緒隊列上的進程
S interruptible sleep,可中斷睡眠狀態,一般是等待事件完成
T stopped by job control signal,由作業信號停止
Z 僵屍進程,屬於終止狀態但尚未被父進程回收
I 空閑狀態,也就是不可中斷睡眠的內核線程,和狀態D相比,狀態I則表示沒有任何負載,因此不會計入CPU時間也不會導致負載升高
具體進程狀態的含義可以通過ps的幫助手冊查看,通過上面的描述直觀上可以這么理解,平均負載就是單位時間內活躍過的進程數,實際上是活躍進程數指數衰減的平均值,因此最理想的情況下就是每個cpu上剛好運行1個進程,這樣每個cpu都得到了充分的利用,假設我們看到平均負載是8,那么意味着什么?可以舉例說明如下:
- 如果我們的cpu恰好是8核,意味着所有的cpu都剛好被完全占用。
- 如果我們的cpu是16核,說明cpu有一半的空閑。
- 但如果我們只有4核,則說明有一般的進程在競爭CPU。
講完上面這些,那么我們該如何判斷CPU負載是不是正常,首先我們要知道系統有幾個CPU,可以在/proc/cpuinfo中讀取到相關的信息:
grep 'model name' /proc/cpuinfo | wc -l
有了CPU個數,我們就可以得知,當平均負載值比CPU個數還大時,系統就出現了過載。那么這3個值,應該看哪個?
實際上上面這3個值都要看,這3個值給我們提供了分析系統負載趨勢的數據來源,讓我們能更全面的了解目前的負載情況,就好像天氣預報一樣,我們可能更關心天氣的變化情況,那么對於這3個值可以大致按照如下的思路分析:
- 如果這3個值基本相同,那么說明最近1分鍾、5分鍾、15分鍾內的負載都比較平穩,說明系統最近一段時間整體運行波動不大。
- 但如果過去1分鍾的值遠遠小於過去15分鍾的值,則說明系統負載在減少,過去15分鍾系統負載比較重,但是慢慢減少了。
- 反過來如果過去1分鍾的值遠遠大於過去15分鍾的值,就說明系統負載正在增大,增加可能是臨時的,也可能是持續的,需要進一步觀察。
不管是過去1分鍾還是15分鍾,只要是負載值超過了cpu個數,那么則意味着系統正在發生或者已經發生了過載問題,系統開始變得繁忙或者擁堵了,這時候就需要分析哪個進程導致的問題,並且想辦法來優化。最理想的情況下的負載值 = CPU個數 * (0.7~0.8)左右,一旦過高,就可能導致進程響應變慢,對服務造成影響。
雖然CPU平均負載和CPU使用率並沒有直接的關系,但是某些情況下會出現相似的表現,比如:
- 對於CPU密集型進程,進程越多會導致CPU利用率越高,同時會引起負載升高,這兩者變化是一致的。
- 對於I/O密集型進程,等待I/O也會導致平均負載升高,但是CPU利用率卻不高。
- 大量等待CPU時間片的進程調度也會導致平均負載升高,CPU利用率的體現也很高。
上面舉例說明了CPU平均負載和利用率的關系,其他的不再多說,接下來我們進入實戰環節來看一下常見的各類情況如何分析。
2.工具准備
模擬系統負載可以使用工具stress,同時還要安裝必要的sysstat包從而可以使用iostat、pidstat等工具查看資源占用的情況,如果是Ubuntu系統可以使用下面的命令安裝:
apt install stress
如果是CentOS可以使用下面的命令安裝:
yum install epel-release
yum install stress
安裝之后可以查看相應的幫助來學習使用:
stress --help
# 版本號
stress --version
當然我們也可以不借助於工具,自己編寫代碼測試,同樣可以實現工具的效果。
3.模擬用戶空間高CPU消耗
我們當前的機器是4核,我們開2個進程來模擬200%的cpu占用:
# --cpu參數等同於-c
stress --cpu 2 --timeout 180
然后我們打開top或者使用uptime看,負載不斷升高,跑2分多鍾后負載接近於2:
17:18:04 up 87 days, 23:49, 4 users, load average: 2.01, 1.00, 0.40
同時看top中cpu占用率每個進程也是100%:
top - 17:19:09 up 87 days, 23:50, 4 users, load average: 2.00, 1.21, 0.52
Tasks: 148 total, 3 running, 145 sleeping, 0 stopped, 0 zombie
%Cpu(s): 49.3 us, 0.2 sy, 0.0 ni, 48.1 id, 0.7 wa, 0.0 hi, 1.6 si, 0.0 st
MiB Mem : 31775.6 total, 3655.0 free, 8232.9 used, 19887.7 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 23081.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
347541 root 20 0 3856 104 0 R 100.0 0.0 2:40.72 stress
347542 root 20 0 3856 104 0 R 99.7 0.0 2:40.72 stress
使用mpstat也可以方便查看到用戶空間占用的變化:
05:21:03 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
05:21:08 PM all 5.67 0.00 0.25 0.40 0.00 0.50 0.00 0.45 0.00 92.73
05:21:08 PM 0 11.34 0.00 0.40 1.01 0.00 0.20 0.00 0.00 0.00 87.04
05:21:08 PM 1 11.38 0.00 0.00 0.61 0.00 0.00 0.00 0.61 0.00 87.40
05:21:08 PM 2 0.00 0.00 0.00 0.00 0.00 1.77 0.00 0.39 0.00 97.83
05:21:08 PM 3 0.20 0.00 0.60 0.00 0.00 0.00 0.00 0.80 0.00 98.40
05:21:08 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
05:21:13 PM all 48.01 0.00 0.19 0.34 0.00 3.46 0.00 0.72 0.00 47.24
05:21:13 PM 0 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
05:21:13 PM 1 99.80 0.00 0.00 0.00 0.00 0.20 0.00 0.00 0.00 0.00
05:21:13 PM 2 0.00 0.00 0.35 0.17 0.00 11.52 0.00 1.75 0.00 86.21
05:21:13 PM 3 0.00 0.00 0.39 1.18 0.00 0.99 0.00 0.99 0.00 96.45
這個很好理解,因為是純計算任務所以用幾個cpu平均負載就會趨近於幾。
4.模擬內存消耗
內存和CPU的關系也是難舍難分,同樣我們可以使用stress模擬內存消耗來窺探內存和CPU使用之間的關系,先看一個最簡單的開辟指定大小的內存並一直占用着:
# --vm指定開辟子進程數量 --vm-bytes每個進程分配內存的大小 --vm-keep表示內存一直占用
stress --vm 2 --vm-bytes 500M --vm-keep
執行之后使用free或者top可以看到進程占用了1GB左右的內存,當然這個過於簡單,可以指定內存申請的頻率如:
# --vm-hang指定釋放頻率
stress --vm 2 --vm-bytes 500M --vm-hang 5
這樣表示每5s釋放一次內存然后再申請,我們看CPU每5s會有一次小的波動:
Tasks: 147 total, 1 running, 146 sleeping, 0 stopped, 0 zombie
%Cpu(s): 1.2 us, 2.7 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi, 0.2 si, 0.0 st
MiB Mem : 31775.6 total, 2679.7 free, 9179.6 used, 19916.3 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 22134.5 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348349 root 20 0 515860 512204 208 S 6.6 1.6 0:00.56 stress --vm 2 --vm-bytes 500+
348350 root 20 0 515860 512204 208 S 6.6 1.6 0:00.56 stress --vm 2 --vm-bytes 500+
原因是因為這2個進程申請內存時會發出系統調用,因此cpu占用其實是內核空間的占用率,另外我們還會注意到此時查看剩余內存並沒有頻繁變化,原因是程序重復申請的內存系統並不會真正的釋放掉,而是僅僅標記為釋放,方便下次重復利用。
稍微熱身之后,來點重磅的,接下來就使用stress模擬連續寫內存,看看具體的變化如何:
# --vm-stride寫內存的步長
stress --vm 2 --vm-bytes 500M --vm-stride 16
stress --vm 2 --vm-bytes 500M --vm-stride 128
stress --vm 2 --vm-bytes 500M --vm-stride 1024
stress --vm 2 --vm-bytes 500M --vm-stride 4096
stress --vm 2 --vm-bytes 500M --vm-stride 1M
我們主要關注CPU的變化:
1.步長16B
top - 19:05:30 up 88 days, 1:36, 4 users, load average: 0.39, 0.12, 0.03
Tasks: 147 total, 4 running, 143 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.6 us, 0.0 sy, 0.0 ni, 88.3 id, 0.0 wa, 0.0 hi, 11.1 si, 0.0 st
%Cpu1 : 45.0 us, 55.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 46.2 us, 53.8 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 1.0 us, 0.0 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
MiB Mem : 31775.6 total, 2680.0 free, 9176.7 used, 19918.9 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 22137.4 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348359 root 20 0 515860 512204 208 R 100.0 1.6 0:07.64 stress
348360 root 20 0 515860 512204 208 R 100.0 1.6 0:07.65 stress
可以看到每個進程CPU占用100%,這100%由45%左右的用戶空間占用以及55%左右的內核空間占用。
2.步長128B
top - 19:07:37 up 88 days, 1:38, 4 users, load average: 0.59, 0.24, 0.08
Tasks: 147 total, 3 running, 144 sleeping, 0 stopped, 0 zombie
%Cpu0 : 1.8 us, 0.4 sy, 0.0 ni, 87.1 id, 0.0 wa, 0.0 hi, 10.7 si, 0.0 st
%Cpu1 : 37.1 us, 62.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 2.0 us, 1.2 sy, 0.0 ni, 95.2 id, 0.0 wa, 0.0 hi, 1.6 si, 0.0 st
%Cpu3 : 38.8 us, 61.2 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 2779.7 free, 9076.5 used, 19919.5 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 22237.6 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348363 root 20 0 515860 395932 208 R 100.0 1.2 0:14.50 stress
348362 root 20 0 515860 395932 208 R 99.6 1.2 0:14.49 stress
同樣是總的CPU占用100%,用戶空間利用率38%左右,內核空間為62%左右。
3.步長1024B即1K
top - 19:09:12 up 88 days, 1:40, 4 users, load average: 1.02, 0.54, 0.21
Tasks: 147 total, 3 running, 144 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.3 us, 0.3 sy, 0.0 ni, 85.7 id, 2.4 wa, 0.0 hi, 11.3 si, 0.0 st
%Cpu1 : 29.3 us, 70.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.7 us, 0.0 sy, 0.0 ni, 98.3 id, 0.7 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu3 : 29.7 us, 70.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 3113.9 free, 8741.6 used, 19920.1 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 22572.5 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348366 root 20 0 515860 240828 272 R 100.0 0.7 0:05.58 stress
348367 root 20 0 515860 202036 272 R 100.0 0.6 0:05.58 stress
4.步長4K
top - 19:13:18 up 88 days, 1:44, 4 users, load average: 1.75, 1.18, 0.55
Tasks: 147 total, 3 running, 144 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.9 us, 0.0 sy, 0.0 ni, 90.2 id, 0.0 wa, 0.0 hi, 8.9 si, 0.0 st
%Cpu1 : 25.2 us, 74.8 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 25.2 us, 74.8 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.0 us, 1.0 sy, 0.0 ni, 97.1 id, 0.0 wa, 0.0 hi, 1.9 si, 0.0 st
MiB Mem : 31775.6 total, 3647.7 free, 8206.5 used, 19921.4 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 23107.4 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348390 root 20 0 515860 20432 272 R 100.0 0.1 0:03.06 stress
348391 root 20 0 515860 24920 272 R 100.0 0.1 0:03.06 stress
5.步長1M
top - 19:12:26 up 88 days, 1:43, 4 users, load average: 1.78, 1.07, 0.48
Tasks: 147 total, 3 running, 144 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni, 95.7 id, 0.0 wa, 0.0 hi, 4.3 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 80.0 id, 0.0 wa, 0.0 hi, 20.0 si, 0.0 st
%Cpu2 : 13.0 us, 87.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 13.3 us, 86.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 3669.1 free, 8185.3 used, 19921.2 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 23128.8 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348383 root 20 0 1052436 4064 272 R 97.8 0.0 0:27.70 stress
348384 root 20 0 1052436 4064 272 R 97.8 0.0 0:27.70 stress
通過上面的變化可以發現一個有趣的規律,就是隨着寫內存步長的增大,用戶空間CPU占用率不斷降低(us),內核空間占用率不斷升高(sy),而且變化相當明顯,事實上當步長為1Byte時,用戶空間占用率接近100%,內核空間的占用率非常小,出現這個現象的原因究竟是為何?
要理解這個現象,首先要明白linux系統虛擬內存管理的機制,linux采用多級頁表管理虛擬內存,內存分配的單元叫做頁,每個頁的物理地址空間和虛擬地址空間都是連續的,通常大小是4K,而頁表(Page Table)是一個由虛擬地址到物理地址的映射表,嚴格來說是虛擬頁號到物理頁號的映射,每個進程單獨維護1個頁表,因此每個進程看起來虛擬地址空間是連續的無限大的,但是物理內存卻是分散的,因此給定虛擬地址可以計算頁號和偏移,然后通過查找頁表可以找到物理頁號然后加上偏移就得到物理地址,可以看下圖:
關於頁表的細節先不多講,我們只要知道頁表用來做虛擬地址和物理地址的轉換即可,而操作系統的作用就是屏蔽硬件的同時采用受限直接執行(limited direct execution)的方式,通過用戶模式到內核模式的轉換從而防止軟件對硬件的破壞,所以我們每次查找頁表的過程其實就是內核模式在做這次轉換,因此此時是內核空間在占用CPU,體現就是sy的占用,但是由於單級頁表占用空間過大的問題,現代linux普遍采用多級頁表(Multilevel page tables)進行內存的映射,比如x86下面的3級頁表:
至於多級頁表如何節省空間有時間再另開1篇文章詳細介紹,總之多級頁表是一種時間換空間的思想,固然可以節省空間,只是會進行多次內存訪問,性能有所下降,所以現代CPU中都有1個專用的芯片叫做TLB,全稱是地址變換高速緩沖(Translation-Lookaside Buffer),這個芯片會緩存我們用過的虛擬內存頁號,當我們在同一個內存頁操作的時候,直接通過查詢TLB,而無需通過從內存中查找頁表就可以直接得到物理頁號,不過TLB的大小和CPU高速緩存一樣速度極快但是空間非常小,這就要求我們的程序具有良好的空間局部性,從而減少仿存的開銷。
好了,有了TLB我們就可以不用查詢頁表通過虛擬地址直接找到物理地址,接下來我們就可以向物理地址寫入數據了,同樣向內存寫入數據也屬於操作系統的保護范圍,由內核來完成,CPU也不是直接就向內存寫入數據,而是直接操作CPU Cache,再由CPU Cache通過緩存一致性協議同步至物理內存,CPU同樣也分為3級緩存,一級緩存速度最快但是空間也最小,通常只有32K,L1和L2 Cache通常是每個CPU核心單獨一個,L3 Cache是多核共享的,下面這是一個物理分布圖:
可以抽象出來如下:
同樣CPU緩存也有最小單元,這個最小單元叫做緩存塊(Cache Line),通常這個大小是64B,也就是從內存中緩存數據的最小單元是64B,如果在這個范圍內多次寫,那么也不用每次都訪問數據而只需要寫緩存就好了,和TLB緩存地址相比,CPU緩存主要緩存的是數據本身,就是程序的指令和數據。
綜上有了TLB和CPU Cache如果我們的操作具有局部性,那么CPU需要等待的時間就會非常短,這樣用戶空間利用率就能上來,而相反,如果程序不具備局部性而是跨度非常大,那么TLB和CPU Cache幾乎起不到作用,相當於每次都得通過頁表尋址並且每次都得同步cpu緩存,那么相當於多出好幾次的內存訪問,系統內核態工作時間變長,cpu會等待內核態仿存,所以內核利用率會增高,此時的表現就是程序性能變低。
根據這些分析,上面的測試出現的現象就不難理解了,開始步長非常小的時候,多次操作都集中在1個內存頁內,因此地址轉換和數據訪問會多次被TLB以及CPU Cache命中,拿步長16來說,平均256次才需要從查找1次頁表,cpu就有更多的周期進行實際的工作,所以用戶空間利用率比較好,但是隨着步長的增大,需要訪問內存的次數同樣情況下就會變多,最后的時候每次操作都需要訪問頁表,重新加載緩存等,完全不具備局部性的特征,cpu大部分時鍾周期內都處於等待狀態,所以內核空間的占用率迅速增長至將近90%,這樣我們就通過現象和實際和原理分析,來揭示內存和CPU之間的關系。
5.模擬I/O wait
說到I/O wait,這和我們日常工作有着太多的關系,但凡做開發總是一個繞不過去的話題,同樣我們也是通過模擬I/O阻塞來淺析這其中的聯系,使用stress可以不斷地執行刷盤:
stress -i 2
查看cpu占用如下:
top - 20:15:17 up 88 days, 2:46, 4 users, load average: 0.59, 0.26, 0.19
Tasks: 147 total, 1 running, 146 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.3 us, 3.3 sy, 0.0 ni, 50.8 id, 39.8 wa, 0.0 hi, 5.7 si, 0.0 st
%Cpu1 : 1.0 us, 4.0 sy, 0.0 ni, 73.9 id, 20.7 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu2 : 1.3 us, 1.3 sy, 0.0 ni, 94.4 id, 2.3 wa, 0.0 hi, 0.7 si, 0.0 st
%Cpu3 : 0.7 us, 4.7 sy, 0.0 ni, 73.2 id, 21.4 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 3656.3 free, 8204.1 used, 19915.2 buff/cache
MiB Swap: 8192.0 total, 8190.7 free, 1.3 used. 23110.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
348459 root 20 0 3856 100 0 D 7.6 0.0 0:00.60 stress
348458 root 20 0 3856 100 0 D 7.0 0.0 0:00.62 stress
可以發現I/O wait每個cpu核心占用了20~40%,同時看平均負載也已經上升至2左右:
20:17:29 up 88 days, 2:48, 4 users, load average: 1.95, 0.92, 0.45
我們通過執行iotop命令也可以發現io占用高的進程:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 0.00 B/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
348458 be/4 root 0.00 B/s 0.00 B/s 0.00 % 93.12 % stress -i 2
348459 be/4 root 0.00 B/s 0.00 B/s 0.00 % 92.03 % stress -i 2
另外通過pidstat也可以查找對應的進程,通過iostat可以看到對應磁盤的tps非常高:
可以看到在這個模擬中,寫入硬盤的數據量或者說帶寬並不高,而是頻繁的向磁盤sync導致io等待很長,那么我們怎么用程序來模擬這個情況呢?
我們可能會想到用程序模擬寫入字節流,用python先寫一段代碼如下:
f = open('test', 'w')
while True:
f.write('abc')
f.close()
跑起來之后,我們看CPU占用如下:
top - 08:24:21 up 88 days, 14:55, 3 users, load average: 0.26, 0.22, 0.15
Tasks: 145 total, 2 running, 143 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.7 us, 0.0 sy, 0.0 ni, 99.0 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 99.7 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 98.3 us, 1.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 3136.4 free, 8213.5 used, 20425.7 buff/cache
MiB Swap: 8192.0 total, 8191.0 free, 1.0 used. 23100.7 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
350271 root 20 0 16432 10176 6552 R 100.0 0.0 0:10.84 python3
查看磁盤占用:
iostat -d -t 1 -p /dev/sda3
08/04/2021 08:40:50 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 0.00 0.00 0.00 0.00 0 0 0
08/04/2021 08:40:51 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 0.00 0.00 0.00 0.00 0 0 0
08/04/2021 08:40:52 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 17.00 0.00 116.00 0.00 0 116 0
查看iotop的寫入帶寬:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 32.41 M/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
350655 be/4 root 0.00 B/s 32.41 M/s 0.00 % 0.00 % python3
我們會發現情況並不是我們想象的那樣,相反的是用戶模式幾乎占用了100%的CPU,查看磁盤發現好大會才有一次較大的寫盤,大部分情況下寫入帶寬都是接近於0,但是平均的寫盤速度卻達到30M/s以上,通過這樣分析我們可能能猜出問題所在,我們的寫入的數據好像是匯總之后批量寫入的,而事實上確實是這樣,想要徹底理解這個問題首先要了解linux文件系統的機制,文件系統、緩存、緩沖以及硬件存儲之間的關系如下:
好吧,有了這張圖我們就能知道使用f.write其實是發起系統調用,由內核執行文件系統的寫入操作,數據是優先被寫到系統的緩沖區Buffer中,也就是內存中的一塊區域,等緩沖區滿了之后再刷到硬盤上,所以寫緩沖區基本上就是寫內存,速度和硬盤差了不是1個量級,也就是說此時系統調用的時間極短,系統將數據交給文件系統后剩下的就由文件系統完成操作即可,所以此時大量的CPU用在用戶代碼上也就是那個while循環上,因此內核占用率和iowait幾乎都沒有,這個時候可能你會想,加個flush不就完事了?然后我們修改代碼試一下每次都sync會怎么樣:
f = open('test', 'w')
while True:
f.write('abc')
f.flush()
查看CPU占用:
top - 09:02:32 up 88 days, 15:33, 3 users, load average: 0.28, 0.09, 0.10
Tasks: 143 total, 2 running, 141 sleeping, 0 stopped, 0 zombie
%Cpu0 : 2.2 us, 0.0 sy, 0.0 ni, 97.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 89.4 id, 6.4 wa, 0.0 hi, 4.3 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.0 ni, 97.8 id, 2.2 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 50.0 us, 50.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 4435.8 free, 8247.0 used, 19092.8 buff/cache
MiB Swap: 8192.0 total, 8191.0 free, 1.0 used. 23067.1 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
350655 root 20 0 16432 10140 6516 R 97.8 0.0 2:37.88 python3
查看硬盤寫入:
08/04/2021 09:03:58 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 0.00 0.00 0.00 0.00 0 0 0
08/04/2021 09:03:59 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 0.00 0.00 0.00 0.00 0 0 0
08/04/2021 09:04:00 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 34.00 0.00 17524.00 0.00 0 17524 0
08/04/2021 09:04:01 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 54.00 0.00 26848.00 0.00 0 26848 0
08/04/2021 09:04:02 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 11.00 0.00 432.00 0.00 0 432 0
08/04/2021 09:04:03 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 0.00 0.00 0.00 0.00 0 0 0
查看寫入平均帶寬:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 1447.25 K/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
350655 be/4 root 0.00 B/s 1447.25 K/s 0.00 % 0.00 % python3
情況又發生了變化,此時iowait也沒有上來,反而是用戶態和內核態各占用50%?磁盤的寫入確實比沒加flush更頻繁,寫入平均帶寬才1.5M/s左右比之前效率低了20倍,說明此時除了用戶態的開銷之外,大部分其實都花在系統調用上,由於每次調用flush都需要保存用戶棧,恢復內核棧,等待調用代碼執行完成后繼續保存內核棧,然后恢復用戶棧,同時要將數據從用戶空間拷貝至內核空間,然后由內核寫入到磁盤,這個操作主要就消耗在了內核空間上。那這個時候你可能會說怎么才能有I/O wait呢?原因是這里我們只寫了3個字節,這個數據太小了,以至於內核開銷得到放大,那我們寫入更長的數據試試:
f = open('test', 'w')
while True:
f.write('abc'*1024)
查看CPU:
top - 09:28:55 up 88 days, 15:59, 3 users, load average: 0.33, 0.11, 0.10
Tasks: 143 total, 2 running, 141 sleeping, 0 stopped, 0 zombie
%Cpu(s): 7.9 us, 18.0 sy, 0.0 ni, 30.5 id, 42.5 wa, 0.0 hi, 1.1 si, 0.0 st
MiB Mem : 31775.6 total, 1925.4 free, 8200.9 used, 21649.4 buff/cache
MiB Swap: 8192.0 total, 8191.0 free, 1.0 used. 23113.3 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
350655 root 20 0 44580 34292 15188 R 100.0 0.1 5:29.63 python3
磁盤:
08/04/2021 09:29:26 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 346.00 0.00 175696.00 0.00 0 175696 0
08/04/2021 09:29:27 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 249.00 0.00 147188.00 0.00 0 147188 0
08/04/2021 09:29:28 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 288.00 0.00 176144.00 0.00 0 176144 0
08/04/2021 09:29:29 AM
Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
sda3 310.00 0.00 180580.00 0.00 0 180580 0
平均帶寬:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 170.25 M/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 184.99 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
350756 be/4 root 0.00 B/s 0.00 B/s 0.00 % 97.78 % [kworker/u8:2+flush-253:0]
350655 be/4 root 0.00 B/s 170.25 M/s 0.00 % 68.76 % python3
現在終於模擬出了我們想要的結果,此時I/O wait也就是cpu的wa那一項占用了40%以上,查看磁盤的TPS和寫入都比較高並且很平穩,帶寬達到180M/s,因為我這里用的是機械盤,所以基本上到了極限,由於寫入的數據比較長因此內核緩沖區將會被頻繁刷新因此內核利用率也能達到18%,所謂某個瓶頸就是在這個方便的占比明顯超過其他方面,是一個相對的關系,這樣就可以根據這個主要方面來優化,你可以搜索一下Amdahl定律,是由計算機科學領域的先鋒Gene Amdahl提出,這個定律的精髓就是漸進加速比例的變化,通過解決所占比例最大的部分就可以獲得最高的性能提升比,你值得擁有!
那么除了寫入更長的字節還有沒有別的辦法來模擬?因為默認情況下文件系統的IO都是緩存I/O,還有一種IO叫做Direct I/O也就是直接I/O,這種情況程序可以直接訪問磁盤數據而不需要經過內核緩沖區,可以減少內核緩存和用戶程序之間的數據復制,舉例如下:
import os
import mmap
f = os.open('test', os.O_DIRECT|os.O_CREAT|os.O_RDWR|os.O_SYNC)
m = mmap.mmap(-1, 1024)
m.write(b'abc')
while True:
os.write(f, m)
這種情況到底能不能模擬出來I/O wait呢?你自己可以試一下就能得到答案。
其實用stress也可以模擬非直接io的情況,比如:
stress -d 1 --hdd-bytes 10M
這個命令就是通過文件系統寫入,使得內核利用率接近100%,你也可以自己試一下。
現在我們對I/O wait狀態就有一個相對清晰的認識了,但是有的時候I/O wait卻會被其他進程“屏蔽”掉,你知道這是什么原因嗎?其實這是操作系統為了充分利用cpu資源而采取的進程調度策略,比如早期我們的CPU可能只有1個核心,如果1個程序I/O wait特別高,另外1個程序計算更多一些,反正I/O等待的時候CPU也是等着,何不讓出資源給另外需要CPU的程序執行呢?所以另外的進程就獲得了CPU時間片,提高了用戶空間或者內核空間的利用率,因此I/O wait會下降,此時的現象就好像wa那一項被“屏蔽”掉了,給排查問題帶來一些困難,不過現在的CPU大都是多核,由於進程的上下文切換可能會調度到不同的核心,有些進程出現wa高的時候還是很容易能體現出來的,不過需要多觀察一會來確定,並不需要停掉其他進程。
6.用stress模擬線上的復雜
stress --cpu 1 --io 2 --vm 1
top - 10:09:39 up 88 days, 16:40, 3 users, load average: 3.21, 1.01, 0.44
Tasks: 147 total, 3 running, 144 sleeping, 0 stopped, 0 zombie
%Cpu0 : 1.3 us, 2.6 sy, 0.0 ni, 43.5 id, 47.7 wa, 0.0 hi, 4.9 si, 0.0 st
%Cpu1 : 1.9 us, 4.2 sy, 0.0 ni, 45.7 id, 44.7 wa, 0.0 hi, 3.5 si, 0.0 st
%Cpu2 : 23.7 us, 76.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31775.6 total, 12647.1 free, 8255.4 used, 10873.1 buff/cache
MiB Swap: 8192.0 total, 8191.0 free, 1.0 used. 23058.8 avail Mem
是不是真相了?
最后感謝您的耐心閱讀,這篇對於CPU性能分析的分享就到這里,希望能對你的工作帶來一點幫助!