Linux性能優化實戰學習筆記:第四十九講


一、上節回顧

上一期,我們一起梳理了,網絡時不時丟包的分析定位和優化方法。先簡單回顧一下。網絡丟包,通常會帶來嚴重的性能下降,特別是對 TCP 來說,丟包通常意味着網絡擁塞和重傳,
進而會導致網絡延遲增大以及吞吐量降低。

而分析丟包問題,還是用我們的老套路,從 Linux 網絡收發的流程入手,結合 TCP/IP 協議棧的原理來逐層分析。

其實,在排查網絡問題時,我們還經常碰到的一個問題,就是內核線程的 CPU 使用率很高。比如,在高並發的場景中,內核線程 ksoftirqd 的 CPU 使用率通常就會比較高。回顧一下前面學過
的 CPU 和網絡模塊,你應該知道,這是網絡收發的軟中斷導致的。

而要分析 ksoftirqd 這類 CPU 使用率比較高的內核線程,如果用我前面介紹過的那些分析方法,你一般需要借助於其他性能工具,進行輔助分析。

二、內核線程

既然要講內核線程的性能問題,在案例開始之前,我們就先來看看,有哪些常見的內核線程。我們知道,在 Linux 中,用戶態進程的“祖先”,都是 PID 號為 1 的 init 進程。比如,現在主
流的 Linux 發行版中,init 都是 systemd 進程;而其他的用戶態進程,會通過 systemd 來進行管理。

稍微想一下 Linux 中的各種進程,除了用戶態進程外,還有大量的內核態線程。按說內核態的線程,應該先於用戶態進程啟動,可是 systemd 只管理用戶態進程。那么,內核態線程又是誰來
管理的呢?

實際上,Linux 在啟動過程中,有三個特殊的進程,也就是 PID 號最小的三個進程:

  • 0 號進程為 idle 進程,這也是系統創建的第一個進程,它在初始化 1 號和 2 號進程后,演變為空閑任務。當 CPU 上沒有其他任務執行時,就會運行它。
  • 1 號進程為 init 進程,通常是 systemd 進程,在用戶態運行,用來管理其他用戶態進程。
  • 2 號進程為 kthreadd 進程,在內核態運行,用來管理內核線程

所以,要查找內核線程,我們只需要從 2 號進程開始,查找它的子孫進程即可。比如,你可以使用 ps 命令,來查找 kthreadd 的子進程:

ps -f --ppid 2 -p 2
UID         PID   PPID  C STIME TTY          TIME CMD
root          2      0  0 12:02 ?        00:00:01 [kthreadd]
root          9      2  0 12:02 ?        00:00:21 [ksoftirqd/0]
root         10      2  0 12:02 ?        00:11:47 [rcu_sched]
root         11      2  0 12:02 ?        00:00:18 [migration/0]
...
root      11094      2  0 14:20 ?        00:00:00 [kworker/1:0-eve]
root      11647      2  0 14:27 ?        00:00:00 [kworker/0:2-cgr]

從上面的輸出,你能夠看到,內核線程的名稱(CMD)都在中括號里(這一點,我們前面內容也有提到過)。所以,更簡單的方法,就是直接查找名稱包含中括號的進程。比如:

ps -ef | grep "\[.*\]"
root         2     0  0 08:14 ?        00:00:00 [kthreadd]
root         3     2  0 08:14 ?        00:00:00 [rcu_gp]
root         4     2  0 08:14 ?        00:00:00 [rcu_par_gp]
...

了解內核線程的基本功能,對我們排查問題有非常大的幫助。比如,我們曾經在軟中斷案例中提到過 ksoftirqd。它是一個用來處理軟中斷的內核線程,並且每個 CPU 上都有一個。

如果你知道了這一點,那么,以后遇到 ksoftirqd 的 CPU 使用高的情況,就會首先懷疑是軟中斷的問題,然后從軟中斷的角度來進一步分析。

其實,除了剛才看到的 kthreadd 和 ksoftirqd 外,還有很多常見的內核線程,我們在性能分析中都經常會碰到,比如下面這幾個內核線程。

  • kswapd0:用於內存回收。在 Swap 變高 案例中,我曾介紹過它的工作原理。
  • kworker:用於執行內核工作隊列,分為綁定 CPU (名稱格式為 kworker/CPU:ID)和未綁定 CPU(名稱格式為 kworker/uPOOL:ID)兩類。
  • migration:在負載均衡過程中,把進程遷移到 CPU 上。每個 CPU 都有一個 migration 內核線程。
  • jbd2/sda1-8:jbd 是 Journaling Block Device 的縮寫,用來為文件系統提供日志功能,以保證數據的完整性;名稱中的 sda1-8,表示磁盤分區名稱和設備號。每個使用了 ext4 文件系   統的磁盤分區,都會有一個 jbd2 內核線程。
  • pdflush:用於將內存中的臟頁(被修改過,但還未寫入磁盤的文件頁)寫入磁盤(已經在3.10 中合並入了 kworker 中)。

