(好文重發)阿里楊勇:淺談 Linux 高負載的系統化分析

本文簡介:

        講解 Linux Load 高如何排查的話題屬於老生常談了,但多數文章只是聚焦了幾個點,缺少整體排查思路的介紹。所謂 “授人以魚不如授人以漁"。本文試圖建立一個方法和套路,來幫助讀者對 Load 高問題排查有一個更全面的認識。

作者簡介

        楊勇 (Oliver Yang),Linux 內核工程師,來自阿里雲系統組(微信訂閱號:內核月談)。曾就職於 EMC,Sun 中國工程研究院,在存儲系統和 Solaris 內核開發領域工作。 近幾年來開始研究 Linux 內核,對 Linux 性能優化,內存管理子系統有濃厚興趣。他的聯繫方式: http://oliveryang.net/about。

        阿里雲系統團隊, (http://kernel.taobao.org) 是由原淘寶內核組擴建而成,2013年淘寶內核組響應阿里巴巴集團的號召,整建制轉入阿里雲,開始爲雲計算底層系統構建完善的系統支持。 阿里雲系統團隊是由一羣具有高度使命感和自我追求的內核開發人員組成,團隊中的大多數人,都是活躍的社區內核開發人員。目前的工作領域主要涉及(但不限於) Linux內核的內存管理、文件系統、網絡和內核維護構建,以及和內核相關聯的用戶態庫和工具。


講解 Linux Load 高如何排查的話題屬於老生常談了,但多數文章只是聚焦了幾個點,缺少整體排查思路的介紹。所謂 “授人以魚不如授人以漁"。本文試圖建立一個方法和套路,來幫助讀者對 Load 高問題排查有一個更全面的認識。

從消除誤解開始

沒有基線的 Load,是不靠譜的 Load

從接觸 Unix/Linux 系統管理的第一天起,很多人就開始接觸 System Load Average 這個監控指標了,然而,並非所有人都知道這個指標的真正含義。一般說來,經常能聽到以下誤解: - Load 高是 CPU 負載高......

傳統 Unix 於 Linux 設計不同。Unix 系統,Load 高就是可運行進程多引發的,但對 Linux 來說不是。對 Linux 來說 Load 高可能有兩種情況: - 系統中處於 R 狀態的進程數增加引發的 - 系統中處於 D 狀態的進程數增加引發的 - Loadavg 數值大於某個值就一定有問題......

Loadavg 的數值是相對值,受到 CPU 和 IO 設備多少的影響,甚至會受到某些軟件定義的虛擬資源的影響。Load 高的判斷需要基於某個歷史基線 (Baseline),不能無原則的跨系統去比較 Load。 - Load 高系統一定很忙.....

Load 高系統可以很忙,例如 CPU 負載高,CPU 很忙。但 Load 高,系統不都很忙,如 IO 負載高,磁盤可以很忙,但 CPU 可以比較空閒,如 iowait 高。這裏要注意,iowait 本質上是一種特殊的 CPU 空閒狀態。另一種 Load 高,可能 CPU 和磁盤外設都很空閒,可能支持鎖競爭引起的,這時候 CPU 時間裏,iowait 不高,但 idle 高。

Brendan Gregg 在最近的博客 [Linux Load Averages: Solving the Mystery] (http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html, Brendan Gregg: Linux Load Averages究竟是個什麼鬼?) 中,討論了 Unix 和 Linux Load Average 的差異,並且回朔到 24 年前 Linux 社區的討論,並找到了當時爲什麼 Linux 要修改 Unix Load Average 的定義。文章認爲,正是由於 Linux 引入的 D 狀態線程的計算方式,從而導致 Load 高的原因變得含混起來。因爲系統中引發 D 狀態切換的原因實在是太多了,絕非 IO 負載,鎖競爭這麼簡單!正是由於這種含混,Load 的數值更加難以跨系統,跨應用類型去比較。所有 Load 高低的依據,全都應該基於歷史的基線。本文無意過多搬運原文的內容,因此,進一步的細節,建議閱讀原文。

如何排查 Load 高的問題

如前所述,由於在 Linux 操作系統裏,Load 是一個定義及其含混的指標,排查 loadavg 高就是一個很複雜的過程。其基本思路就是,根據引起 Load 變化的根源是 R 狀態任務增多,還是 D 狀態任務增多,來進入到不同的流程。

這裏給出了 Load 增高的排查的一般套路,僅供參考:

在 Linux 系統裏,讀取 /proc/stat 文件,即可獲取系統中 R 狀態的進程數;但 D 狀態的任務數恐怕最直接的方式還是使用 ps 命令比較方便。而/proc/stat 文件裏 procs_blocked 則給出的是處於等待磁盤 IO 的進程數:

$cat /proc/stat 
.......
processes 50777849
procs_running 1
procs_blocked 0
......

通過簡單區分 R 狀態任務增多,還是 D 狀態任務增多,我們就可以進入到不同的排查流程裏。下面,我們就這個大圖的排查思路,做一個簡單的梳理。

R 狀態任務增多

即通常所說的 CPU 負載高。此類問題的排查定位主要思路是系統,容器,進程的運行時間分析上,找到在 CPU 上的熱點路徑,或者分析 CPU 的運行時間主要是在哪段代碼上。

CPU user  sys 時間的分佈通常能幫助人們快速定位與用戶態進程有關,還是與內核有關。另外,CPU 的 run queue 長度和調度等待時間,非主動的上下文切換 (nonvoluntary context switch) 次數都能幫助大致理解問題的場景。

因此,如果要將問題的場景關聯到相關的代碼,通常需要使用 perfsystemtap, ftrace 這種動態的跟蹤工具。

關聯到代碼路徑後,接下來的代碼時間分析過程中,代碼中的一些無效的運行時間也是分析中首要關注的,例如用戶態和內核態中的自旋鎖 (Spin Lock)。

當然,如果 CPU 上運行的都是有非常意義,非常有效率的代碼,那唯一要考慮的就是,是不是負載真得太大了。

D 狀態任務增多

根據 Linux 內核的設計, D 狀態任務本質上是 TASK_UNINTERRUPTIBLE 引發的主動睡眠,因此其可能性非常多。但是由於 Linux 內核 CPU 空閒時間上對 IO 棧引發的睡眠做了特殊的定義,即 iowait,因此 iowait 成爲 D 狀態分類裏定位是否 Load 高是由 IO 引發的一個重要參考。

當然,如前所述, /proc/stat 中的 procs_blocked 的變化趨勢也可以是一個非常好的判定因 iowait 引發的 Load 高的一個參考。

CPU iowait 高

很多人通常都對 CPU iowait 有一個誤解,以爲 iowait 高是因爲這時的 CPU 正在忙於做 IO 操作。其實恰恰相反, iowait 高的時候,CPU 正處於空閒狀態,沒有任何任務可以運行。只是因爲此時存在已經發出的磁盤 IO,因此這時的空閒狀態被標識成了 iowait ,而不是 idle

但此時,如果用 perf probe 命令,我們可以清楚得看到,在 iowait 狀態的 CPU,實際上是運行在 pid 爲 0 的 idle 線程上:

$ sudo perf probe -a account_idle_ticks
$sudo perf record -e probe:account_idle_ticks -ag sleep 1
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.418 MB perf.data (843 samples) ]

