剖析《Linux 平均負載:解開謎團》【轉】

轉自:https://blog.csdn.net/cs_tech/article/details/126563993

目錄

原文翻譯

歷史

這3個數字

Linux Uninterruptible Tasks

尋找一個古老的 Linux 補丁

uninterruptible 的由來

今天的 uninterruptible

測量 uninterruptible tasks

分解 Linux 平均負載

理解 Linux 平均負載

什麼是“好”或“壞”的平均負載?

更好的指標

結論

剖析

指數加權移動平均

什麼是指數加權移動平均?

指數加權平均的作用是什麼?

Linux 計算平均負載的代碼怎麼實現?

分解 Linux 平均負載的背後邏輯是什麼?

本文是閱讀 Brendan 的傑作 Linux Load Averages: Solving the Mystery 後的一些總結,歡迎大家指教。

原文翻譯
負載平均數是一個行業關鍵指標--我的公司花費了數百萬美元根據它和其他指標自動擴展雲實例--但在Linux上,它有一些神祕的地方。Linux負載平均數不僅跟蹤 runnable 的任務,而且還跟蹤處於 uninterruptible sleep 狀態的任務。爲什麼呢?我從來沒有看到過一個解釋。在這篇文章中,我將解開這個謎團,並對負載平均數進行總結,爲每個試圖解釋它們的人提供參考。

Linux 負載平均值是“系統負載平均值”,它顯示了系統上正在運行的線程(任務)需求,是正在運行的線程加上等待的線程的平均數量。這可以衡量需求,這可能大於系統當前正在處理的數量。大多數工具顯示三個平均值,分別爲 1、5 和 15 分鐘:

:/proc # uptime
21:09:38 up 6:51, 0 users, load average: 2.58, 2.64, 2.43
:/proc # cat loadavg
2.58 2.64 2.43 3/7364 29138
一些解釋:

如果平均數是0.0,那麼你的系統是空閒的。

如果1分鐘的平均數高於5分鐘或15分鐘的平均數,那麼負載在增加。

如果1分鐘的平均數低於5分鐘或15分鐘的平均數,那麼負載在減少。

如果它們高於你的CPU數量,那麼你可能有一個性能問題(這取決於)。

作爲一個三元組,您可以判斷負載是增加還是減少,這很有用。當需要單個需求值時,它們也很有用,例如雲自動縮放規則。但是如果沒有其他指標的幫助,很難更詳細地理解它們。 23 - 25 的單個值本身沒有任何意義,但如果 CPU 數量已知並且已知它是受 CPU 限制的工作負載,則可能意味着什麼。

我通常不會嘗試調試負載平均值,而是切換到其他指標。我將在接近結尾的“更好的指標”部分討論這些。

歷史
原始負載平均值僅顯示 CPU 需求:正在運行的進程數加上等待運行的進程數。在 1973 年 8 月的 RFC 546 標題爲“TENEX Load Averages”中有一個很好的描述:

[1] TENEX負載平均值是對CPU需求的一種衡量。負載平均值是指在給定時間段內可運行進程數量的平均值。例如,每小時負荷平均值爲10,這意味着(對於一個單CPU系統)在該小時內的任何時候,人們可以看到1個正在運行的進程和其他9個準備運行的進程(即,沒有因爲 I/O 而阻塞)等待CPU。

ietf.org 上的這個版本鏈接到 1973 年 7 月手繪負載平均圖的 PDF 掃描,顯示這已經被監控了幾十年:

 

 

 

 


source: https://tools.ietf.org/html/rfc546
​如今,舊操作系統的源代碼也可以在網上找到。這是來自 TENEX(1970 年代初期)SCHED.MAC 的 DEC 宏程序集的一個例外:

NRJAVS==3 ;NUMBER OF LOAD AVERAGES WE MAINTAIN
GS RJAV,NRJAVS ;EXPONENTIAL AVERAGES OF NUMBER OF ACTIVE PROCESSES
[...]
;UPDATE RUNNABLE JOB AVERAGES

DORJAV: MOVEI 2,^D5000
MOVEM 2,RJATIM ;SET TIME OF NEXT UPDATE
MOVE 4,RJTSUM ;CURRENT INTEGRAL OF NBPROC+NGPROC
SUBM 4,RJAVS1 ;DIFFERENCE FROM LAST UPDATE
EXCH 4,RJAVS1
FSC 4,233 ;FLOAT IT
FDVR 4,[5000.0] ;AVERAGE OVER LAST 5000 MS
[...]
;TABLE OF EXP(-T/C) FOR T = 5 SEC.

EXPFF: EXP 0.920043902 ;C = 1 MIN
EXP 0.983471344 ;C = 5 MIN
EXP 0.994459811 ;C = 15 MIN
下面是今天 Linux 的一個摘錄(include/linux/sched/loadavg.h):

#define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */
#define EXP_5 2014 /* 1/exp(5sec/5min) */
#define EXP_15 2037 /* 1/exp(5sec/15min) */
Linux 還對 1、5 和 15 分鐘常量進行了硬編碼。

在舊系統中也有類似的平均負載指標,包括 Multics,它具有指數調度隊列平均值。

這3個數字
這三個數字是 1、5 和 15 分鐘的平均負載。除了它們不是真正的平均值,它們也不是 1、5 和 15 分鐘。從上面的源代碼中可以看出,1、5 和 15 分鐘是方程中使用的常數,用於計算 5 秒平均值的指數加權移動平均。生成的 1、5 和 15 分鐘負載平均值反映了遠遠超過 1、5 和 15 分鐘的負載。

如果您使用一個空閒系統,然後開始一個單線程 CPU 密集型工作負載(一個循環中的一個線程),那麼 60 秒後一分鐘的平均負載是多少?如果它是一個普通的平均值,它將是 1.0。這是那個實驗,圖表:

 

 


負載平均實驗以可視化指數阻尼
所謂的“一分鐘平均值”,到一分鐘大關才達到0.62左右。有關方程和類似實驗的更多信息,Neil Gunther 博士寫了一篇關於平均負載的文章:How It Works,此外 loadavg.c 中有許多 Linux 源代碼塊註釋。

Linux Uninterruptible Tasks
當負載平均值首次出現在 Linux 中時,它們反映了 CPU 需求,與其他操作系統一樣。但後來在 Linux 上將它們更改爲不僅包括可運行(runnable)任務,還包括處於不可中斷狀態的任務(TASK_UNINTERRUPTIBLE 或 nr_uninterruptible)。此狀態由希望避免信號中斷的代碼路徑使用,其中包括在磁盤 I/O 上阻塞的任務和一些鎖。您之前可能已經看到過這種狀態:它在輸出 ps 和 top 中顯示爲“D”狀態。 ps(1) 手冊頁稱其爲“不間斷睡眠(通常爲 IO)”。

