08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下)
生活随笔
收集整理的這篇文章主要介紹了
08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下)
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
上一節(jié),我給你講了 Linux 進(jìn)程狀態(tài)的含義,以及不可中斷進(jìn)程和僵尸進(jìn)程產(chǎn)生的原因,我們先來簡單復(fù)習(xí)下。使用 ps 或者 top 可以查看進(jìn)程的狀態(tài),這些狀態(tài)包括運(yùn)行、空閑、不可中斷睡眠、可中斷睡眠、僵尸以及暫停等。其中,我們重點(diǎn)學(xué)習(xí)了不可中斷狀態(tài)和僵尸進(jìn)程:
- 不可中斷狀態(tài),一般表示進(jìn)程正在跟硬件交互,為了保護(hù)進(jìn)程數(shù)據(jù)與硬件一致,系統(tǒng)不允許其他進(jìn)程或中斷打斷該進(jìn)程。
- 僵尸進(jìn)程表示進(jìn)程已經(jīng)退出,但它的父進(jìn)程沒有回收該進(jìn)程所占用的資源。
- 第一,iowait 太高了,導(dǎo)致系統(tǒng)平均負(fù)載升高,并且已經(jīng)達(dá)到了系統(tǒng) CPU 的個(gè)數(shù)。
- 第二,僵尸進(jìn)程在不斷增多,看起來是應(yīng)用程序沒有正確清理子進(jìn)程的資源。
iowait 分析
我們先來看一下 iowait 升高的問題。我相信,一提到 iowait 升高,你首先會(huì)想要查詢系統(tǒng)的 I/O 情況。我一般也是這種思路,那么什么工具可以查詢系統(tǒng)的 I/O 情況呢?這里,我推薦的正是上節(jié)課要求安裝的 dstat ,它的好處是,可以同時(shí)查看 CPU 和 I/O 這兩種資源的使用情況,便于對比分析。那么,我們在終端中運(yùn)行 dstat 命令,觀察 CPU 和 I/O 的使用情況:# 間隔 1 秒輸出 10 組數(shù)據(jù) $ dstat 1 10 You did not select any stats, using -cdngy by default. --total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai stl| read writ| recv send| in out | int csw0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 8850 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 1380 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 1350 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 1770 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 1440 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 1470 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 1340 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 1310 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 1680 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134從 dstat 的輸出,我們可以看到,每當(dāng) iowait 升高(wai)時(shí),磁盤的讀請求(read)都會(huì)很大。這說明 iowait 的升高跟磁盤的讀請求有關(guān),很可能就是磁盤讀導(dǎo)致的。那到底是哪個(gè)進(jìn)程在讀磁盤呢?不知道你還記不記得,上節(jié)在 top 里看到的不可中斷狀態(tài)進(jìn)程,我覺得它就很可疑,我們試著來分析下。我們繼續(xù)在剛才的終端中,運(yùn)行 top 命令,觀察 D 狀態(tài)的進(jìn)程:# 觀察一會(huì)兒按 Ctrl+C 結(jié)束 $ top ...PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app ...我們從 top 的輸出找到 D 狀態(tài)進(jìn)程的 PID,你可以發(fā)現(xiàn),這個(gè)界面里有兩個(gè) D 狀態(tài)的進(jìn)程,PID 分別是 4344 和 4345。接著,我們查看這些進(jìn)程的磁盤讀寫情況。對了,別忘了工具是什么。一般要查看某一個(gè)進(jìn)程的資源使用情況,都可以用我們的老朋友 pidstat,不過這次記得加上 -d 參數(shù),以便輸出 I/O 使用情況。比如,以 4344 為例,我們在終端里運(yùn)行下面的 pidstat 命令,并用 -p 4344 參數(shù)指定進(jìn)程號:# -d 展示 I/O 統(tǒng)計(jì)數(shù)據(jù),-p 指定進(jìn)程號,間隔 1 秒輸出 3 組數(shù)據(jù) $ pidstat -d -p 4344 1 3 06:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:38:51 0 4344 0.00 0.00 0.00 0 app 06:38:52 0 4344 0.00 0.00 0.00 0 app 06:38:53 0 4344 0.00 0.00 0.00 0 app在這個(gè)輸出中, kB_rd 表示每秒讀的 KB 數(shù), kB_wr 表示每秒寫的 KB 數(shù),iodelay 表示 I/O 的延遲(單位是時(shí)鐘周期)。它們都是 0,那就表示此時(shí)沒有任何的讀寫,說明問題不是 4344 進(jìn)程導(dǎo)致的。可是,用同樣的方法分析進(jìn)程 4345,你會(huì)發(fā)現(xiàn),它也沒有任何磁盤讀寫。那要怎么知道,到底是哪個(gè)進(jìn)程在進(jìn)行磁盤讀寫呢?我們繼續(xù)使用 pidstat,但這次去掉進(jìn)程號,干脆就來觀察所有進(jìn)程的 I/O 使用情況。在終端中運(yùn)行下面的 pidstat 命令:# 間隔 1 秒輸出多組數(shù)據(jù) (這里是 20 組) $ pidstat -d 1 20 ... 06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1 06:48:47 0 6080 32768.00 0.00 0.00 170 app 06:48:47 0 6081 32768.00 0.00 0.00 184 app06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:48 0 6080 0.00 0.00 0.00 110 app06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:49 0 6081 0.00 0.00 0.00 191 app06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:51 0 6082 32768.00 0.00 0.00 0 app 06:48:51 0 6083 32768.00 0.00 0.00 0 app06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:52 0 6082 32768.00 0.00 0.00 184 app 06:48:52 0 6083 32768.00 0.00 0.00 175 app06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command 06:48:53 0 6083 0.00 0.00 0.00 105 app ...觀察一會(huì)兒可以發(fā)現(xiàn),的確是 app 進(jìn)程在進(jìn)行磁盤讀,并且每秒讀的數(shù)據(jù)有 32 MB,看來就是 app 的問題。不過,app 進(jìn)程到底在執(zhí)行啥 I/O 操作呢?這里,我們需要回顧一下進(jìn)程用戶態(tài)和內(nèi)核態(tài)的區(qū)別。進(jìn)程想要訪問磁盤,就必須使用系統(tǒng)調(diào)用,所以接下來,重點(diǎn)就是找出 app 進(jìn)程的系統(tǒng)調(diào)用了。strace 正是最常用的跟蹤進(jìn)程系統(tǒng)調(diào)用的工具。所以,我們從 pidstat 的輸出中拿到進(jìn)程的 PID 號,比如 6082,然后在終端中運(yùn)行 strace 命令,并用 -p 參數(shù)指定 PID 號:$ strace -p 6082 strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted這兒出現(xiàn)了一個(gè)奇怪的錯(cuò)誤,strace 命令居然失敗了,并且命令報(bào)出的錯(cuò)誤是沒有權(quán)限。按理來說,我們所有操作都已經(jīng)是以 root 用戶運(yùn)行了,為什么還會(huì)沒有權(quán)限呢?你也可以先想一下,碰到這種情況,你會(huì)怎么處理呢?一般遇到這種問題時(shí),我會(huì)先檢查一下進(jìn)程的狀態(tài)是否正常。比如,繼續(xù)在終端中運(yùn)行 ps 命令,并使用 grep 找出剛才的 6082 號進(jìn)程:$ ps aux | grep 6082 root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct>果然,進(jìn)程 6082 已經(jīng)變成了 Z 狀態(tài),也就是僵尸進(jìn)程。僵尸進(jìn)程都是已經(jīng)退出的進(jìn)程,所以就沒法兒繼續(xù)分析它的系統(tǒng)調(diào)用。關(guān)于僵尸進(jìn)程的處理方法,我們一會(huì)兒再說,現(xiàn)在還是繼續(xù)分析 iowait 的問題。到這一步,你應(yīng)該注意到了,系統(tǒng) iowait 的問題還在繼續(xù),但是 top、pidstat 這類工具已經(jīng)不能給出更多的信息了。這時(shí),我們就應(yīng)該求助那些基于事件記錄的動(dòng)態(tài)追蹤工具了。你可以用 perf top 看看有沒有新發(fā)現(xiàn)。再或者,可以像我一樣,在終端中運(yùn)行 perf record,持續(xù)一會(huì)兒(例如 15 秒),然后按 Ctrl+C 退出,再運(yùn)行 perf report 查看報(bào)告:$ perf record -g $ perf report接著,找到我們關(guān)注的 app 進(jìn)程,按回車鍵展開調(diào)用棧,你就會(huì)得到下面這張調(diào)用關(guān)系圖:這個(gè)圖里的 swapper 是內(nèi)核中的調(diào)度進(jìn)程,你可以先忽略掉。我們來看其他信息,你可以發(fā)現(xiàn), app 的確在通過系統(tǒng)調(diào)用 sys_read() 讀取數(shù)據(jù)。并且從 new_sync_read 和 blkdev_direct_IO 能看出,進(jìn)程正在對磁盤進(jìn)行直接讀,也就是繞過了系統(tǒng)緩存,每個(gè)讀請求都會(huì)從磁盤直接讀,這就可以解釋我們觀察到的 iowait 升高了。看來,罪魁禍?zhǔn)资?app 內(nèi)部進(jìn)行了磁盤的直接 I/O 啊!下面的問題就容易解決了。我們接下來應(yīng)該從代碼層面分析,究竟是哪里出現(xiàn)了直接讀請求。查看源碼文件 app.c,你會(huì)發(fā)現(xiàn)它果然使用了 O_DIRECT 選項(xiàng)打開磁盤,于是繞過了系統(tǒng)緩存,直接對磁盤進(jìn)行讀寫。open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)直接讀寫磁盤,對 I/O 敏感型應(yīng)用(比如數(shù)據(jù)庫系統(tǒng))是很友好的,因?yàn)槟憧梢栽趹?yīng)用中,直接控制磁盤的讀寫。但在大部分情況下,我們最好還是通過系統(tǒng)緩存來優(yōu)化磁盤 I/O,換句話說,刪除 O_DIRECT 這個(gè)選項(xiàng)就是了。app-fix1.c 就是修改后的文件,我也打包成了一個(gè)鏡像文件,運(yùn)行下面的命令,你就可以啟動(dòng)它了:# 首先刪除原來的應(yīng)用 $ docker rm -f app # 運(yùn)行新的應(yīng)用 $ docker run --privileged --name=app -itd feisky/app:iowait-fix1最后,再用 top 檢查一下:$ top top - 14:59:32 up 19 min, 1 user, load average: 0.15, 0.07, 0.05 Tasks: 137 total, 1 running, 72 sleeping, 0 stopped, 12 zombie %Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.0 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st ...PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND3084 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app3085 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app1 root 20 0 159848 9120 6724 S 0.0 0.1 0:09.03 systemd2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd3 root 20 0 0 0 0 I 0.0 0.0 0:00.40 kworker/0:0 ...你會(huì)發(fā)現(xiàn), iowait 已經(jīng)非常低了,只有 0.3%,說明剛才的改動(dòng)已經(jīng)成功修復(fù)了 iowait 高的問題,大功告成!不過,別忘了,僵尸進(jìn)程還在等著你。仔細(xì)觀察僵尸進(jìn)程的數(shù)量,你會(huì)郁悶地發(fā)現(xiàn),僵尸進(jìn)程還在不斷的增長中。僵尸進(jìn)程
接下來,我們就來處理僵尸進(jìn)程的問題。既然僵尸進(jìn)程是因?yàn)楦高M(jìn)程沒有回收子進(jìn)程的資源而出現(xiàn)的,那么,要解決掉它們,就要找到它們的根兒,也就是找出父進(jìn)程,然后在父進(jìn)程里解決。父進(jìn)程的找法我們前面講過,最簡單的就是運(yùn)行 pstree 命令:# -a 表示輸出命令行選項(xiàng) # p 表 PID # s 表示指定進(jìn)程的父進(jìn)程 $ pstree -aps 3084 systemd,1└─dockerd,15006 -H fd://└─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml└─docker-containe,3991 -namespace moby -workdir...└─app,4009└─(app,3084)運(yùn)行完,你會(huì)發(fā)現(xiàn) 3084 號進(jìn)程的父進(jìn)程是 4009,也就是 app 應(yīng)用。所以,我們接著查看 app 應(yīng)用程序的代碼,看看子進(jìn)程結(jié)束的處理是否正確,比如有沒有調(diào)用 wait() 或 waitpid() ,抑或是,有沒有注冊 SIGCHLD 信號的處理函數(shù)。現(xiàn)在我們查看修復(fù) iowait 后的源碼文件 app-fix1.c ,找到子進(jìn)程的創(chuàng)建和清理的地方:int status = 0;for (;;) {for (int i = 0; i < 2; i++) {if(fork()== 0) {sub_process();}}sleep(5);}while(wait(&status)>0);循環(huán)語句本來就容易出錯(cuò),你能找到這里的問題嗎?這段代碼雖然看起來調(diào)用了 wait() 函數(shù)等待子進(jìn)程結(jié)束,但卻錯(cuò)誤地把 wait() 放到了 for 死循環(huán)的外面,也就是說,wait() 函數(shù)實(shí)際上并沒被調(diào)用到,我們把它挪到 for 循環(huán)的里面就可以了。修改后的文件我放到了 app-fix2.c 中,也打包成了一個(gè) Docker 鏡像,運(yùn)行下面的命令,你就可以啟動(dòng)它:# 先停止產(chǎn)生僵尸進(jìn)程的 app $ docker rm -f app # 然后啟動(dòng)新的 app $ docker run --privileged --name=app -itd feisky/app:iowait-fix2 啟動(dòng)后,再用 top 最后來檢查一遍:$ top top - 15:00:44 up 20 min, 1 user, load average: 0.05, 0.05, 0.04 Tasks: 125 total, 1 running, 72 sleeping, 0 stopped, 0 zombie %Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st ...PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND3198 root 20 0 4376 840 780 S 0.3 0.0 0:00.01 app2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd3 root 20 0 0 0 0 I 0.0 0.0 0:00.41 kworker/0:0 ...好了,僵尸進(jìn)程(Z 狀態(tài))沒有了, iowait 也是 0,問題終于全部解決了。小結(jié)
今天我用一個(gè)多進(jìn)程的案例,帶你分析系統(tǒng)等待 I/O 的 CPU 使用率(也就是 iowait%)升高的情況。雖然這個(gè)案例是磁盤 I/O 導(dǎo)致了 iowait 升高,不過, iowait 高不一定代表 I/O 有性能瓶頸。當(dāng)系統(tǒng)中只有 I/O 類型的進(jìn)程在運(yùn)行時(shí),iowait 也會(huì)很高,但實(shí)際上,磁盤的讀寫遠(yuǎn)沒有達(dá)到性能瓶頸的程度。因此,碰到 iowait 升高時(shí),需要先用 dstat、pidstat 等工具,確認(rèn)是不是磁盤 I/O 的問題,然后再找是哪些進(jìn)程導(dǎo)致了 I/O。等待 I/O 的進(jìn)程一般是不可中斷狀態(tài),所以用 ps 命令找到的 D 狀態(tài)(即不可中斷狀態(tài))的進(jìn)程,多為可疑進(jìn)程。但這個(gè)案例中,在 I/O 操作后,進(jìn)程又變成了僵尸進(jìn)程,所以不能用 strace 直接分析這個(gè)進(jìn)程的系統(tǒng)調(diào)用。這種情況下,我們用了 perf 工具,來分析系統(tǒng)的 CPU 時(shí)鐘事件,最終發(fā)現(xiàn)是直接 I/O 導(dǎo)致的問題。這時(shí),再檢查源碼中對應(yīng)位置的問題,就很輕松了。而僵尸進(jìn)程的問題相對容易排查,使用 pstree 找出父進(jìn)程后,去查看父進(jìn)程的代碼,檢查 wait() / waitpid() 的調(diào)用,或是 SIGCHLD 信號處理函數(shù)的注冊就行了。 與50位技術(shù)專家面對面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的08 | 案例篇:系统中出现大量不可中断进程和僵尸进程怎么办?(下)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 07 | 案例篇:系统中出现大量不可中断
- 下一篇: 09 | 基础篇:怎么理解Linux软中