淺談 Linux 高負(fù)載的系統(tǒng)化分析
講解 Linux Load 高如何排查的話題屬于老生常談了,但多數(shù)文章只是聚焦了幾個(gè)點(diǎn),缺少整體排查思路的介紹。所謂 “授人以魚不如授人以漁”。本文試圖建立一個(gè)方法和套路,來(lái)幫助讀者對(duì) Load 高問(wèn)題排查有一個(gè)更全面的認(rèn)識(shí)。
從消除誤解開始
沒有基線的 Load,是不靠譜的 Load
從接觸 Unix/Linux 系統(tǒng)管理的第一天起,很多人就開始接觸 System Load Average 這個(gè)監(jiān)控指標(biāo)了,然而,并非所有人都知道這個(gè)指標(biāo)的真正含義。一般說(shuō)來(lái),經(jīng)常能聽到以下誤解:
Load 高是 CPU 負(fù)載高……
傳統(tǒng) Unix 于 Linux 設(shè)計(jì)不同。Unix 系統(tǒng),Load 高就是可運(yùn)行進(jìn)程多引發(fā)的,但對(duì) Linux 來(lái)說(shuō)不是。對(duì) Linux 來(lái)說(shuō) Load 高可能有兩種情況:
系統(tǒng)中處于 R 狀態(tài)的進(jìn)程數(shù)增加引發(fā)的
系統(tǒng)中處于 D 狀態(tài)的進(jìn)程數(shù)增加引發(fā)的
Loadavg 數(shù)值大于某個(gè)值就一定有問(wèn)題……
Loadavg 的數(shù)值是相對(duì)值,受到 CPU 和 IO 設(shè)備多少的影響,甚至?xí)艿侥承┸浖x的虛擬資源的影響。Load 高的判斷需要基于某個(gè)歷史基線 (Baseline),不能無(wú)原則的跨系統(tǒng)去比較 Load。
Load 高系統(tǒng)一定很忙…..
Load 高系統(tǒng)可以很忙,例如 CPU 負(fù)載高,CPU 很忙。但 Load 高,系統(tǒng)不都很忙,如 IO 負(fù)載高,磁盤可以很忙,但 CPU 可以比較空閑,如 iowait 高。這里要注意,iowait 本質(zhì)上是一種特殊的 CPU 空閑狀態(tài)。另一種 Load 高,可能 CPU 和磁盤外設(shè)都很空閑,可能支持鎖競(jìng)爭(zhēng)引起的,這時(shí)候 CPU 時(shí)間里,iowait 不高,但 idle 高。
Brendan Gregg 在最近的博客 [Linux Load Averages: Solving the Mystery] (http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html) 中,討論了 Unix 和 Linux Load Average 的差異,并且回朔到 24 年前 Linux 社區(qū)的討論,并找到了當(dāng)時(shí)為什么 Linux 要修改 Unix Load Average 的定義。文章認(rèn)為,正是由于 Linux 引入的 D 狀態(tài)線程的計(jì)算方式,從而導(dǎo)致 Load 高的原因變得含混起來(lái)。因?yàn)橄到y(tǒng)中引發(fā) D 狀態(tài)切換的原因?qū)嵲谑翘嗔?,絕非 IO 負(fù)載,鎖競(jìng)爭(zhēng)這么簡(jiǎn)單!正是由于這種含混,Load 的數(shù)值更加難以跨系統(tǒng),跨應(yīng)用類型去比較。所有 Load 高低的依據(jù),全都應(yīng)該基于歷史的基線。本微信公眾號(hào)也曾寫過(guò)一篇相關(guān)文章,可以參見Linux Load Average那些事兒。
如何排查 Load 高的問(wèn)題
如前所述,由于在 Linux 操作系統(tǒng)里,Load 是一個(gè)定義及其含混的指標(biāo),排查 loadavg 高就是一個(gè)很復(fù)雜的過(guò)程。其基本思路就是,根據(jù)引起 Load 變化的根源是 R 狀態(tài)任務(wù)增多,還是 D 狀態(tài)任務(wù)增多,來(lái)進(jìn)入到不同的流程。
這里給出了 Load 增高的排查的一般套路,僅供參考:
在 Linux 系統(tǒng)里,讀取 /proc/stat 文件,即可獲取系統(tǒng)中 R 狀態(tài)的進(jìn)程數(shù);但 D 狀態(tài)的任務(wù)數(shù)恐怕最直接的方式還是使用 ps 命令比較方便。而 /proc/stat 文件里 procs_blocked 則給出的是處于等待磁盤 IO 的進(jìn)程數(shù):
通過(guò)簡(jiǎn)單區(qū)分 R 狀態(tài)任務(wù)增多,還是 D 狀態(tài)任務(wù)增多,我們就可以進(jìn)入到不同的排查流程里。下面,我們就這個(gè)大圖的排查思路,做一個(gè)簡(jiǎn)單的梳理。
R 狀態(tài)任務(wù)增多
即通常所說(shuō)的 CPU 負(fù)載高。此類問(wèn)題的排查定位主要思路是系統(tǒng),容器,進(jìn)程的運(yùn)行時(shí)間分析上,找到在 CPU 上的熱點(diǎn)路徑,或者分析 CPU 的運(yùn)行時(shí)間主要是在哪段代碼上。
CPU user 和 sys 時(shí)間的分布通常能幫助人們快速定位與用戶態(tài)進(jìn)程有關(guān),還是與內(nèi)核有關(guān)。另外,CPU 的 run queue 長(zhǎng)度和調(diào)度等待時(shí)間,非主動(dòng)的上下文切換 (nonvoluntary context switch) 次數(shù)都能幫助大致理解問(wèn)題的場(chǎng)景。
因此,如果要將問(wèn)題的場(chǎng)景關(guān)聯(lián)到相關(guān)的代碼,通常需要使用 perf,systemtap, ftrace 這種動(dòng)態(tài)的跟蹤工具。
關(guān)聯(lián)到代碼路徑后,接下來(lái)的代碼時(shí)間分析過(guò)程中,代碼中的一些無(wú)效的運(yùn)行時(shí)間也是分析中首要關(guān)注的,例如用戶態(tài)和內(nèi)核態(tài)中的自旋鎖 (Spin Lock)。
當(dāng)然,如果 CPU 上運(yùn)行的都是有非常意義,非常有效率的代碼,那唯一要考慮的就是,是不是負(fù)載真得太大了。
D 狀態(tài)任務(wù)增多
根據(jù) Linux 內(nèi)核的設(shè)計(jì), D 狀態(tài)任務(wù)本質(zhì)上是 TASK_UNINTERRUPTIBLE 引發(fā)的主動(dòng)睡眠,因此其可能性非常多。但是由于 Linux 內(nèi)核 CPU 空閑時(shí)間上對(duì) IO 棧引發(fā)的睡眠做了特殊的定義,即 iowait,因此iowait 成為 D 狀態(tài)分類里定位是否 Load 高是由 IO 引發(fā)的一個(gè)重要參考。
當(dāng)然,如前所述, /proc/stat 中的 procs_blocked 的變化趨勢(shì)也可以是一個(gè)非常好的判定因 iowait引發(fā)的 Load 高的一個(gè)參考。
CPU iowait 高
很多人通常都對(duì) CPU iowait 有一個(gè)誤解,以為 iowait 高是因?yàn)檫@時(shí)的 CPU 正在忙于做 IO 操作。其實(shí)恰恰相反, iowait 高的時(shí)候,CPU 正處于空閑狀態(tài),沒有任何任務(wù)可以運(yùn)行。只是因?yàn)榇藭r(shí)存在已經(jīng)發(fā)出的磁盤 IO,因此這時(shí)的空閑狀態(tài)被標(biāo)識(shí)成了 iowait ,而不是 idle。
但此時(shí),如果用 perf probe 命令,我們可以清楚得看到,在 iowait 狀態(tài)的 CPU,實(shí)際上是運(yùn)行在 pid 為 0 的 idle 線程上:
相關(guān)的 idle 線程的循環(huán)如何分別對(duì) CPU iowait 和 idle 計(jì)數(shù)的代碼,如下所示:
而 Linux IO 棧和文件系統(tǒng)的代碼則會(huì)調(diào)用 io_schedule,等待磁盤 IO 的完成。這時(shí)候,對(duì) CPU 時(shí)間被記為 iowait 起關(guān)鍵計(jì)數(shù)的原子變量 rq->nr_iowait 則會(huì)在睡眠前被增加。注意,io_schedule 在被調(diào)用前,通常 caller 會(huì)先將任務(wù)顯式地設(shè)置成 TASK_UNINTERRUPTIBLE 狀態(tài):
CPU idle 高
如前所述,有相當(dāng)多的內(nèi)核的阻塞,即 TASK_UNINTERRUPTIBLE 的睡眠,實(shí)際上與等待磁盤 IO 無(wú)關(guān),如內(nèi)核中的鎖競(jìng)爭(zhēng),再如內(nèi)存直接頁(yè)回收的睡眠,又如內(nèi)核中一些代碼路徑上的主動(dòng)阻塞,等待資源。
Brendan Gregg 在最近的博客 [Linux Load Averages: Solving the Mystery] (http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html)中,使用 perf 命令產(chǎn)生的 TASK_UNINTERRUPTIBLE 的睡眠的火焰圖,很好的展示了引起 CPU idle 高的多樣性。本文不在贅述。
因此,CPU idle 高的分析,實(shí)質(zhì)上就是分析內(nèi)核的代碼路徑引起阻塞的主因是什么。通常,我們可以使用 perf inject 對(duì) perf record 記錄的上下文切換的事件進(jìn)行處理,關(guān)聯(lián)出進(jìn)程從 CPU 切出 (swtich out) 和再次切入 (switch in) 的內(nèi)核代碼路徑,生成一個(gè)所謂的 Off CPU 火焰圖.
當(dāng)然,類似于鎖競(jìng)爭(zhēng)這樣的比較簡(jiǎn)單的問(wèn)題,Off CPU 火焰圖足以一步定位出問(wèn)題。但是對(duì)于更加復(fù)雜的因 D 狀態(tài)而阻塞的延遲問(wèn)題,可能 Off CPU 火焰圖只能給我們一個(gè)調(diào)查的起點(diǎn)。
例如,當(dāng)我們看到,Off CPU 火焰圖的主要睡眠時(shí)間是因?yàn)?epoll_wait 等待引發(fā)的。那么,我們繼續(xù)要排查的應(yīng)該是網(wǎng)絡(luò)棧的延遲,即本文大圖中的 Net Delay 這部分。
至此,你也許會(huì)發(fā)現(xiàn),CPU iowait 和 idle 高的性能分析的實(shí)質(zhì)就是 延遲分析。這就是大圖按照內(nèi)核中資源管理的大方向,將延遲分析細(xì)化成了六大延遲分析:
CPU 延遲
內(nèi)存延遲
文件系統(tǒng)延遲
IO 棧延遲
網(wǎng)絡(luò)棧延遲
鎖及同步原語(yǔ)競(jìng)爭(zhēng)
任何上述代碼路徑引發(fā)的 TASK_UNINTERRUPTIBLE 的睡眠,都是我們要分析的對(duì)象!
以問(wèn)題結(jié)束
限于篇幅,本文很難將其所涉及的細(xì)節(jié)一一展開,因?yàn)樽x到這里,你也許會(huì)發(fā)現(xiàn),原來(lái) Load 高的分析,實(shí)際上就是對(duì)系統(tǒng)的全面負(fù)載分析。怪不得叫 System Load 呢。這也是 Load 分析為什么很難在一篇文章里去全面覆蓋。