了解這幾個容易發生性能問題的內核線程,有助於我們更快地定位性能瓶頸。接下來,我們來看今天的案例。

三、案例准備

今天的案例還是基於 Ubuntu 18.04,同樣適用於其他的 Linux 系統。我使用的案例環境如下所示:

機器配置:2 CPU,8GB 內存。
預先安裝 docker、perf、hping3、curl 等工具,如 apt install docker.io linux-tools-common hping3。

本次案例用到兩台虛擬機,我畫了一張圖來表示它們的關系。

你需要打開兩個終端,分別登錄這兩台虛擬機中,並安裝上述工具。

注意,以下所有命令都默認以 root 用戶運行,如果你用普通用戶身份登陸系統,請運行 sudosu root 命令,切換到 root 用戶。

如果安裝過程有問題,你可以先上網搜索解決,實在解決不了的,記得在留言區向我提問。

到這里,准備工作就完成了。接下來,我們正式進入操作環節。

四、案例分析

安裝完成后,我們先在第一個終端,執行下面的命令運行案例,也就是一個最基本的 Nginx 應用:

# 運行 Nginx 服務並對外開放 80 端口
$ docker run -itd --name=nginx -p 80:80 nginx

然后,在第二個終端,使用 curl 訪問 Nginx 監聽的端口,確認 Nginx 正常啟動。假設192.168.0.30 是 Nginx 所在虛擬機的 IP 地址,運行 curl 命令后,你應該會看到下面這個輸出界面:

curl http://192.168.0.30/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

接着,還是在第二個終端中,運行 hping3 命令,模擬 Nginx 的客戶端請求:

# -S 參數表示設置 TCP 協議的 SYN(同步序列號),-p 表示目的端口為 80
# -i u10 表示每隔 10 微秒發送一個網絡幀
# 注:如果你在實踐過程中現象不明顯,可以嘗試把 10 調小,比如調成 5 甚至 1
$ hping3 -S -p 80 -i u10 192.168.0.30

現在,我們再回到第一個終端,你應該就會發現異常——系統的響應明顯變慢了。我們不妨執行top,觀察一下系統和進程的 CPU 使用情況:

top
top - 08:31:43 up 17 min,  1 user,  load average: 0.00, 0.00, 0.02
Tasks: 128 total,   1 running,  69 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.3 us,  0.3 sy,  0.0 ni, 66.8 id,  0.3 wa,  0.0 hi, 32.4 si,  0.0 st
%Cpu1  :  0.0 us,  0.3 sy,  0.0 ni, 65.2 id,  0.0 wa,  0.0 hi, 34.5 si,  0.0 st
KiB Mem :  8167040 total,  7234236 free,   358976 used,   573828 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  7560460 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    9 root      20   0       0      0      0 S   7.0  0.0   0:00.48 ksoftirqd/0
   18 root      20   0       0      0      0 S   6.9  0.0   0:00.56 ksoftirqd/1
 2489 root      20   0  876896  38408  21520 S   0.3  0.5   0:01.50 docker-containe
 3008 root      20   0   44536   3936   3304 R   0.3  0.0   0:00.09 top
    1 root      20   0   78116   9000   6432 S   0.0  0.1   0:11.77 systemd
 ...

實際測試截圖如下:

從 top 的輸出中,你可以看到,兩個 CPU 的軟中斷使用率都超過了 30%;而 CPU 使用率最高的進程,正好是軟中斷內核線程 ksoftirqd/0 和 ksoftirqd/1。

雖然,我們已經知道了 ksoftirqd 的基本功能,可以猜測是因為大量網絡收發,引起了 CPU 使用率升高;但它到底在執行什么邏輯,我們卻並不知道。

對於普通進程,我們要觀察其行為有很多方法,比如 strace、pstack、lsof 等等。但這些工具並不適合內核線程,比如,如果你用 pstack ,或者通過 /proc/pid/stack 查看 ksoftirqd/0(進程
號為 9)的調用棧時,分別可以得到以下輸出:

pstack 9
Could not attach to target 9: Operation not permitted.
detach: No such process

 

cat /proc/9/stack
[<0>] smpboot_thread_fn+0x166/0x170
[<0>] kthread+0x121/0x140
[<0>] ret_from_fork+0x35/0x40
[<0>] 0xffffffffffffffff