添加不間斷狀態意味着 Linux 平均負載可能會因磁盤(或 NFS)I/O 工作負載而增加,而不僅僅是 CPU 需求。對於熟悉其他操作系統及其 CPU 平均負載的每個人來說,包括這種狀態,起初都令人深感困惑。

爲什麼?確切地說,爲什麼 Linux 會這樣做?

有無數關於負載平均數的文章,其中許多指出了Linux nr_uninterruptible 的問題。但我沒有看到任何一篇文章解釋或甚至猜測爲什麼它被包括在內。我自己的猜測是,它是爲了反映更廣泛意義上的需求,而不僅僅是CPU需求。

尋找一個古老的 Linux 補丁
理解爲什麼Linux中的東西會發生變化是很容易的:你可以閱讀有關文件的git提交歷史,並閱讀變化描述。我檢查了 loadavg.c 的歷史,但增加不間斷狀態的改動比該文件早,而該文件是用先前文件的代碼創建的。我檢查了另一個文件,但那條線索也很冷:代碼本身已經在不同的文件中跳來跳去。爲了走捷徑,我轉儲了整個Linux github倉庫的 "git log -p",它有4G字節的文本,並開始向後閱讀它,看看代碼何時首次出現。這也是一個死衚衕。整個Linux版本中最古老的變化可以追溯到2005年,當時Linus導入了Linux 2.6.12-rc2,而這個變化早於這個時間。

有一些歷史上的Linux版本(這裏 和 那裏),但是這些版本中也沒有這個變化的描述。我試圖發現,至少這個變化是什麼時候發生的,我在 kernel.org上搜索了tarballs,發現它在0.99.15時發生了變化,而不是在0.99.13時--然而,0.99.14的tarball卻不見了。我在其他地方找到了它,並確認該變化是在Linux 0.99 patchlevel 14,1993年11月。我希望Linus對0.99.14的發佈說明能夠解釋這一變化,但那也是一個死衚衕。

"與上一個正式版本(P13)相比,變化太多,無法提及(甚至無法記住)......" - Linus

他提到了重大變化,但沒有提到平均負載變化。

根據日期,我查找了內核郵件列表檔案,以找到實際的補丁,但現有的最古老的郵件是1995年6月的,當時系統管理員寫道。

"當我在研究一個系統,使這些郵件檔案更有效地擴展時,我不小心破壞了當前的檔案集(啊嗚)。"

我的搜索開始感到被詛咒了。值得慶幸的是,我找到了一些較早的Linux-devel郵件列表檔案,這些檔案是從服務器備份中搶救出來的,通常以文摘的tarballs形式存儲。我搜索了6000多份摘要,包含98000多封郵件,其中30000封是1993年的。但不知何故,所有這些郵件中都沒有它。看起來,原始的補丁描述可能永遠失去了,而 "爲什麼 "將是一個謎。

uninterruptible 的由來
幸運的是,我終於在 oldlinux.org 上 1993 年的壓縮郵箱文件中找到了變化。這裏是:

From: Matthias Urlichs <[email protected]>
Subject: Load average broken ?
Date: Fri, 29 Oct 1993 11:37:23 +0200


The kernel only counts "runnable" processes when computing the load average.
I don't like that; the problem is that processes which are swapping or
waiting on "fast", i.e. noninterruptible, I/O, also consume resources.

It seems somewhat nonintuitive that the load average goes down when you
replace your fast swap disk with a slow swap disk...

Anyway, the following patch seems to make the load average much more
consistent WRT the subjective speed of the system. And, most important, the
load is still zero when nobody is doing anything. ;-)

--- kernel/sched.c.orig Fri Oct 29 10:31:11 1993
+++ kernel/sched.c Fri Oct 29 10:32:51 1993
@@ -414,7 +414,9 @@
unsigned long nr = 0;

for(p = &LAST_TASK; p > &FIRST_TASK; --p)
- if (*p && (*p)->state == TASK_RUNNING)
+ if (*p && ((*p)->state == TASK_RUNNING) ||
+ (*p)->state == TASK_UNINTERRUPTIBLE) ||
+ (*p)->state == TASK_SWAPPING))
nr += FIXED_1;
return nr;
}
--
Matthias Urlichs \ XLink-POP N|rnberg | EMail: [email protected]
Schleiermacherstra_e 12 \ Unix+Linux+Mac | Phone: ...please use email.
90491 N|rnberg (Germany) \ Consulting+Networking+Programming+etc'ing 42
內核在計算平均負載時只計算“可運行”進程。我不喜歡那樣;問題是正在“快速”交換或等待的進程,即不可中斷的 I/O,也會消耗資源。 當您用慢速交換磁盤替換快速交換磁盤時,平均負載下降似乎有點不直觀...... 無論如何,下面的補丁似乎使負載平均值更加一致 WRT 系統的主觀速度。而且,最重要的是,當沒有人做任何事情時,負載仍然爲零。 ;-)

閱讀近24年前的這一變化背後的想法,真是令人驚訝。

這證實了負載平均數被故意改變以反映對其他系統資源的需求,而不僅僅是CPU。Linux從 "CPU負載平均數 "變爲人們所說的 "系統負載平均數"。

他使用較慢的交換盤的例子是有道理的:通過降低系統的性能,對系統的需求(以運行+排隊的方式衡量)應該增加。然而,負載平均數下降了,因爲他們只跟蹤了CPU的運行狀態,而不是交換狀態。馬蒂亞斯認爲這是不直觀的,確實如此,所以他修復了它。

今天的 uninterruptible
但是,Linux 平均負載是否有時會過高,超過磁盤 I/O 可以解釋的程度?是的,雖然我的猜測是這是由於使用 TASK_UNINTERRUPTIBLE 的新代碼路徑在 1993 年不存在。在 Linux 0.99.14 中,有 13 個代碼路徑直接設置 TASK_UNINTERRUPTIBLE 或 TASK_SWAPPING(交換狀態後來從 Linux 中刪除)。如今,在 Linux 4.12 中,有近 400 個設置 TASK_UNINTERRUPTIBLE 的代碼路徑,包括一些鎖原語。這些代碼路徑之一可能不應該包含在負載平均值中。下次我的平均負載似乎太高時,我會看看是否是這種情況,是否可以修復。

我(第一次)給 Matthias 發了電子郵件,詢問他對將近 24 年後的平均負載變化有何看法。他在一小時內回覆(正如我在推特上提到的),並寫道:

““平均負載”的目的是從人類的角度得出一個與系統有多忙有關的數字。TASK_UNINTERRUPTIBLE 表示(意思是?)進程正在等待諸如磁盤讀取之類的東西,這會增加系統負載。一個嚴重的磁盤綁定系統可能非常緩慢,但平均只有 0.1 的 TASK_RUNNING,這對任何人都沒有幫助。”

(如此迅速地得到迴應,甚至得到迴應,真的讓我很開心。謝謝!)

所以 Matthias 仍然認爲這是有道理的,至少考慮到 TASK_UNINTERRUPTIBLE 過去的含義。

但是今天 TASK_UNITERRUPTIBLE 匹配更多的東西。我們是否應該將平均負載更改爲 CPU 和磁盤需求?調度程序維護者 Peter Zijstra 已經給我發送了一個聰明的選項來探索這樣做:在平均負載中包含 task_struct->in_iowait 而不是 TASK_UNINTERRUPTIBLE,以便它更接近地匹配磁盤 I/O。然而,這引出了另一個問題,我們真正想要的是什麼?我們是想根據線程來衡量對系統的需求,還是僅僅對物理資源的需求?如果是前者,則應包括等待 uninterruptible 鎖,因爲系統需要這些線程。他們沒有閒着。所以也許 Linux 平均負載已經按照我們想要的方式工作了。

爲了更好地理解 uninterruptible 的代碼路徑,我想要一種在行動中測量它們的方法。然後我們可以檢查不同的例子,量化他們花費的時間,看看這一切是否有意義。

測量 uninterruptible tasks
下面是一個來自生產服務器的 Off-CPU flame graph 火焰圖,時間跨度爲60秒,只顯示了內核堆棧,其中我過濾後只包括那些處於TASK_UNINTERRUPTIBLE 狀態的堆棧(SVG)。它提供了許多 uninterruptible 的代碼路徑的示例:

 

 

 

如果您不熟悉 off-CPU 火焰圖:您可以單擊幀放大,檢查顯示爲幀塔的完整堆棧。 x 軸大小與阻塞在 off-CPU 的時間成正比,排序順序(從左到右)沒有實際意義。off-CPU 堆棧的顏色爲藍色(我對 CPU 堆棧使用暖色),飽和度具有隨機變化來區分幀。

我使用來自 bcc 的 offcputime 工具(該工具需要 Linux 4.8+ 的 eBPF 功能)和我的火焰圖軟件生成了這個:

# ./bcc/tools/offcputime.py -K --state 2 -f 60 > out.stacks
# awk '{ print $1, $2 / 1000 }' out.stacks | ./FlameGraph/flamegraph.pl --color=io --countname=ms > out.offcpu.svgb>
我正在使用 awk 將輸出從微秒更改爲毫秒。 offcputime "--state 2" 匹配 TASK_UNINTERRUPTIBLE(參見 sched.h),這是我剛剛爲這篇文章添加的一個選項。 Facebook 的 Josef Bacik 首先用他的 kernelscope 工具做到了這一點,該工具也使用 bcc 和火焰圖。在我的示例中,我只是顯示內核堆棧,但 offcputime.py 也支持顯示用戶堆棧。

至於上面的火焰圖:它顯示,60秒中只有926毫秒是在 uninterruptible 的睡眠中度過的。這只是給我們的負載平均數增加了0.015。這是一些cgroup路徑中的時間,但是這個服務器沒有做太多的磁盤I/O。

這是一個更有趣的,這次只跨越 10 秒(SVG):

 

 

 

​右邊的寬塔顯示systemd-journal在 proc_pid_cmdline_read() 中(讀取 /proc/PID/cmdline)被阻塞,對平均負載有 0.07 的貢獻。而左邊有一個更寬的頁面故障塔,最後也出現在 rwsem_down_read_failed() 中(給平均負載增加0.23)。我使用火焰圖的搜索功能將這些函數用洋紅色標出。下面是rwsem_down_read_failed()的摘錄。

/* wait to be given the lock */
while (true) {
set_task_state(tsk, TASK_UNINTERRUPTIBLE);
if (!waiter.task)
break;
schedule();
}
這是使用 TASK_UNINTERRUPTIBLE 的鎖獲取代碼。 Linux 具有互斥量獲取函數的 uninterruptible 和 interruptible(例如 mutex_lock() 與 mutex_lock_interruptible(),以及用於信號量的 down() 和 down_interruptible())。interruptible 版本允許任務被信號中斷,然後在獲得鎖之前喚醒處理它。uninterruptible 鎖休眠的時間通常不會對平均負載增加太多,但在這種情況下,它們會增加 0.30。如果這要高得多,那麼值得分析一下是否可以減少鎖爭用(例如,我將開始研究 systemd-journal 和 proc_pid_cmdline_read()!),這應該會提高性能並降低平均負載。

將這些代碼路徑包含在平均負載中是否有意義?是的,我會這麼說。這些線程正在工作,並且碰巧被鎖阻塞。他們沒有閒着。它們是對系統的需求,儘管需要軟件資源而不是硬件資源。

分解 Linux 平均負載
Linux負載平均值可以完全分解成組件嗎?這是一個例子:在一個空閒的 8 CPU 系統上,我啓動了 tar 來歸檔一些未緩存的文件。它在磁盤讀取上花費了幾分鐘大部分時間。以下是從三個不同的終端窗口收集的統計數據:

terma$ pidstat -p `pgrep -x tar` 60
Linux 4.9.0-rc5-virtual (bgregg-xenial-bpf-i-0b7296777a2585be1) 08/01/2017 _x86_64_ (8 CPU)

10:15:51 PM UID PID %usr %system %guest %CPU CPU Command
10:16:51 PM 0 18468 2.85 29.77 0.00 32.62 3 tar

termb$ iostat -x 60
[...]
avg-cpu: %user %nice %system %iowait %steal %idle
0.54 0.00 4.03 8.24 0.09 87.10

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdap1 0.00 0.05 30.83 0.18 638.33 0.93 41.22 0.06 1.84 1.83 3.64 0.39 1.21
xvdb 958.18 1333.83 2045.30 499.38 60965.27 63721.67 98.00 3.97 1.56 0.31 6.67 0.24 60.47
xvdc 957.63 1333.78 2054.55 499.38 61018.87 63722.13 97.69 4.21 1.65 0.33 7.08 0.24 61.65
md0 0.00 0.00 4383.73 1991.63 121984.13 127443.80 78.25 0.00 0.00 0.00 0.00 0.00 0.00

termc$ uptime
22:15:50 up 154 days, 23:20, 5 users, load average: 1.25, 1.19, 1.05
[...]
termc$ uptime
22:17:14 up 154 days, 23:21, 5 users, load average: 1.19, 1.17, 1.06
我還爲不間斷狀態 (SVG)) 收集了一個 Off-CPU 火焰圖:

 

 

 

最後一分鐘的平均負載爲 1.19。讓我分解一下:

0.33 來自 tar 的 CPU 時間 (pidstat)

0.67是來自於tar的不間斷磁盤讀取,推斷出來的(offcpu火焰圖上的數值是0.69,我懷疑是由於它開始收集的時間稍晚,而且跨越的時間範圍略有不同)。

0.04 來自其他 CPU 消費者(iostat 用戶 + 系統,從 pidstat 減去 tar 的 CPU)

0.11 來自 kernel workers 不間斷磁盤 I/O 時間,刷新磁盤寫入(offcpu 火焰圖,左側的兩個塔)

加起來是 1.15。我仍然缺少 0.04,其中一些可能是舍入和測量間隔偏移誤差,但很多可能是由於負載平均值是指數加權移動平均,而我正在使用的其他平均值(pidstat,iostat)是正常平均值。在 1.19 之前,一分鐘平均值爲 1.25,因此其中一些仍將拖累我們走高。多少呢?從我之前的圖表來看,在一分鐘標記處,62% 的指標是從那一分鐘開始的,其餘的則更舊。所以 0.62 x 1.15 + 0.38 x 1.25 = 1.18。這與報告的 1.19 非常接近。

這是一個系統,其中一個線程(tar)加上更多線程(內核工作線程中的一些時間)正在工作,Linux 報告負載平均值爲 1.19,這是有道理的。如果它正在測量“CPU 負載平均值”,系統將報告 0.37(從 mpstat 的摘要中推斷),這僅適用於 CPU 資源,但隱藏了需要超過一個線程的工作價值的事實。

我希望這個例子表明這些數字確實意味着一些經過深思熟慮的事情(CPU + uninterruptible),你可以分解它們並弄清楚。

理解 Linux 平均負載
我是在平均負載意味着 CPU 平均負載的操作系統中長大的,所以 Linux 版本一直困擾着我。也許真正的問題一直是“平均負載”這個詞和“I/O”一樣模棱兩可。哪種類型的 I/O?磁盤 I/O?文件系統 I/O?網絡輸入/輸出? ...同樣,哪個負載平均值? CPU平均負載?系統負載平均值?以這種方式澄清它讓我這樣理解它:

在 Linux 上,負載平均值是(或試圖成爲)“系統負載平均值”,對於整個系統來說,測量正在工作和等待工作(CPU、磁盤、uninterruptible 鎖)的線程數。換句話說,它測量不完全空閒的線程數。優勢:包括對不同資源的需求

在其他操作系統上,負載平均值是“CPU 負載平均值”,測量 CPU 正在運行的數量 + CPU 可運行線程數。優點:可以更容易理解和推理(僅適用於 CPU)。

請注意,還有另一種可能的類型:“物理資源負載平均值”,它僅包括物理資源的負載(CPU + 磁盤)。

也許有一天我們會爲 Linux 添加額外的負載平均值,讓用戶選擇他們想要使用的內容:單獨的“CPU 負載平均值”、“磁盤負載平均值”、“網絡負載平均值”等。或者只是使用不同的指標。

什麼是“好”或“壞”的平均負載?
有些人發現似乎適用於他們的系統和工作負載的值:他們知道當負載超過 X 時,應用程序延遲很高並且客戶開始抱怨。但這並沒有真正的規則。

 

 

 

​對於 CPU 負載平均值,可以將該值除以 CPU 數量,然後說如果該比率超過 1.0,則您正在以飽和狀態運行,這可能會導致性能問題。這有點模棱兩可,因爲它是一個可以隱藏變化的長期平均值(至少一分鐘)。一個比率爲 1.5 的系統可能運行良好,而另一個比率爲 1.5 的系統在一分鐘內突然爆發可能表現不佳。

我曾經管理過一個雙 CPU 的電子郵件服務器,它在白天運行的 CPU 平均負載在 11 到 16 之間(比率在 5.5 到 8 之間)。延遲是可以接受的,沒有人抱怨。這是一個極端的例子:大多數系統的負載/CPU 比率僅爲 2。

至於 Linux 的系統負載平均值:這些更加模糊,因爲它們涵蓋了不同的資源類型,所以你不能只除以 CPU 數量。它對於相對比較更有用:如果您知道系統在負載爲 20 時運行良好,而現在是 40,那麼是時候深入研究其他指標以查看發生了什麼。

更好的指標
當 Linux 平均負載增加時,您知道您對資源(CPU、磁盤和一些鎖)有更高的需求,但您不確定是哪一個。您可以使用其他指標進行說明。例如,對於 CPU:

每個 CPU 的利用率:例如,使用 mpstat -P ALL 1 。

每個進程的 CPU 利用率:例如,top、pidstat 1 等。

每線程運行隊列(調度程序)延遲:例如,在 /proc/PID/schedstats、delaystats、perf sched 。

CPU 運行隊列延遲:例如,在 /proc/schedstat、perf sched、我的 runqlat bcc 工具。

CPU 運行隊列長度:例如,使用 vmstat 1 和 'r' 列,或者我的 runqlen bcc 工具。

前兩個是利用率指標,後三個是飽和度指標。利用率指標可用於工作負載表徵,而飽和指標可用於識別性能問題。最佳 CPU 飽和度指標是運行隊列(或調度程序)延遲的度量:任務/線程處於可運行狀態但必須等待輪到它的時間。這些允許您計算性能問題的嚴重程度,例如,線程花費在調度程序延遲上的時間百分比。相反,測量運行隊列長度可能表明存在問題,但更難以估計量級。

在Linux 4.6中,schedstats設施被列爲內核可調控項(sysctl kernel.sched_schedstats),並被改爲默認關閉。延遲覈算暴露了相同的調度器延遲度量,它在cpustat中,我只是建議把它也添加到 htop 中,因爲這將使人們更容易使用它。比起從(無文件記錄的)/proc/sched_debug輸出中刮取等待時間(調度器延遲)指標要簡單。

$ awk 'NF > 7 { if ($1 == "task") { if (h == 0) { print; h=1 } } else { print } }' /proc/sched_debug
task PID tree-key switches prio wait-time sum-exec sum-sleep
systemd 1 5028.684564 306666 120 43.133899 48840.448980 2106893.162610 0 0 /init.scope
ksoftirqd/0 3 99071232057.573051 1109494 120 5.682347 21846.967164 2096704.183312 0 0 /
kworker/0:0H 5 99062732253.878471 9 100 0.014976 0.037737 0.000000 0 0 /
migration/0 9 0.000000 1995690 0 0.000000 25020.580993 0.000000 0 0 /
lru-add-drain 10 28.548203 2 100 0.000000 0.002620 0.000000 0 0 /
watchdog/0 11 0.000000 3368570 0 0.000000 23989.957382 0.000000 0 0 /
cpuhp/0 12 1216.569504 6 120 0.000000 0.010958 0.000000 0 0 /
xenbus 58 72026342.961752 343 120 0.000000 1.471102 0.000000 0 0 /
khungtaskd 59 99071124375.968195 111514 120 0.048912 5708.875023 2054143.190593 0 0 /
[...]
dockerd 16014 247832.821522 2020884 120 95.016057 131987.990617 2298828.078531 0 0 /system.slice/docker.service
dockerd 16015 106611.777737 2961407 120 0.000000 160704.014444 0.000000 0 0 /system.slice/docker.service
dockerd 16024 101.600644 16 120 0.000000 0.915798 0.000000 0 0 /system.slice/
除了 CPU 指標,您還可以查找磁盤設備的利用率和飽和度指標。我專注於USE method 中的這些指標,並有一個 Linux checklist。

雖然有更明確的指標,但這並不意味着平均負載毫無用處。它們與其他指標一起成功地用於雲計算微服務的擴展策略。這有助於微服務響應不同類型的負載增加、CPU 或磁盤 I/O。對於這些策略,在擴大規模上犯錯(花錢)比不擴大規模(花錢的客戶)更安全,所以包括更多的信號是可取的。如果我們擴大規模太多,我們第二天就會調試出原因。

我一直在使用負載平均數的一件事是它們的歷史信息。如果我被要求在雲端檢查一個表現不佳的實例,然後登錄後發現一分鐘的平均數比十五分鐘的平均數低得多,這就是一個很大的線索,我可能來不及看到現場的性能問題。但我只花了幾秒鐘考慮負載平均數,然後就轉向其他指標。

結論
1993 年,一位 Linux 工程師發現了一個不直觀的負載平均值案例,並通過三行補丁將它們從“CPU 負載平均值”永久更改爲人們所謂的“系統負載平均值”。他的更改包括處於 uninterruptible 狀態的任務,因此負載平均值反映了對磁盤資源的需求,而不僅僅是 CPU。這些系統負載平均值計算工作和等待工作的線程數,並總結爲指數加權移動平均的三元組,在方程中使用 1、5 和 15 分鐘作爲常數。這三組數字可以讓您查看負載是增加還是減少,他們最大的價值可能是與自己的相對比較。

此後,不間斷狀態的使用在 Linux 內核中得到了發展,現在包括 uninterruptible 的鎖原語。如果平均負載是對運行和等待線程(而不是嚴格意義上的需要硬件資源的線程)的需求量度,那麼它們仍然按照我們希望的方式工作。

在這篇文章中,我挖掘了 1993 年的 Linux 平均負載補丁(出奇地難找到),其中包含作者的原始解釋。我還在現代 Linux 系統上使用 bcc/eBPF 測量了不間斷狀態下的堆棧跟蹤和時間,並將這次可視化爲 off-CPU 火焰圖。這種可視化提供了許多不間斷睡眠的示例,並且可以在需要解釋異常高負載平均值時生成。我還提出了可以用來更詳細地瞭解系統負載的其他指標,而不是平均負載。

最後,我將引用調度程序維護者 Peter Zijlstra 在 Linux 源代碼中 kernel/sched/loadavg.c 頂部的評論:

該文件包含計算全局 loadavg 數字所需的魔法位。 這是一個愚蠢的數字,但人們認爲它很重要。 我們煞費苦心地讓它在大型機器和無滴答內核上工作。

剖析
指數加權移動平均
什麼是指數加權移動平均?
指數加權移動平均,又稱做指數加權平均。

 

:替代 的估計值,也就是第 t 次採樣的指數加權平均值

:第 t 次採樣的值

: 的權重,是可調參數。

指數加權平均的作用是什麼?
指數加權平均作爲原數據的估計值,主要作用是:

撫平短期波動,起到了平滑的作用

還能夠將長線趨勢或週期趨勢顯現出來

計算平均值時不需要保存以往的數據

列如:我們有這樣一組氣溫數據,圖中橫軸爲一年中的第幾天,縱軸爲氣溫:

 

 

 

​直接看上面的數據圖會發現噪音很多,這時,我們可以用指數加權平均來提取這組數據的趨勢。

按照前面的公式計算,設 β = 0.9:

 

 

 

​計算後得到的值用紅色線表示:

 

 

 

​可以看出,紅色線比藍色的原始數據更加光滑,少了很多噪聲,並且可以表徵原始數據的趨勢。

根據式子:

 

 

 

​展開得到:

 

 

 

​這裏可以看出:

是對每天溫度的加權平均,之所以稱之爲指數加權,是因爲加權係數是隨着時間以指數形式遞減的,時間越靠近,權重越大。

 

 

 

​再來看下面三種情況:

當 β = 0.9 時,指數加權平均最後的結果如圖紅色線所示,代表的是最近 10 天的平均溫度值;

當 β = 0.98 時,指結果如圖綠色線所示,代表的是最近 50 天的平均溫度值;

當 β = 0.5 時,結果如下圖黃色線所示,代表的是最近 2 天的平均溫度值;

 

 

 

 

β 越小,噪音越多,雖然能夠很快的適應溫度的變化,但是更容易出現奇異值。

β 越大,得到的曲線越平坦,因爲多平均了幾天的溫度,這個曲線的波動更小。但有個缺點是,因爲只有 0.02 的權重給了當天的值,而之前的數值權重佔了 0.98 ,曲線進一步右移,在溫度變化時就會適應地更緩慢一些,會出現一定延遲。

通過上面的內容可知,β 也是一個很重要的超參數,不同的值有不同的效果,需要調節來達到最佳效果,一般 0.9 的效果就很好。

參考文章:

爲什麼在優化算法中使用指數加權平均

優化算法之指數加權平均詳解

指數加權移動平均法(EWMA)

https://www.deeplearning.ai/

Linux 計算平均負載的代碼怎麼實現?

 

 


如上文所述,linux kernel 會分別計算最近1分鐘,5分鐘,15分鐘的平均負載。

因爲 linux kernel 每 5s 採樣一次數據,所以最近 1 分鐘的平均負載是最近 12 次的採樣平均值,最近 5 分鐘的平均負載是最近 60 次的採樣平均值,最近 15 分鐘的平均負載是最近 180 次的採樣平均值。

1. 根據公式:

 

 

 

和示例:

 

 

 

還需要確定 的值,才能計算平均負載。

2. 根據公式 :

 

 

 

 

 

 

可得:

 

 

 

平均負載 N
最近 1 分鐘 12 0.920044415
最近 5 分鐘 60 0.983471454
最近 15分鐘 180 0.994459848
可得最近1分鐘的平均負載公式:

 

最近5分鐘和15分鐘的平均負載公式以此類推。

由於 linux kernel 不支持浮點數計算,因此需要把浮點數計算轉換成定點數計算。linux 把這些浮點數乘以 來保留精度,此處使用二進制的好處是隻需要移位即可。得到:

 


這就是下面 linux 代碼中宏 EXP_1,EXP_5,EXP_15 的由來,LOAD_FREQ 代表每 5s 採樣一次數據。

// include/linux/sched/loadavg.h
/*
* These are the constant used to fake the fixed-point load-average
* counting. Some notes:
* - 11 bit fractions expand to 22 bits by the multiplies: this gives
* a load-average precision of 10 bits integer + 11 bits fractional
* - if you want to count load-averages more often, you need more
* precision, or rounding will get you. With 2-second counting freq,
* the EXP_n values would be 1981, 2034 and 2043 if still using only
* 11 bit fractions.
*/
extern unsigned long avenrun[]; /* Load averages */
extern void get_avenrun(unsigned long *loads, unsigned long offset, int shift);

#define FSHIFT 11 /* nr of bits of precision */
#define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */
#define LOAD_FREQ (5*HZ+1) /* 5 sec intervals */
#define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */
#define EXP_5 2014 /* 1/exp(5sec/5min) */
#define EXP_15 2037 /* 1/exp(5sec/15min) */

#define LOAD_INT(x) ((x) >> FSHIFT)
#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100)

Linux 內核計算平均負載的代碼實現主要有 3 部分組成:

1. 每個 CPU 每 5s 報告一次當前處於 runnable 和 uninterruptible 狀態的進程數量,並記錄到變量 calc_load_tasks。

相關代碼:

void calc_global_load_tick(struct rq *this_rq)
2. 專門有一個由 tick_do_timer_cpu 指定的 CPU 計算平均負載,計算平均負載的時間戳至少是在 CPU 報告進程數量後的 10 個 ticks,並將結果記錄到變量 avenrun。

相關代碼:

void calc_global_load(void)

 

 


3. 從第 1,2 點可以看出 linux kernel 通過 tick 採樣和計算平均負載。考慮到支持 NO_HZ 的系統,在CPU 進入 idle 後, tick 將被關閉,這樣會導致一部分採樣數據丟失,如下圖所示。因此當 CPU 退出 idle 後,需要把這部分未採樣和計算的數據找補回來。

相關代碼:

void calc_load_nohz_start(void)
void calc_load_nohz_stop(void)
void calc_global_nohz(void)

 

 


calc_global_load_tick:

每個 CPU 每 5s 報告一次當前處於 runnable 和 uninterruptible 狀態的進程數量,並記錄到變量 calc_load_tasks。

void calc_global_load_tick(struct rq *this_rq)
{
long delta;

if (time_before(jiffies, this_rq->calc_load_update))
return;

delta = calc_load_fold_active(this_rq, 0);
if (delta)
atomic_long_add(delta, &calc_load_tasks);

this_rq->calc_load_update += LOAD_FREQ;
}
LOAD_FREQ :即 (5*HZ+1),定義每 5s 採樣一次數據。
this_rq->calc_load_update :記錄當前 CPU 下次報告進程數量的最早時間戳。
this_rq->calc_load_active:記錄當前 CPU 中, 狀態爲 runnable 和 uninterruptible 的進程數量。
calc_load_tasks :記錄當前系統中狀態爲 runnable 和 uninterruptible 的進程數量。
calc_global_load:

專門有一個由 tick_do_timer_cpu 指定的 CPU 計算平均負載,計算平均負載的時間至少是在 CPU 報告進程數量後的 10 個 ticks ,並將結果記錄到變量 avenrun。

// kernel/sched/core.c
void __init sched_init(void) {
...
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
rq->calc_load_active = 0;
rq->calc_load_update = jiffies + LOAD_FREQ;
}
calc_load_update = jiffies + LOAD_FREQ;
...
}
calc_load_update :和 this_rq->calc_load_update 不同,這個變量記錄系統下次計算平均負載的時間戳。
// kernel/sched/loadavg.c
/*
gic_handle irq
->__handle_domain_irq
-->handle_percpu_devid_1rq
--->arch_timer_handler_phys
---->tick_handle_periodic
*/

void tick_handle_periodic(struct clock_event_device *dev) {
tick_periodic(cpu) {
if (tick_do_timer_cpu == cpu) {
do_timer(1/*ticks*/) {
iffies_64 += ticks;
calc_global_load();
}
}
update_process_times(user_mode(get_irq_regs())/*int user_tick*/) {
}
}

void calc_global_load() {
// alc_load - update the avenrun load estimates 10 ticks after the CPUs have updated calc_load_tasks.
// 在 CPU 更新 calc_load_tasks 後 10 個 ticks 更新 avenrun 負載估計
sample_window = READ_ONCE(calc_load_update);
if (time_before(jiffies, sample_window + 10))
return;

delta = calc_load_nohz_read()
if (delta)
atomic_long_add(delta, &calc_load_tasks);

active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0

avenrun[0] = calc_load(avenrun[0]/*load*/, EXP_1/*exp*/, active) {
// include/linux/sched/loadavg.h
// a1 = a0 * e + a * (1 - e)
unsigned long newload;

newload = load * exp + active * (FIXED_1 - exp);
//如果新增active比之前的完整還多,則newload + 1,體現趨勢
if (active >= load)
newload += FIXED_1-1;

return newload / FIXED_1;
}
avenrun[1] = calc_load(avenrun[1], EXP_5, active);
avenrun[2] = calc_load(avenrun[2], EXP_15, active);

WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);

// In case we went to NO_HZ for multiple LOAD_FREQ intervals catch up in bulk.
calc_global_nohz();
}

函數 calc_load(unsigned long load, unsigned long exp, unsigned long active) 是計算平均負載的核心,它的實現即上文所介紹的計算公式。

calc_load_nohz_start,calc_load_nohz_stop 以及 calc_global_nohz:

從第 1,2 點可以看出 linux kernel 通過 tick 採樣和計算平均負載。考慮到支持 NO_HZ 的系統,在CPU 進入 idle 後,tick 將被關閉,這樣會導致一部分採樣數據丟失,如下圖所示。因此當 CPU 退出 idle 後,需要把這部分未採樣和計算的數據找補回。

CPU 進入 NO_HZ 時:

通過函數 calc_load_fold_active 將當前處於 runnable 和 uninterruptible 狀態的進程數量記錄到 calc_load_nohz[idx]。

void calc_load_nohz_start(void)
{
/*
* We're going into NO_HZ mode, if there's any pending delta, fold it
* into the pending NO_HZ delta.
*/
calc_load_nohz_fold(this_rq());
}

static void calc_load_nohz_fold(struct rq *rq)
{
long delta;

delta = calc_load_fold_active(rq, 0);
if (delta) {
int idx = calc_load_write_idx() {
int idx = calc_load_idx;

/*
* See calc_global_nohz(), if we observe the new index, we also
* need to observe the new update time.
*/
smp_rmb();

/*
* If the folding window started, make sure we start writing in the
* next NO_HZ-delta.
*/
if (!time_before(jiffies, READ_ONCE(calc_load_update)))
idx++;

return idx & 1;
}

atomic_long_add(delta, &calc_load_nohz[idx]);
}
}
CPU 退出 NO_HZ 時:

如果當前時間戳 jiffies 還未到達 CPU 報告時間戳 this_rq->calc_load_update 時,不做任何處理。
如果已經到達或者過了 CPU 報告時間戳 this_rq->calc_load_update 時, 那需要更新當前 CPU 下次報告時間戳 this_rq->calc_load_update,這是因爲原本負責更新這個值的函數 calc_global_load_tick 已經錯過了。
void calc_load_nohz_stop(void)
{
struct rq *this_rq = this_rq();

/*
* If we're still before the pending sample window, we're done.
*/
this_rq->calc_load_update = READ_ONCE(calc_load_update);
if (time_before(jiffies, this_rq->calc_load_update))
return;

/*
* We woke inside or after the sample window, this means we're already
* accounted through the nohz accounting, so skip the entire deal and
* sync up for the next window.
*/
if (time_before(jiffies, this_rq->calc_load_update + 10))
this_rq->calc_load_update += LOAD_FREQ;
}
接下來到計算平均負載的時間戳 calc_load_update 時,call void calc_global_load(void) :

void calc_global_load(void)
{
unsigned long sample_window;
long active, delta;

sample_window = READ_ONCE(calc_load_update);
if (time_before(jiffies, sample_window + 10))
return;

/*
* Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.
*/
delta = calc_load_nohz_read();
if (delta)
atomic_long_add(delta, &calc_load_tasks);

active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;

avenrun[0] = calc_load(avenrun[0], EXP_1, active);
avenrun[1] = calc_load(avenrun[1], EXP_5, active);
avenrun[2] = calc_load(avenrun[2], EXP_15, active);

WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);

/*
* In case we went to NO_HZ for multiple LOAD_FREQ intervals
* catch up in bulk.
*/
calc_global_nohz();
}

static long calc_load_nohz_read(void)
{
int idx = calc_load_read_idx();
long delta = 0;

if (atomic_long_read(&calc_load_nohz[idx]))
delta = atomic_long_xchg(&calc_load_nohz[idx], 0);

return delta;
}
當前時間戳如果未到達計算平均負載時間戳 (calc_load_update + 10)時則跳過。
如果 CPU 因爲 idle 而錯過報告狀態爲 runnable 和 uninterruptible 的進程數量時,即 this_rq->calc_load_update 時間戳出現在 CPU idle 期間,導致 CPU 無法通過 calc_global_load_tick 報告進程的數量。那可以通過函數 calc_load_nohz_read 獲取當時 runnable 和 uninterruptible 的進程數量(CPU 進入 idle 後,這兩種狀態的進程數量將保持不變。如果 CPU 未 idle,則 calc_load_nohz_read 返回 0。
接着 call 函數 calc_global_nohz:

如果當前時間戳已經超過計算平均負載時間戳 (calc_load_update + 10)時,則說在 CPU idle 期間,至少有一次或者多次需要計算平均負載的時間戳,因此這裏需要把這些錯過的採樣點補回來,代碼如下所示:

static void calc_global_nohz(void)
{
unsigned long sample_window;
long delta, active, n;

sample_window = READ_ONCE(calc_load_update);
if (!time_before(jiffies, sample_window + 10)) {
/*
* Catch-up, fold however many we are behind still
*/
delta = jiffies - sample_window - 10;
n = 1 + (delta / LOAD_FREQ);

active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;

avenrun[0] = calc_load_n(avenrun[0], EXP_1, active, n);
avenrun[1] = calc_load_n(avenrun[1], EXP_5, active, n);
avenrun[2] = calc_load_n(avenrun[2], EXP_15, active, n);

WRITE_ONCE(calc_load_update, sample_window + n * LOAD_FREQ);
}

/*
* Flip the NO_HZ index...
*
* Make sure we first write the new time then flip the index, so that
* calc_load_write_idx() will see the new time when it reads the new
* index, this avoids a double flip messing things up.
*/
smp_wmb();
calc_load_idx++;
}

這種情況出現時 CPU Idle 期間,這個期間每個採樣點的 runnable 和 uninterruptible 的進程數量沒有變化,所以 calc_load_n 簡化計算方式。

/*
* a1 = a0 * e + a * (1 - e)
*
* a2 = a1 * e + a * (1 - e)
* = (a0 * e + a * (1 - e)) * e + a * (1 - e)
* = a0 * e^2 + a * (1 - e) * (1 + e)
*
* a3 = a2 * e + a * (1 - e)
* = (a0 * e^2 + a * (1 - e) * (1 + e)) * e + a * (1 - e)
* = a0 * e^3 + a * (1 - e) * (1 + e + e^2)
*
* ...
*
* an = a0 * e^n + a * (1 - e) * (1 + e + ... + e^n-1) [1]
* = a0 * e^n + a * (1 - e) * (1 - e^n)/(1 - e)
* = a0 * e^n + a * (1 - e^n)
*
* [1] application of the geometric series:
*
* n 1 - x^(n+1)
* S_n := \Sum x^i = -------------
* i=0 1 - x
*/
unsigned long
calc_load_n(unsigned long load, unsigned long exp,
unsigned long active, unsigned int n)
{
return calc_load(load, fixed_power_int(exp, FSHIFT, n), active);
}
分解 Linux 平均負載的背後邏輯是什麼?
在原文裏面介紹多個工具組合分解平均負載分佈情況,這裏我們解釋一下這些數字背後的邏輯。

Linux負載平均值可以完全分解成組件嗎?這是一個例子:在一個空閒的 8 CPU 系統上,我啓動了 tar 來歸檔一些未緩存的文件。它在磁盤讀取上花費了幾分鐘大部分時間。以下是從三個不同的終端窗口收集的統計數據:

terma$ pidstat -p `pgrep -x tar` 60
Linux 4.9.0-rc5-virtual (bgregg-xenial-bpf-i-0b7296777a2585be1) 08/01/2017 _x86_64_ (8 CPU)

10:15:51 PM UID PID %usr %system %guest %CPU CPU Command
10:16:51 PM 0 18468 2.85 29.77 0.00 32.62 3 tar

termb$ iostat -x 60
[...]
avg-cpu: %user %nice %system %iowait %steal %idle
0.54 0.00 4.03 8.24 0.09 87.10

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
xvdap1 0.00 0.05 30.83 0.18 638.33 0.93 41.22 0.06 1.84 1.83 3.64 0.39 1.21
xvdb 958.18 1333.83 2045.30 499.38 60965.27 63721.67 98.00 3.97 1.56 0.31 6.67 0.24 60.47
xvdc 957.63 1333.78 2054.55 499.38 61018.87 63722.13 97.69 4.21 1.65 0.33 7.08 0.24 61.65
md0 0.00 0.00 4383.73 1991.63 121984.13 127443.80 78.25 0.00 0.00 0.00 0.00 0.00 0.00

termc$ uptime
22:15:50 up 154 days, 23:20, 5 users, load average: 1.25, 1.19, 1.05
[...]
termc$ uptime
22:17:14 up 154 days, 23:21, 5 users, load average: 1.19, 1.17, 1.06
我還爲不間斷狀態 (SVG)) 收集了一個 Off-CPU 火焰圖:

 

 

 