$sudo perf script
swapper     0 [013] 5911414.451891: probe:account_idle_ticks: (ffffffff810b6af0)
             2b6af1 account_idle_ticks (/lib/modules/3.10.0/build/vmlinux)
             2d65d9 cpu_startup_entry (/lib/modules/3.10.0/build/vmlinux)
             24840a start_secondary (/lib/modules/3.10.0/build/vmlinux)

相關的 idle 線程的循環如何分別對 CPU iowait  idle 計數的代碼,如下所示:

/*       

 * Account multiple ticks of idle time.

 * @ticks: number of stolen ticks

 */   

void account_idle_ticks(unsigned long ticks)

{        

    if (sched_clock_irqtime) {

        irqtime_account_idle_ticks(ticks);

        return;

    }   

    account_idle_time(jiffies_to_cputime(ticks)); 

}        

         

/*

 * Account for idle time.

 * @cputime: the cpu time spent in idle wait

 */

void account_idle_time(cputime_t cputime)

{

    u64 *cpustat = kcpustat_this_cpu->cpustat;

    struct rq *rq = this_rq();

 

    if (atomic_read(&rq->nr_iowait) > 0)

        cpustat[CPUTIME_IOWAIT] += (__force u64) cputime;

    else

        cpustat[CPUTIME_IDLE] += (__force u64) cputime;

}

而 Linux IO 棧和文件系統的代碼則會調用 io_schedule,等待磁盤 IO 的完成。這時候,對 CPU 時間被記爲 iowait 起關鍵計數的原子變量rq->nr_iowait 則會在睡眠前被增加。注意,io_schedule 在被調用前,通常 caller 會先將任務顯式地設置成 TASK_UNINTERRUPTIBLE 狀態:

/*           

 * This task is about to go to sleep on IO. Increment rq->nr_iowait so

 * that process accounting knows that this is a task in IO wait state.

 */          

void __sched io_schedule(void)

{

    io_schedule_timeout(MAX_SCHEDULE_TIMEOUT);

}            

EXPORT_SYMBOL(io_schedule);

             

long __sched io_schedule_timeout(long timeout)

{            

    int old_iowait = current->in_iowait;

    struct rq *rq; 

    long ret;

             

    current->in_iowait = 1; 

    if (old_iowait)

        blk_schedule_flush_plug(current);

    else 

        blk_flush_plug(current);

    delayacct_blkio_start();

    rq = raw_rq();

    atomic_inc(&rq->nr_iowait);

    ret = schedule_timeout(timeout);

    current->in_iowait = old_iowait;

    atomic_dec(&rq->nr_iowait);

    delayacct_blkio_end();

             

    return ret; 

}            

EXPORT_SYMBOL(io_schedule_timeout);

CPU idle 高

如前所述,有相當多的內核的阻塞,即 TASK_UNINTERRUPTIBLE 的睡眠,實際上與等待磁盤 IO 無關,如內核中的鎖競爭,再如內存直接頁回收的睡眠,又如內核中一些代碼路徑上的主動阻塞,等待資源。

Brendan Gregg 在最近的博客 [Linux Load Averages: Solving the Mystery] (http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html)中,使用 perf 命令產生的 TASK_UNINTERRUPTIBLE 的睡眠的火焰圖,很好的展示了引起 CPU idle 高的多樣性。本文不在贅述。

因此,CPU idle 高的分析,實質上就是分析內核的代碼路徑引起阻塞的主因是什麼。通常,我們可以使用 perf inject 對 perf record 記錄的上下文切換的事件進行處理,關聯出進程從 CPU 切出 (swtich out) 和再次切入 (switch in) 的內核代碼路徑,生成一個所謂的 Off CPU 火焰圖.

當然,類似於鎖競爭這樣的比較簡單的問題,Off CPU 火焰圖足以一步定位出問題。但是對於更加複雜的因 D 狀態而阻塞的延遲問題,可能 Off CPU 火焰圖只能給我們一個調查的起點。

例如,當我們看到,Off CPU 火焰圖的主要睡眠時間是因爲 epoll_wait 等待引發的。那麼,我們繼續要排查的應該是網絡棧的延遲,即本文大圖中的 Net Delay 這部分。

至此,你也許會發現,CPU iowait 和 idle 高的性能分析的實質就是 延遲分析。這就是大圖按照內核中資源管理的大方向,將延遲分析細化成了六大延遲分析:

  • CPU 延遲

  • 內存延遲

  • 文件系統延遲

  • IO 棧延遲

  • 網絡棧延遲

  • 鎖及同步原語競爭

任何上述代碼路徑引發的 TASK_UNINTERRUPTIBLE 的睡眠,都是我們要分析的對象!

以問題結束

限於篇幅,本文很難將其所涉及的細節一一展開,因爲讀到這裏,你也許會發現,原來 Load 高的分析,實際上就是對系統的全面負載分析。怪不得叫 System Load 呢。這也是 Load 分析爲什麼很難在一篇文章裏去全面覆蓋。

(完)

Linux閱碼場原創精華文章彙總

更多精彩,盡在"Linux閱碼場",掃描下方二維碼關注

如果您覺得文章對您有幫助,請點一點右下角“在看”吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章