顯然,pstack 報出的是不允許掛載進程的錯誤;而 /proc/9/stack 方式雖然有輸出,但輸出中並沒有詳細的調用棧情況。

那還有沒有其他方法,來觀察內核線程 ksoftirqd 的行為呢?

既然是內核線程,自然應該用到內核中提供的機制。回顧一下我們之前用過的 CPU 性能工具,我想你肯定還記得 perf ,這個內核自帶的性能剖析工具。

perf 可以對指定的進程或者事件進行采樣,並且還可以用調用棧的形式,輸出整個調用鏈上的匯總信息。 我們不妨就用 perf ,來試着分析一下進程號為 9 的 ksoftirqd。

繼續在終端一中,執行下面的 perf record 命令;並指定進程號 9 ,以便記錄 ksoftirqd 的行為:

# 采樣 30s 后退出
$ perf record -a -g -p 9 -- sleep 30

稍等一會兒,在上述命令結束后,繼續執行 perf report 命令,你就可以得到 perf 的匯總報告。按上下方向鍵以及回車鍵,展開比例最高的 ksoftirqd 后,你就可以得到下面這個調用關系鏈圖:

實際測試截圖如下:

從這個圖中,你可以清楚看到 ksoftirqd 執行最多的調用過程。雖然你可能不太熟悉內核源碼,但通過這些函數,我們可以大致看出它的調用棧過程。

  • net_rx_action 和 netif_receive_skb,表明這是接收網絡包(rx 表示 receive)。
  • br_handle_frame ,表明網絡包經過了網橋(br 表示 bridge)。
  • br_nf_pre_routing ,表明在網橋上執行了 netfilter 的 PREROUTING(nf 表示 netfilter)。而我們已經知道 PREROUTING 主要用來執行 DNAT,所以可以猜測這里有 DNAT 發生。
  • br_pass_frame_up,表明網橋處理后,再交給橋接的其他橋接網卡進一步處理。比如,在新的網卡上接收網絡包、執行 netfilter 過濾規則等等。

我們的猜測對不對呢?實際上,我們案例最開始用 Docker 啟動了容器,而 Docker 會自動為容器創建虛擬網卡、橋接到 docker0 網橋並配置 NAT 規則。這一過程,如下圖所示:

當然了,前面 perf report 界面的調用鏈還可以繼續展開。但很不幸,我的屏幕不夠大,如果展開更多的層級,最后幾個層級會超出屏幕范圍。這樣,即使我們能看到大部分的調用過程,卻也
不能說明后面層級就沒問題。

那么,有沒有更好的方法,來查看整個調用棧的信息呢?

五、火焰圖

針對 perf 匯總數據的展示問題,Brendan Gragg 發明了火焰圖,通過矢量圖的形式,更直觀展示匯總結果。下圖就是一個針對 mysql 的火焰圖示例。

這張圖看起來像是跳動的火焰,因此也就被稱為火焰圖。要理解火焰圖,我們最重要的是區分清楚橫軸和縱軸的含義。

  • 橫軸表示采樣數和采樣比例。一個函數占用的橫軸越寬,就代表它的執行時間越長。同一層的多個函數,則是按照字母來排序。
  • 縱軸表示調用棧,由下往上根據調用關系逐個展開。換句話說,上下相鄰的兩個函數中,下面的函數,是上面函數的父函數。這樣,調用棧越深,縱軸就越高。

另外,要注意圖中的顏色,並沒有特殊含義,只是用來區分不同的函數。

火焰圖是動態的矢量圖格式,所以它還支持一些動態特性。比如,鼠標懸停到某個函數上時,就會自動顯示這個函數的采樣數和采樣比例。而當你用鼠標點擊函數時,火焰圖就會把該層及其上
的各層放大,方便你觀察這些處於火焰圖頂部的調用棧的細節。

上面 mysql 火焰圖的示例,就表示了 CPU 的繁忙情況,這種火焰圖也被稱為 on-CPU 火焰圖。

如果我們根據性能分析的目標來划分,火焰圖可以分為下面這幾種。

  • on-CPU 火焰圖:表示 CPU 的繁忙情況,用在 CPU 使用率比較高的場景中。
  • off-CPU 火焰圖:表示 CPU 等待 I/O、鎖等各種資源的阻塞情況。
  • 內存火焰圖:表示內存的分配和釋放情況。
  • 熱 / 冷火焰圖:表示將 on-CPU 和 off-CPU 結合在一起綜合展示。
  • 差分火焰圖:表示兩個火焰圖的差分情況,紅色表示增長,藍色表示衰減。差分火焰圖常用來比較不同場景和不同時期的火焰圖,以便分析系統變化前后對性能的影響情況。

