記錄《Linux效能優化》課程實踐過程。(可點選右方檢視目錄)
前言
等待 I/O 的 CPU 使用率(以下簡稱為 iowait)升高,也是最常見的一個伺服器效能問題。當 iowait 升高時,程式很可能因為得不到硬體的響應,而長時間處於不可中斷狀態。從 ps 或者 top 命令的輸出中,你可以發現它們都處於 D 狀態,也就是不可中斷狀態(Uninterruptible Sleep)。
程式狀態
top 和 ps 是最常用的檢視程式狀態的工具。如下:
常見的程式狀態有哪些?
- R 是 Running 或 Runnable 的縮寫,表示程式在 CPU 的就緒佇列中,正在執行或者正在等待執行。
- D 是 Disk Sleep 的縮寫,也就是不可中斷狀態睡眠(Uninterruptible Sleep),一般表示程式正在跟硬體互動,並且互動過程不允許被其他程式或中斷打斷。
- Z 是 Zombie 的縮寫,它表示殭屍程式,也就是程式實際上已經結束了,但是父程式還沒有回收它的資源(比如程式的描述符、PID 等)。
- S 是 Interruptible Sleep 的縮寫,也就是可中斷狀態睡眠,表示程式因為等待某個事件而被系統掛起。當程式等待的事件發生時,它會被喚醒並進入 R 狀態。
- I 是 Idle 的縮寫,也就是空閒狀態,用在不可中斷睡眠的核心執行緒上。前面說了,硬體互動導致的不可中斷程式用 D 表示,但對某些核心執行緒來說,它們有可能實際上並沒有任何負載,用 Idle 正是為了區分這種情況。要注意,D 狀態的程式會導致平均負載升高, I 狀態的程式卻不會。
除了以上 5 個狀態,程式還包括下面這 2 個狀態。
- 第一個是 T 或者 t,也就是 Stopped 或 Traced 的縮寫,表示程式處於暫停或者跟蹤狀態。
- 另一個是 X,也就是 Dead 的縮寫,表示程式已經消亡,所以你不會在 top 或者 ps 命令中看到它。
可以通過 man ps 檢視詳細解釋,如圖:
案例一
本案例是一個基於C開發的多程式應用,用來分析大量不可中斷狀態和殭屍狀態程式的問題。由於它的編譯和執行步驟比較麻煩,課程提供了 Docker 映象,只需要執行容器,就可以得到模擬環境。
環境準備
- 虛擬機器(Ubuntu 18.04)
- 機器配置:2 CPU,8GB 記憶體
- 預先安裝 docker、sysstat、dstat 等工具,如 apt install docker.io dstat sysstat
開啟終端,SSH 登入到機器上,並安裝上面提到的工具。
預設以 root 使用者執行所有命令,先執行 sudo su root 命令切換到 root 使用者。
執行docker環境
docker run --privileged --name=app -itd feisky/app:iowait
下載過程可能比較緩慢... 下載完成後,執行 docker ps -a ,如圖:
輸入 ps 命令,確認案例應用是否已正常啟動。如下:
$ ps aux | grep /app
圖中,S 表示可中斷睡眠狀態,D 表示不可中斷睡眠狀態。關於 s 和 +,s 表示該程式是一個會話的領導程式,而 + 表示前臺程式組,可以通過 man ps 檢視詳細解釋。
關於程式組和會話,它們是用來管理一組相互關聯的程式。
- 程式組表示一組相互關聯的程式,比如每個子程式都是父程式所在組的成員;
- 而會話是指共享同一個控制終端的一個或多個程式組。
應用啟動前後的CPU狀態
可以執行命令 docker stop app,停止應用。執行命令 top(按下數字 1 切換到所有 CPU 的使用情況),如圖:
執行命令 docker start app,然後 top,最好是觀察5分鐘以上,如圖:
可以觀察到,我的機器出現了一些變化:
- Load Average:過去 1 分鐘、5 分鐘和 15 分鐘內的平均負載在依次減小,說明平均負載正在升高;而 1 分鐘內的平均負載(2.18)已經達到系統的 CPU 個數,說明系統很可能已經有了效能瓶頸。
- Tasks:有 1 個正在執行的程式,但殭屍程式比較多(114),而且還在不停增加,說明有子程式在退出時沒被清理。
- 接下來看兩個 CPU 的使用率情況,使用者 CPU 使用率(us)不高,但系統CPU使用率(sy)和 iowait 有明顯的升高。
- 最後再看每個程式的情況, CPU 使用率最高的程式是兩個(Z)程式,高達30%多;觀察過程中發現,在演變成 Z 狀態之前,app 是處於 D 狀態的,它們可能在等待 I/O,但光憑這裡並不能確定是它們導致了 iowait 升高。
彙總上述情況:
- 第一點,啟動 app 應用程式後,iowait 確實升高了,系統的平均負載升高,甚至達到了系統 CPU 的個數。
- 第二點,殭屍程式在不斷增多,說明有程式沒能正確清理子程式的資源。
注意,一開始我的虛擬機器只有 1 CPU,發現觀察不到任何情況。於是,我重新安裝虛擬機器,併為其分配了 2 CPU。
案例二
案例一發現了一些負載問題,但具體根源是什麼?還得繼續分析。
環境準備
與案例一保持一致。
執行docker環境
如下:
# 先刪除上次啟動的案例
$ docker rm -f app
# 重新執行案例
$ docker run --privileged --name=app -itd feisky/app:iowait
Ⅰ. iowait 分析
先來看一下 iowait 升高的問題。
1. dstat 檢視系統I/O
那麼什麼工具可以查詢系統的 I/O 情況呢?這裡推薦使用 dstat ,它的好處是,可以同時檢視 CPU 和 I/O 這兩種資源的使用情況,便於對比分析。
# 間隔1秒輸出10組資料
$ dstat 1 10
可以使用 docker stop/start app,觀察應用啟動前後的變化,如圖:
可以看到,每當 iowait 升高(wai)時,對應的磁碟的讀請求(read)都會很大。這說明 iowait 的升高跟磁碟的讀請求有關,很可能就是磁碟讀導致的。
那到底是哪個程式在讀磁碟呢?再次關閉並重啟應用,top 觀察下輸出情況。
重點關注下 D 狀態的程式,因為它們可能在等待I/O。
2. pidstat 檢視程式磁碟讀寫
使用 pidstat 檢視這些程式的磁碟讀寫情況,如下:
# -d 展示 I/O 統計資料,-p 指定程式號,間隔 1 秒輸出 3 組資料
$ pidstat -d -p XXX 1 3
可以看到,這兩個程式目前並沒有任何磁碟讀寫。那要怎麼知道,到底是哪個程式在進行磁碟讀寫呢?
繼續使用 pidstat,這次觀察所有程式的 I/O 使用情況,如下:
# 間隔 1 秒輸出多組資料 (這裡是 20 組)
$ pidstat -d 1 20
如圖:
可以發現,的確是 app 程式在進行磁碟讀,並且每秒讀的資料有 320 MB,看來就是 app 的問題。
不過,app 程式到底在執行啥 I/O 操作呢?這裡,注意程式使用者態和核心態的區別,程式想要訪問磁碟,就必須使用系統呼叫。
接著,嘗試找出 app 程式的系統呼叫。
3. strace 跟蹤程式系統呼叫
strace 是常用的跟蹤程式系統呼叫的工具。可以用 -p 引數指定 PID 號:
$ strace -p 6680
圖中,提示 strace 命令執行失敗,沒有許可權。其實本身就處於 root 使用者,不應該沒有許可權的。
一般遇到這種問題時,先檢查一下程式的狀態是否正常。
$ ps aux | grep 6680
如圖:
可以看到,程式 6680 已經變成了 Z 狀態,也就是殭屍程式。殭屍程式都是已經退出的程式,所以就沒法繼續分析它的系統呼叫。
至此,發現 top、pidstat 工具無法給出更多資訊。這時,可以求助那些基於事件記錄的動態追蹤工具,比如 perf。
4. perf 分析系統 CPU 時鐘事件
如下:
# 記錄效能事件,等待大約15秒後按 Ctrl+C 退出
$ perf record -g
# 檢視報告
$ perf report
保持 app 正常啟動狀態,執行以上命令。(如果提示命令 not found,按提示安裝即可)
如圖:
圖中的 swapper 是核心中的排程程式,可以先忽略掉。
可以發現, app 的確在通過系統呼叫 sys_read() 讀取資料。並且從 new_sync_read 和 blkdev_direct_IO 能看出,程式正在對磁碟進行直接讀,也就是繞過了系統快取,每個讀請求都會從磁碟直接讀,這就可以解釋我們觀察到的 iowait 升高了。
關於 blkdev_direct_IO 可能會有點疑惑,可以查閱相關資料瞭解一下:
找到了問題的根源,那麼可以從程式碼層面,看是否能找到 直接I/O 的操作?
由於 app 是經過編譯後的 C 應用程式,所以還是直接看下課程提供的 C 原始碼 ,具體呼叫位置如下:
// O_DIRECT 選項開啟磁碟,於是繞過了系統快取,直接對磁碟進行讀寫。
int fd = open(disk, O_RDONLY | O_DIRECT | O_LARGEFILE, 0755);
5. 優化 iowait 問題
直接讀寫磁碟,對 I/O 敏感型應用(比如資料庫系統)是很友好的,因為你可以在應用中,直接控制磁碟的讀寫。但在大部分情況下,我們最好還是通過系統快取來優化磁碟 I/O,換句話說,刪除 O_DIRECT 這個選項就是了。
重新下載並啟動優化後的映象,如下:
# 首先刪除原來的應用
$ docker rm -f app
# 執行新的應用
$ docker run --privileged --name=app -itd feisky/app:iowait-fix1
重新 top 檢視最新輸出情況,如圖:
可以發現,sy、iowait 確實有明顯的下降。不過,zombie 程式的數量還是在不斷上升的。
Ⅱ. 殭屍程式
接下來,處理殭屍程式的問題。既然殭屍程式是因為父程式沒有回收子程式的資源而出現的,那麼,要解決掉它們,就要找到它們的根兒,也就是找出父程式,然後在父程式裡解決。
1. pstree 分析程式呼叫關係
# -a 表示輸出命令列選項 -p 表PID -s 表示指定程式的父程式
$ pstree -aps XXX
嘗試找下 PID 5638 的父程式,如圖:
執行完,你會發現 5638 號程式的父程式是 5630,也就是 app 應用。
所以,我們接著檢視 app 應用程式的程式碼,看看子程式結束的處理是否正確,比如有沒有呼叫 wait() 或 waitpid() ,抑或是,有沒有註冊 SIGCHLD 訊號的處理函式。
關於 父程式、子程式 可能會有點疑惑,可以查閱相關資料瞭解一下:
2. 檢視原始碼
檢視修復 iowait 後的原始碼檔案 app-fix1.c ,找到子程式的建立和清理的地方:
int status = 0;
// 死迴圈
for (;;) {
for (i = 0; i < 2; i++) {
if (fork() == 0) {
sub_process(disk, buffer_size, buffer_count);
}
}
sleep(5);
}
while (wait(&status) > 0);
很明顯,wait() 被放到了 for 死迴圈的外面,也就是說,wait() 函式實際上並沒被呼叫到。
3. 優化原始碼
修復程式碼,如下:
int status = 0;
// 死迴圈
for (;;) {
for (i = 0; i < 2; i++) {
if (fork() == 0) {
sub_process(disk, buffer_size, buffer_count);
}
}
while (wait(&status) > 0);
sleep(5);
}
4. 檢視優化效果
# 先停止產生殭屍程式的 app
$ docker rm -f app
# 然後啟動新的 app
$ docker run --privileged --name=app -itd feisky/app:iowait-fix2
重新 top 檢視最新輸出情況,如圖:
可以看到,已經不再有殭屍程式堆積了。
結語
雖然本文案例是基於 C 語言,但我認為,解決問題的思路都是通用的。如果其他語言的應用遇到了類似問題,比如 PHP
應用、Java應用,是不是也可以類似地去解決呢?
理論結合實踐,更能加深對知識的理解。如果有不明白的,也可以在評論區留言。
同時,我會把更多實踐案例歸納在 Linux 效能優化筆記 文章底部,歡迎閱讀。:smile:
本作品採用《CC 協議》,轉載必須註明作者和本文連結