最後一分鐘的平均負載爲 1.19。讓我分解一下:

0.33 來自 tar 的 CPU 時間 (pidstat)

0.67是來自於tar的不間斷磁盤讀取,推斷出來的(offcpu火焰圖上的數值是0.69,我懷疑是由於它開始收集的時間稍晚,而且跨越的時間範圍略有不同)。

0.04 來自其他 CPU 消費者(iostat 用戶 + 系統,從 pidstat 減去 tar 的 CPU)

0.11 來自 kernel workers 不間斷磁盤 I/O 時間,刷新磁盤寫入(offcpu 火焰圖,左側的兩個塔)

加起來是 1.15。我仍然缺少 0.04,其中一些可能是舍入和測量間隔偏移誤差,但很多可能是由於負載平均值是指數加權移動平均,而我正在使用的其他平均值(pidstat,iostat)是正常平均值。在 1.19 之前,一分鐘平均值爲 1.25,因此其中一些仍將拖累我們走高。多少呢?從我之前的圖表來看,在一分鐘標記處,62% 的指標是從那一分鐘開始的,其餘的則更舊。所以 0.62 x 1.15 + 0.38 x 1.25 = 1.18。這與報告的 1.19 非常接近。

這是一個系統,其中一個線程(tar)加上更多線程(內核工作線程中的一些時間)正在工作,Linux 報告負載平均值爲 1.19,這是有道理的。如果它正在測量“CPU 負載平均值”,系統將報告 0.37(從 mpstat 的摘要中推斷),這僅適用於 CPU 資源,但隱藏了需要超過一個線程的工作價值的事實。

我希望這個例子表明這些數字確實意味着一些經過深思熟慮的事情(CPU + uninterruptible),你可以分解它們並弄清楚。

Linux 平均負載是一個數量的概念,即狀態爲 runnable 和 uninterruptible 的進程數量的指數加權平均。而文中作者通過 pidstat,iostat 和 perf cpu-off 這些時間概念的工具,把平均負載分解到各個應用程序不同的行爲,看似牛馬不相及關係,這樣做的邏輯是:

1.19 :在最近 60s 內,linux kernel 每 5s 統計一次狀態爲 runnable 和 uninterruptible 的進程數量的指數加權平均值。
0.33 :最近 60s 內 ,tar 在 CPU 3 上運行時間比例 0.3262 的近似值。這個是 runnable 狀態下的 tar 程序貢獻的負載正常平均值(與指數加權平均值區分),雖然是時間上的概念,但可以理解爲每 1ns 採樣一次 runnable 進程的數量,一共採樣 次,即 60 s,最後按照正常平均值計算的平均負載,這就很容易理解,這指標確實可以作爲 linux 平均負載值的一部分。當然這只是 runnable 狀態下的 tar 程序貢獻的負載。
0.67:0.33 是 tar 程序處於 runnable 狀態下的貢獻的平均負載,那麼因爲 io 阻塞進程被切到 uninterruptible 狀態貢獻的負載呢?0.67(1 - 0.33),即 tar 程序在 runnable 和 uninterruptible 兩種狀態之間切換。但是通過 perf cpu-off 火焰圖也可以得到這個數據,由於讀操作阻塞 CPU,導致 CPU idle 的時間佔比 0.69 (41.164/60)。

 

 


0.04:來自其他 CPU runnable 狀態進程貢獻的平均負載,(0.54%+4.03%)*8 - 32.62% = 0.04。
0.11 :來自 kernel workers 不間斷磁盤 I/O 時間,刷新磁盤寫入(offcpu 火焰圖,左側的兩個塔),​​​​​​​(3.684+3.102)/60 = 0.11。即除了 tar 以外,其他所有 uninterruptible 進程貢獻的負載。
最終總平均負載:0.33+0.67+0.04+0.11=1.15,和 linux 平均負載值 1.19 存在一定的差距。上文作者已經有解釋,作者根據公式 ​​​​​​​0.62*1.15 + 0.38*1.25 = 1.18 進一步縮短差距。0.62 來源於下面的解釋(感覺這裏在湊數字,不太理解!!!)。

如果您使用一個空閒系統,然後開始一個單線程 CPU 密集型工作負載(一個循環中的一個線程),那麼 60 秒後一分鐘的平均負載是多少?如果它是一個普通的平均值,它將是 1.0。這是那個實驗,圖表:

 

 


負載平均實驗以可視化指數阻尼
所謂的“一分鐘平均值”,到一分鐘大關才達到0.62左右。有關方程和類似實驗的更多信息,Neil Gunther 博士寫了一篇關於平均負載的文章:How It Works, 此外 loadavg.c 中有許多 Linux 源代碼塊註釋。
————————————————
版權聲明:本文爲CSDN博主「zs.w」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/cs_tech/article/details/126563993

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