了解了火焰圖的含義和查看方法后,接下來,我們再回到案例,運用火焰圖來觀察剛才 perfrecord 得到的記錄

六、火焰圖分析

首先,我們需要生成火焰圖。我們先下載幾個能從 perf record 記錄生成火焰圖的工具,這些工具都放在 https://github.com/brendangregg/FlameGraph 上面。你可以執行下面的命令來下載:

git clone https://github.com/brendangregg/FlameGraph
$ cd FlameGraph

安裝好工具后,要生成火焰圖,其實主要需要三個步驟:

1. 執行 perf script ,將 perf record 的記錄轉換成可讀的采樣記錄;
2. 執行 stackcollapse-perf.pl 腳本,合並調用棧信息;
3. 執行 flamegraph.pl 腳本,生成火焰圖。

不過,在 Linux 中,我們可以使用管道,來簡化這三個步驟的執行過程。假設剛才用 perfrecord 生成的文件路徑為 /root/perf.data,執行下面的命令,你就可以直接生成火焰圖:

perf script -i /root/perf.data | ./stackcollapse-perf.pl --all |  ./flamegraph.pl > ksoftirqd.svg

執行成功后,使用瀏覽器打開 ksoftirqd.svg ,你就可以看到生成的火焰圖了。如下圖所示:

實際測試截圖如下:

根據剛剛講過的火焰圖原理,這個圖應該從下往上看,沿着調用棧中最寬的函數來分析執行次數最多的函數。這兒看到的結果,其實跟剛才的 perf report 類似,但直觀了很多,中間這一團
火,很明顯就是最需要我們關注的地方。

我們順着調用棧由下往上看(順着圖中藍色箭頭),就可以得到跟剛才 perf report 中一樣的結果:

  1. 最開始,還是 net_rx_action 到 netif_receive_skb 處理網絡收包;
  2. 然后, br_handle_frame 到 br_nf_pre_routing ,在網橋中接收並執行 netfilter 鈎子函數;
  3. 再向上, br_pass_frame_up 到 netif_receive_skb ,從網橋轉到其他網絡設備又一次接收。

不過最后,到了 ip_forward 這里,已經看不清函數名稱了。所以我們需要點擊 ip_forward,展開最上面這一塊調用棧:

實際測試截圖如下:

這樣,就可以進一步看到 ip_forward 后的行為,也就是把網絡包發送出去。根據這個調用過程,再結合我們前面學習的網絡收發和 TCP/IP 協議棧原理,這個流程中的網絡接收、網橋以及
netfilter 調用等,都是導致軟中斷 CPU 升高的重要因素,也就是影響網絡性能的潛在瓶頸。不過,回想一下網絡收發的流程,你可能會覺得它缺了好多步驟。

比如,這個堆棧中並沒有 TCP 相關的調用,也沒有連接跟蹤 conntrack 相關的函數。實際上,這些流程都在其他更小的火焰中,你可以點擊上圖左上角的“Reset Zoom”,回到完整火焰圖
中,再去查看其他小火焰的堆棧。

所以,在理解這個調用棧時要注意。從任何一個點出發、縱向來看的整個調用棧,其實只是最頂端那一個函數的調用堆棧,而非完整的內核網絡執行流程。

另外,整個火焰圖不包含任何時間的因素,所以並不能看出橫向各個函數的執行次序。

到這里,我們就找出了內核線程 ksoftirqd 執行最頻繁的函數調用堆棧,而這個堆棧中的各層級函數,就是潛在的性能瓶頸來源。這樣,后面想要進一步分析、優化時,也就有了根據。

六、小結

今天這個案例,你可能會覺得比較熟悉。實際上,這個案例,正是我們專欄 CPU 模塊中的 軟中斷案例。

當時,我們從軟中斷 CPU 使用率的角度入手,用網絡抓包的方法找出了瓶頸來源,確認是測試機器發送的大量 SYN 包導致的。而通過今天的 perf 和火焰圖方法,我們進一步找出了軟中斷內
核線程的熱點函數,其實也就找出了潛在的瓶頸和優化方向。

其實,如果遇到的是內核線程的資源使用異常,很多常用的進程級性能工具並不能幫上忙。這時,你就可以用內核自帶的 perf 來觀察它們的行為,找出熱點函數,進一步定位性能瓶。當
然,perf 產生的匯總報告並不夠直觀,所以我也推薦你用火焰圖來協助排查。

實際上,火焰圖方法同樣適用於普通進程。比如,在分析 Nginx、MySQL 等各種應用場景的性能問題時,火焰圖也能幫你更快定位熱點函數,找出潛在性能問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM