5 秒一格的 load,和尖峰那一刻的執行緒 右邊是一排 5 秒一根的柱子,中間 6 根強調色的高柱是一次 30 秒的負載尖峰;一條平滑的 load1 曲線慢慢爬升又慢慢回落,明顯落後於柱子。尖峰上方是一小堆標著 R 和 D 的執行緒方塊,表示那一刻被抓下來的執行緒。左邊寫著 load5s、load2p -c 和 R 16 · D 24。 load5s load2p -c R 16 · D 24 RRDDRDDRDRDR

設想一臺 4 核的機器,晚上 22:31 業務報了一次響應變慢,持續不到一分鐘。幾分鐘後,22:34 我們登入上去,/proc/loadavg 的三個數是:

2.90 2.87 1.41

load 已經在往下走了,top 裡也沒有任何異常程序。可剛才那半分鐘到底發生了什麼?是 CPU 不夠用,還是有一批執行緒卡在了磁碟或鎖上?如果是後者,是哪個程序、卡在哪裡?

這種“事後回看”的需求,Linux 上有一整個家族的工具在做:sysstat 的 sar1、atop2、Meta 的 below3、Red Hat 主推的 PCP4、collectl5、nmon6、淘寶的 tsar7,以及本文的主角,阿里貢獻給龍蜥社群的 SSAR8。它們都在後臺按固定間隔把系統狀態寫進磁碟,出事以後再拿出來翻。區別在於記什麼、多久記一次、記下的東西能回答多少問題。

我們沒有去比較功能列表,而是在同一臺機器上把它們全部裝好,按各自的預設配置跑起來,然後人為製造開頭那樣一次 30 秒的尖峰,看事後每個工具能說出什麼。上面那三個數,和下文所有的輸出,都來自這次實驗。

一、實驗:同一個尖峰,八個記錄工具

這一家族的工具

先把參賽的工具介紹一下。它們都分成“採集”和“查詢”兩半:採集端常駐或由定時任務拉起,把資料寫進本地檔案;查詢端事後讀這些檔案。

參加對比的八個工具(都按預設或最常見的配置執行)
工具出處 · 語言預設取樣間隔程序級記錄落盤格式
SSAR 1.0.5阿里,2021 年開源到龍蜥社群;C 採集,C++ 查詢,Python 擴充套件指令碼整機 60 秒,load 5 秒,程序 60 秒每分鐘全部程序快照;load 超閾值時按執行緒抓現場和核心棧/proc 原檔案,程序資料 gzip
sar(sysstat 12.6.1)Linux 上最老牌的一個,C600 秒(發行版定時任務)不記(pidstat 只能實時看)二進位制 sa 檔案
atop 2.10.0Atoptool,C600 秒區間內每個程序的累計資源,含已退出程序二進位制,增量壓縮
below 0.11.0Meta,2021 年開源;Rust5 秒每次完整快照:程序狀態、cgroup、PSI每個樣本完整儲存,zstd 壓縮
PCP 6.2.0源自 SGI,現由 Red Hat 主導;C,外掛式 PMDA大部分 60 秒,程序記憶體 300 秒預設只有程序記憶體,可另配帶後設資料的歸檔
collectl 4.3.1源自 HP;Perl系統 10 秒(服務方式)、程序 60 秒程序快照gzip 的原始文字
nmon 16p源自 IBM;C常用 300 秒加 -T 時記下當時的 top 程序CSV 文字
tsar淘寶,C,模組化60 秒(cron)不記文字,每分鐘一行

算上 sar 和 atop 各多跑的一份 60 秒配置,一共十路記錄同時執行。collectl 在尖峰實驗裡沒有加 -D,系統指標實際是 1 秒一次;開銷測量時改回了服務方式的 10 秒。

圖 1 · sar 家族的工具:都分採集和查詢兩半,差別在記什麼、多久記一次

netdata、Prometheus 的 node_exporter 這一類也能看歷史,但它們是監控系統:資料要發到服務端或單獨的時序庫裡,粒度和儲存時長取決於整套監控怎麼搭,和“機器本地自帶一個黑匣子”不是一回事,本文不放進來。龍蜥社群還有一個 sysak,它是一組診斷工具的合集,偏向出事時現場排查,也不在這裡比較。

製造一個尖峰

實驗機是一臺 4 核、16 GB 的虛擬機器,Ubuntu 24.04,核心 6.18。為了讓結果更像生產環境,後臺常駐了 400 個空閒程序,整機大約 550 個執行緒。

22:31:45,我們同時拉起兩樣東西,持續 30 秒:

  • stress-ng --cpu 16:16 個程序把 4 個 CPU 打滿,它們是 R 狀態(執行或在執行佇列裡排隊)。
  • 一個叫 stuck 的小程式,fork 出 24 個程序,每個都呼叫 vfork(),子程序睡 30 秒,父程序就一直卡在核心裡等它,處於 D 狀態(不可中斷睡眠)。生產環境裡,等 NFS、等磁碟、等核心裡的讀寫鎖都會讓執行緒進入 D 狀態,這裡用 vfork 只是因為它最容易精確控制。

所以在這 30 秒裡,真實情況是 16 個 R 加 24 個 D,一共 40 個“活躍”執行緒,而機器只有 4 個核。22:32:15 兩者同時結束。

這 30 秒,事後能在各個工具裡找回多少?

二、事後回放:取樣點有沒有落進去

現象:預設配置的 sar 什麼也沒看到

先用最常見的 sar 看 22:20 以來的 load:

$ sar -q -f sa600
22:19:36 runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked
22:29:36 1 556 0.16 0.30 0.34 0
22:39:36 0 556 0.07 0.97 0.99 0

發行版預設每 10 分鐘採一次,兩個點一個在尖峰前兩分鐘,一個在尖峰後七分鐘。只有 ldavg-5 和 ldavg-15 微微抬頭,暗示中間“有過點什麼”。

第一次嘗試:把間隔調到 60 秒

很多人會把 sar 的間隔改成 1 分鐘。我們同時跑了一份 60 秒的:

$ sar -q -f sa60
22:29:36 runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked
22:31:36 0 552 0.03 0.20 0.30 0
22:32:36 0 552 11.37 3.76 1.53 0

22:32:36 的 ldavg-1 跳到了 11.37,看得出出過事。可是 runq-sz 是 0,blocked 也是 0,因為這個取樣點比尖峰結束晚了 21 秒,runq-sz 是取樣那一瞬間的值。一個 30 秒的事件,用 60 秒的間隔去採,大約一半的機會會漏掉。

每個工具的取樣點落在哪裡

把八個工具(sar 和 atop 各有 600 秒、60 秒兩份)的取樣時刻畫在同一條時間軸上,就能看清誰“碰上了”、誰沒碰上:

同一個 30 秒尖峰落在各工具取樣點上的位置 時間軸 22:31:20 到 22:32:50,虛線框是 22:31:45 到 22:32:15 的尖峰。ssar load5s 和 below 每 5 秒一個點,尖峰裡有 6 個點;ssar load2p 在 22:31:49 觸發一次;ssar procs 和 tsar 的 22:32:00、PCP 的 22:31:49 碰巧落在尖峰裡;collectl 程序取樣 22:31:37、22:32:37,atop 和 sar 的 22:31:36、22:32:36 都落在尖峰外,但 atop 記的是整個區間的累計值;預設 600 秒的 sar 和 300 秒的 nmon 在這段時間沒有采樣。右側一列寫著每個工具事後能讀到的內容。 每個工具的取樣點落在哪裡 事後能讀到 尖峰 30 秒 真實情況 16 R + 24 D,持續 30 秒 ssar load5s 5 秒 每 5 秒一個 40 ssar load2p 超閾值觸發 40 個執行緒 + 核心棧 ssar procs 60 秒 程序狀態快照 below 5 秒 16 執行 + 24 不可中斷 PCP pmlogger 60 秒 只有計數 19 / 24 tsar 60 秒 runq 16,load1 8.88 collectl 1 秒 / 程序 60 秒 RunQ 16,BlkT 0 atop 60 / 600 秒 各程序累計 CPU sar 60 秒 load1 11.37,runq 0 sar · nmon 600 / 300 秒 沒有記錄 22:31:30 22:31:45 22:32:00 22:32:15 22:32:30 22:32:45 取樣落在尖峰裡 取樣落在尖峰外
圖 2 · 同一個 30 秒尖峰落在各工具的取樣點上:只有 5 秒一採的 SSAR load5s 和 below 穩穩碰上,其餘要麼靠運氣,要麼只留下區間累計值

能穩穩抓住這 30 秒的只有 5 秒一採的兩個:SSAR 的 load5s 和 below。PCP、tsar 和 SSAR 的程序快照是碰巧有一個點落在了裡面;atop 的點雖然在外面,但它記的是整個區間裡每個程序的累計值,已經退出的程序也會以 <stress-ng-cp> 這樣帶尖括號的名字列出來2,所以照樣能看到 16 個 stress-ng 各用了約 7.5 秒 CPU。

即使碰上了,各家讀到的東西也不一樣。tsar 22:32:00 那一行是 runq 16,PCP 22:31:49 的 proc.runq.blocked 是 24,collectl 每秒都記,RunQ 一直是 16,BlkT 卻一直是 0。同一時刻,為什麼有的說 16,有的說 24,有的說 0?

原因:“blocked”並不是全部 D 狀態

RunQ、runq-sz 來自 /proc/loadavg 第四列或 /proc/stat 的 procs_running,是 R 狀態的執行緒數,16 沒有問題。問題出在 blocked:sar、collectl、tsar 讀的都是 /proc/stat 裡的 procs_blocked,而核心在這裡輸出的是 nr_iowait(),只統計正在等塊裝置 IO 的任務9。我們的 24 個 stuck 卡在 vfork 上,不是在等磁碟,所以永遠是 0。

PCP 的 proc.runq.blocked 不一樣,它是 PCP 自己遍歷 /proc/<pid>/stat 數出來的 D 狀態程序數,所以是 24。但它只有一個數字,預設配置裡程序級的指標只有記憶體,5 分鐘一次。

到這裡,大部分工具能告訴我們的是“22:32 前後 load 很高”。可這個“高”到底有多高?sar 說 11.37,tsar 說 8.88,PCP 在 22:31:49 甚至只讀到 0.02,沒有一個接近真實的 40。load average 為什麼會差這麼多?

三、load average 是怎麼算出來的

核心每 5 秒更新一次指數平均

Linux 核心每隔 5 秒(準確說是 5*HZ+1 個時鐘節拍)數一次全系統的活躍執行緒數 active,也就是 R 狀態加 D 狀態的執行緒數,然後用它去更新三個指數衰減平均10:

loadnew=loadold⋅18842048+active⋅1642048\text{load}_{new} = \text{load}_{old}\cdot\frac{1884}{2048} + \text{active}\cdot\frac{164}{2048}

這裡 1884/2048 ≈ 0.92,約等於 e−5/60e^{-5/60}。每次更新,舊值保留 92%,新數進來 8%。把 D 狀態也算進 load 是 Linux 特有的做法,本意是讓“等磁碟”的壓力也能體現出來11。

於是 load1 就像一輛車的車速,active 是油門。油門一下踩到 40,車速要好幾分鐘才能追上;油門鬆開,車速也是慢慢降下來。我們這次的 30 秒尖峰裡,load1 只爬到 15.77;尖峰結束 20 秒後 sar 讀到 11.37,此時真實的活躍執行緒已經回到 1 個左右:

同一個尖峰裡 load1 和 load5s 的走勢 橫軸 22:31:30 到 22:35:00。虛線框標出 22:31:45 到 22:32:15 的尖峰,真實的 R 加 D 執行緒數是 40。強調色柱子是 ssar 每 5 秒算出的 load5s,尖峰期間 6 根都是 40,之前之後都是 0 或 1。折線是 load1,從 0.02 爬到 22:32:15 的 15.77,然後慢慢回落,22:32:35 還有 11.37,22:35:00 還有 1.21。 ssar load5s 記錄的 5 秒一行(4 核機器) 尖峰 22:31:45–22:32:15 0 10 20 30 40 22:31:30 22:32 22:32:30 22:33 22:33:30 22:34 22:34:30 22:35 load5s = 40(16 R + 24 D) load1 最高 15.77 ← 22:32:35 尖峰已過,load1 還有 11.37 22:35:00 仍有 1.21 load5s:ssar 由相鄰兩次 load1 反推的 R+D 執行緒數 load1:/proc/loadavg 第一列
圖 3 · load1 是指數平均:40 個活躍執行緒持續 30 秒,load1 只爬到 15.77,尖峰過後又拖了幾分鐘;load5s 每 5 秒給出一次真實值

所以 sar 的 11.37 既不是峰值,也不是取樣時的狀態,而是“過去幾分鐘加權後的餘溫”。PCP 那個 0.02 更極端:22:31:49 尖峰已經開始了 4 秒,核心還沒輪到下一次更新。

能不能把那一次 active 找回來

既然核心每 5 秒用一個精確的 active 去更新 load1,公式又是公開的,那隻要拿到相鄰兩次 load1,就能把 active 反解出來:

active=loadnew⋅2048−loadold⋅1884164\text{active} = \frac{\text{load}_{new}\cdot 2048 - \text{load}_{old}\cdot 1884}{164}

難點在於“相鄰兩次”。核心的 5 秒週期從開機時刻算起,和整分鐘、整 5 秒都對不齊,而 /proc/loadavg 也不會通知誰它更新了。sar、tsar 們按自己的節奏讀,讀到的兩個 load1 之間可能隔了好幾次更新,就沒法反解。SSAR 的 load5s,解決的就是這個對齊問題。

四、SSAR 的 load5s:把指數平均倒推回去

每 100 毫秒看一眼 /proc/loadavg

SSAR 的採集程序 sresar 裡有一個專門的執行緒,每 100 毫秒讀一次 /proc/loadavg,只要第一列和上一次不一樣,就說明核心剛剛完成了一次更新,它立刻用上一次和這一次的 load1 反推 active,記成一行12。原始碼裡就是這一行:

load5s = (int)round((2048 * (2048 * current_node->load1 - 10) - 1024
- 1884 * 2048 * current_node->prev->load1) / 164 / 2048);

公式裡多出來的 -10 和 -1024,是在補償核心定點數運算和 /proc/loadavg 只保留兩位小數帶來的取整偏差。拿尖峰開始後的第一次更新來算一遍:

從兩次 load1 反推出 load5s 左卡片:核心在 22:31:49 前後做了一次 calc_load,active 等於 16 個 R 加 24 個 D 共 40,load1 從 0.02 變成 0.02 乘 1884/2048 加 40 乘 164/2048,約 3.23。中卡片:/proc/loadavg 在 22:31:44 讀到 0.02,22:31:49 讀到 3.23;sresar 每 100 毫秒讀一次,只在第一列變化時記一行。右卡片:反推 3.23 乘 2048 減 0.02 乘 1884,除以 164,得 40.1,取整為 40;下一次 6.17 和 3.23 反推得 39.9。 核心 calc_load() 每 5 秒 active = R + D = 16 + 24 = 40 load1 = 0.02 × 1884/2048 + 40 × 164/2048 ≈ 3.23 /proc/loadavg 每 100 ms 讀一次 22:31:44 0.02 0.20 0.30 22:31:49 3.23 0.86 0.51 第一列變了 → 核心剛更新過 沒變就不記,接著等 sresar 反推 load5s (3.23×2048 − 0.02×1884) ÷ 164 = 40.1 → 40 下一次 22:31:54: (6.17×2048 − 3.23×1884) ÷ 164 = 39.9 → 40 1884/2048 ≈ 0.92:每次更新舊值保留 92%,新的 active 只進來 8%(164/2048)。 知道了相鄰兩次的 load1,就能把這 8% 裡的 active 解出來。
圖 4 · 核心每次更新只讓新的 active 進來 8%,拿到相鄰兩次 load1 就能把它解出來

結果:5 秒一行,起止都對得上

查詢端用 ssar load5s 讀出來:

$ ssar load5s
collect_datetime threads load1 runq load5s stype sstate zstate act act_rto actr actr_rto actd
2026-10-21T22:31:44 554 0.02 1 0 5s N U - - - - -
2026-10-21T22:31:49 618 3.23 17 40 5s Y Z 40 100.0% 16 94.1% 24
2026-10-21T22:31:54 617 6.17 17 40 5s Y U - - - - -
2026-10-21T22:32:00 617 8.88 19 40 5s Y U - - - - -
2026-10-21T22:32:05 618 11.37 17 40 5s Y U - - - - -
2026-10-21T22:32:10 621 13.66 18 40 5s Y U - - - - -
2026-10-21T22:32:15 621 15.77 17 40 5s Y U - - - - -
2026-10-21T22:32:20 552 14.51 1 0 5s N U - - - - -

六行 40,一個不差;尖峰從 22:31:44 到 22:31:49 之間開始,從 22:32:15 到 22:32:20 之間結束,誤差就是一個 5 秒週期。load1 那一列從 3.23 慢慢爬到 15.77,正好是圖 2 裡那條曲線。這就是 SSAR 文件裡說的“load5s 是油門,load1 是車速”13。

它也有兩個小毛病。一是精度受 /proc/loadavg 的兩位小數限制,反推出來的數有 ±0.1 左右的誤差,對“幾十個執行緒”的問題無關緊要。二是它靠“第一列變了”來判斷核心更新過,機器很閒、load1 連續兩次都是 0.03 時就認不出來,那 5 秒就沒有記錄,我們的輸出裡 22:31:19、22:31:24 就缺了行。閒的時候缺幾行,並不影響排查。

load5s 回答了“有多少、什麼時候”。可是 40 個執行緒,是誰?

五、是誰:程序級的歷史

各家的程序記錄

要知道是誰,就得有程序級的歷史。八個工具裡,sar、tsar 根本不記程序,nmon 和 collectl 記,但 5 分鐘、1 分鐘一次,都沒碰上。剩下幾家:

  • atop 記的是區間累計:10 分鐘(或 60 秒)裡每個程序用了多少 CPU、記憶體、磁碟,已退出的程序也在。它清楚地告訴我們 16 個 stress-ng-cpu 各用了 7.5 秒 CPU。但 24 個 stuck 也以 <stuck> 出現,CPU 是 0%,狀態是 E(已退出),看不出它們曾經卡在 D 狀態。累計值擅長回答“誰花了資源”,不擅長回答“誰在等”。
  • below 每 5 秒存一份完整的程序快照,包括狀態14。在尖峰的 6 個取樣點上,它都記下了 16 個 RUNNING 的 stress-ng-cpu 和 24 個 UNINTERRUPTIBLE 的 stuck,單就這一次實驗,它和 SSAR 打了個平手。
  • SSAR 的 ssar procs 每分鐘存一份全部程序的快照,22:32:00 這一份正好落在尖峰裡,狀態裡能看到 24 個 D 的 stuck。但這是運氣,換個時刻發生,一分鐘一次的快照就會錯過。
$ below dump process -b "22:31:50" -e "22:31:55" -f datetime pid comm state
2026-10-21 22:31:53 19492 stuck UNINTERRUP
2026-10-21 22:31:53 19493 stuck UNINTERRUP
2026-10-21 22:31:53 19540 stress-ng-cpu RUNNING
...

既然 below 每 5 秒都存了程序狀態,SSAR 還有什麼不同?我們注意到 below 記的是程序,狀態取的是主執行緒的狀態。如果卡住的不是 24 個程序,而是一個程序裡的 24 個執行緒呢?

六、是哪個執行緒,卡在哪裡:load2p

現象:程序是 S,load 卻是 24

我們又做了一次實驗。22:45:45,起一個名叫 javaapp 的程序,它開 24 個工作執行緒(執行緒名 pool-worker),每個執行緒都去 vfork,同樣卡 30 秒,而主執行緒只是在 pthread_join 里正常睡眠。這更接近 Java、Go 服務的真實情況:一個程序,幾百個執行緒,出事的是其中一部分。

真實情況是:程序 24317 裡有 24 個執行緒處於 D 狀態,主執行緒處於 S 狀態。各家事後的記錄是這樣的:

一個程序裡 24 個執行緒卡在 D 狀態(22:45:45–22:46:15)
工具事後讀到的能定位到執行緒嗎
SSAR load5s22:45:55 起每 5 秒都是 24,其中 actr 0、actd 24—
SSAR load2p24 個 D 執行緒,全在 pid 24317,執行緒名 pool-worker,逐個列出 tid 和所在 CPU;核心棧都是 __do_sys_vfork能,帶核心棧
SSAR procsjavaapp 狀態 S,執行緒數 25不能
below5 個取樣點裡 javaapp 都是 SLEEPING,執行緒數 25;沒有任何程序是 UNINTERRUPTIBLE不能
atopjavaapp 和退出的子程序 <pool-worker>,CPU 都是 0%不能
PCP22:45:49 的 proc.runq.blocked 是 0不能
sar · tsarload1 6.77 / 5.32,runq 0,blocked 0不能

below 和 SSAR procs 記錄的是程序,狀態取自主執行緒;PCP 22:45:49 的取樣已經線上程卡住 3 秒之後,仍然是 0,看來它也是按程序統計的(推斷)。

圖 6 · 卡住的是執行緒而不是程序時,只有按執行緒掃描的 load2p 認得出來

load5s 這一次還暴露了一個邊界:尖峰前機器很閒,22:45:25 起 load1 已經降到 0,公式裡的“上一次”是 0,sresar 就退回到直接拿 load1 當 load5s(輸出裡 stype 一列標成 1m),22:45:50 那一行只記成了 1,從 22:45:55 起才是 24。同一段輸出裡,22:44:10、22:44:40、22:45:25 之間也缺了好幾行,就是上一節說的“load1 沒變就認不出更新”。

SSAR 的做法:超過閾值才抓現場

load2p 的思路是“平時只記數,超標才抓人”。還是 sresar 裡那個 100 毫秒看一眼 /proc/loadavg 的執行緒,它算出的 load5s 一旦超過 CPU 核数 × load5s_threshold(預設閾值 1,4 核機器就是 4),並且正在上漲(load5s 和 load1 都比上一次高 5% 以上)或者最近 10 次更新裡還沒抓過,就透過 eventfd 叫醒另一個執行緒,後者立刻遍歷 /proc/*/task/*/stat,把所有 R 和 D 狀態的執行緒連同所在 CPU、優先順序記下來,再抽樣最多 100 個 D 狀態執行緒,讀它們的 /proc/<tid>/stack12。

sresar 的幾個執行緒和它寫下的檔案 左邊是常駐程序 sresar 的執行緒:load1_thread 每 100 毫秒讀 /proc/loadavg,算出 load5s 寫進 2026102122_load5s,超過 4 核乘閾值 1 且在上漲時,透過 eventfd 叫醒 loadrd_thread;後者掃描 /proc/*/task/*/stat,把 R 和 D 執行緒寫進 20261021223149_loadrd,抽樣最多 100 個 D 執行緒讀核心棧寫進 _stack。另幾個執行緒每分鐘複製 stat、meminfo、vmstat 等原檔案,並把全部程序寫進 sresar25.gz 和 cmdline.gz。右邊是資料目錄的檔案列表,下面是查詢端 ssar,按 sys.conf 現場解析這些檔案。 sresar(常駐採集程序) /var/log/sre_proc/data/2026102122/ load1_thread 每 100 ms 讀 /proc/loadavg,第一列變了就反推 load5s 追加一行到 _load5s load5s > 4 核 × 1 且在上漲 → 叫醒 eventfd loadrd_thread 平時睡著 掃 /proc/*/task/*/stat,只留 R 和 D 每個執行緒的 tid、pid、CPU 號 → _loadrd 抽 ≤100 個 D 讀 /proc/<tid>/stack → _stack snapsys / sresar / cmdinfo 執行緒 每分鐘 原樣複製 stat、meminfo、vmstat、snmp… 全部程序 → sresar25.gz、cmdline.gz 一小時一個目錄 節選 2026102122_load5s 5 秒一行 20261021223149_loadrd 觸發時 20261021223149_stack 觸發時 20261021223200_stat 原檔案 20261021223200_meminfo 原檔案 20261021223200_vmstat … 原檔案 20261021223200_sresar25.gz 程序 20261021223200_cmdline.gz 查詢時才解析 ssar(查詢端) 按 /etc/ssar/sys.conf ssar load5s · load2p -c · procs ssar -o 'cfile=stat|line_begin=…'
圖 5 · sresar 平時只記數字和原檔案,load5s 超過閾值時才叫醒另一個執行緒,按執行緒抓現場

查詢時用 ssar load5s -z 找出抓過現場的時刻,再用 ssar load2p -c 開啟。第一次實驗在 22:31:49 抓到的現場是這樣的:

$ ssar load2p -c 2026-10-21T22:31:49 --loadd
count s pid cmd cmdline
1 D 19492 stuck -
1 D 19493 stuck -
...(共 24 行)
$ ssar load2p -c 2026-10-21T22:31:49 --stackinfo
count s_unit wchan stackinfo
24 number kernel_clone kernel_clone,__do_sys_vfork,x64_sys_call,do_syscall_64,entry_SYSCALL_64_after_hwframe

--stackinfo 把 D 狀態執行緒按核心棧聚合:24 個全卡在 __do_sys_vfork 裡。生產環境裡,這一行可能是 rwsem_down_read_slowpath、jbd2_log_wait_commit 或者 nfs_wait_bit_killable,一眼就能分辨是鎖、日誌提交還是網路儲存。SSAR 的分享裡有一個例子,D 狀態執行緒 80% 卡在 call_rwsem_down_read_failed,順著棧找到了缺頁時搶 mmap 讀寫鎖的那一組程序13。

--psr 還會按 CPU 號聚合 R 狀態執行緒,用來發現 cgroup 綁核不均:如果 150 個可執行執行緒都擠在 34 號 CPU 上,平均負載看起來並不誇張,但那顆核上的服務已經排了很長的隊。

我們也看到了它在新核心上的一處小瑕疵:輸出裡還有一列 nwchan,本意是取棧頂函式地址的後 6 位。老核心的 /proc/<pid>/stack 每行是 [<ffffffff813003d4>] call_rwsem_down_read_failed 這種格式,而 4.x 以後的核心出於安全考慮把地址統一顯示成 [<0>],SSAR 按固定偏移擷取,這一列就成了亂碼。上面的輸出裡我們把它刪掉了。它說明 SSAR 主要是在阿里內部的 3.10、4.9、4.19 這幾代核心上長大的。

執行緒名、核心棧都有了。還剩一個問題:如果當時就是沒記某個指標呢?

七、事後才想到的指標

現象:sar 不記的,事後就沒有

回頭看第二節,sar 的 blocked 一直是 0,我們是查了核心原始碼才知道它只算 iowait。假如這時還想看看那一分鐘裡 fork 了多少程序(/proc/stat 的 processes 行),sar 沒有這一列,也就無從查起。sar、atop、tsar 的資料檔案都是採集時就解析好的二進位制或文字記錄,記了哪些欄位,在寫程式碼時就定死了。

SSAR 的做法:存原檔案,查詢時再解析

SSAR 的整機資料,存的是 /proc 檔案本身。上面圖 5 右邊的資料目錄裡,每分鐘一組 20261021223200_stat、_meminfo、_vmstat、_snmp……都是原樣複製的檔案。哪一行哪一列是什麼指標,寫在查詢端的配置 /etc/ssar/sys.conf 裡,查詢時現場解析8。

所以只要檔案採了,任何一個欄位都能事後查。比如 fork 速率和 procs_running、procs_blocked,不改配置,直接在命令列裡寫:

$ ssar -i 1 -o 'metric=d|cfile=stat|line_begin=processes|column=2|alias=forks;
metric=c|cfile=stat|line_begin=procs_running|column=2|alias=running;
metric=c|cfile=stat|line_begin=procs_blocked|column=2|alias=blocked'
collect_datetime forks/s running blocked
2026-10-21T22:31:00 13.07 2 0
2026-10-21T22:32:00 14.30 18 0
2026-10-21T22:33:00 10.98 3 0

metric=d 表示取相鄰兩次的差再除以秒數,c 表示取當時的值。這同時也印證了第二節的結論:procs_blocked 在 24 個 D 狀態程序面前確實是 0。

想加一個原本不採的檔案也只要一行配置。比如我們在實驗開始前往 sys.conf 里加了 {src_path='/proc/pressure/io', turn=true, cfile='pressure_io'},之後 IO 的 PSI 就每分鐘落盤一份,同樣可以用 -o 查。

這條路的代價是:只對“檔案”有效,而且得是事先採了的檔案。PCP 走的是另一條路,每類資料來源寫一個 PMDA 外掛,指標有名字、有後設資料、能遠端查,擴充套件能力最強,但新加一個指標就是寫一個外掛。below 的資料模型則是寫在 Rust 程式碼裡的。

記得越多越細,代價是什麼?

八、記錄的代價

我們在尖峰之後取了一段 40 分鐘的平穩視窗(22:40 到 23:20,期間包含第二次實驗),記錄每個採集程序的 CPU 時間、常駐記憶體和資料檔案的增長,換算成每小時 CPU 秒和每天的磁碟佔用:

採集端的開銷(約 550 個執行緒的 4 核機器)
採集端(間隔)CPU 秒 / 小時常駐記憶體磁碟 / 天
SSAR sresar(60 秒,load 5 秒)4.14.4 MB50 MB
below(5 秒,--compress)32.612.7 MB362 MB
below(5 秒,不壓縮)41.915.5 MB4.0 GB
collectl(10 秒 / 程序 60 秒)7.631 MB112 MB
PCP(pmcd + PMDA + pmlogger)0.546 MB51 MB
atop(600 秒)0.218 MB8.0 MB
atop(60 秒)1.511 MB28 MB
tsar(cron 60 秒)1.1—1.2 MB
sar(600 秒 / 60 秒)<0.12.5 MB0.3 / 3.4 MB
nmon(300 秒)0.22.3 MB0.4 MB

CPU 是採集程序及其子程序的 utime + stime 之和,視窗 22:40–23:20(40 分鐘);collectl 按服務方式的 10 秒間隔重啟後測了 37 分鐘;帶 --compress 的 below(官方 systemd 單元的引數)另測了 15 分鐘。tsar 每分鐘由 cron 拉起一次,沒有常駐程序。一小時 3600 CPU 秒是一個核跑滿。

圖 7 · 記錄的代價:SSAR 每小時用 4 秒 CPU、每天 50 MB,比 sar 重,但比 5 秒一採的 below 輕一個數量級

幾點值得說一下:

SSAR 的開銷主要花在程序上。 每分鐘把全部程序的狀態和命令列壓縮落盤,一天 50 MB,預設保留 7 天,也就 350 MB 左右;每小時 4 秒 CPU,大約是一個核的 0.1%。它的 README 說在萬臺規模上觀察到的上限是單核 3%、記憶體 4 MB8,和我們測到的量級一致。load5s 那個每 100 毫秒醒一次的執行緒幾乎不花什麼,讀一次 /proc/loadavg 只是幾十個位元組。

below 貴在“每 5 秒一份完整快照”。 它不存增量,每個樣本都完整儲存,這正是它能穩穩抓住尖峰、回放體驗好的原因3。但不加 --compress 時一天 4 GB,加上之後也有 362 MB,是 SSAR 的七倍;CPU 大約一個核的 1%。直接 below record 預設是不壓縮的,要按官方的 systemd 單元加上 --compress。

sar、atop、nmon 幾乎不花錢,代價是我們在第二、五節看到的:10 分鐘一個點,或者只有區間累計值。PCP 的 CPU 不高,但 pmcd 和一組 PMDA 程序加起來常駐 46 MB,是這裡最重的部署。

這些數字和程序數直接相關:SSAR 和 below 每次都要遍歷 /proc,機器上有幾千個執行緒時,開銷會成比例上漲。

九、對照與選擇

把前面的結果放進一張表:

事後排查一次 load 尖峰,各工具能回答到哪一步
工具何時、多大是誰哪個執行緒、卡在哪事後查新指標cgroup
SSARload5s,5 秒精度每分鐘程序快照load2p:按執行緒 + 核心棧存原檔案,事後解析不記
below5 秒的執行 / 阻塞計數,沒有 load每 5 秒程序狀態只到程序固定的資料模型以 cgroup v2 為中心
atop區間末尾的 load 平均區間累計,含已退出程序不記固定欄位程序所屬 cgroup v2
PCP60 秒的 load 平均和 R / D 計數預設只有程序記憶體,可另配不記寫 PMDA 外掛,要事先配可配
collectl10 秒的 RunQ 和 load 平均每分鐘程序快照不記固定欄位不記
tsar每分鐘 load 平均和 runq不記不記寫 C 模組不記
sar10 分鐘一次 load 平均不記不記固定欄位不記
nmon5 分鐘一次當時的 top 程序不記固定欄位不記

“D 狀態計數”只有 PCP 和 below 的程序掃描是真的全部 D,sar、collectl、tsar 讀的 procs_blocked 只含等塊裝置 IO 的任務。

圖 8 · 同一個尖峰排查到底:SSAR 在“多大、哪個執行緒、卡在哪”上最強,below 在“是誰”和 cgroup 上最強,atop 擅長回答誰花了資源

從這張表看,沒有一個工具能覆蓋所有格子,選擇取決於機器上最常出的是哪類問題:

物理機、大規模同構叢集,問題集中在 load 尖峰和 D 狀態,SSAR 最合適。load5s 和 load2p 是這個家族裡獨一份的設計,而且它在阿里內部的萬臺規模上跑過8,開銷實測也很低。但要接受它的幾個短板:它對 cgroup 和容器一無所知,看不出是哪個容器把 load 打上去的;倉庫最後一次提交是 2025 年 2 月,幾乎靠一位維護者;Ubuntu 24.04 的軟體源裡搜不到它,我們是用倉庫裡的原始碼編譯安裝的(倉庫也帶了打 rpm / deb 的指令碼),安裝指令碼只認 systemd;在新核心上還有 nwchan 這樣的小毛病。

容器化、cgroup v2 的機器,below 更合適。它每 5 秒一份完整的程序和 cgroup 快照,事後回放的體驗最好,代價是隻支援 cgroup v2,以及比別人多一個數量級的 CPU 和磁碟。

只想知道“這 10 分鐘誰花了資源”,atop 就夠了,發行版直接裝,已退出的程序也不會漏。

需要全公司統一的指標體系、遠端查詢,PCP 是這裡唯一一個按“監控框架”設計的,代價是部署和學習成本。sar 適合做所有機器上的保底,tsar、nmon、collectl 則更多是存量環境裡還在用。

它們也不互斥。比較實際的組合是:sar 或 atop 作保底,再加上 SSAR 專門盯 load 尖峰,容器機器換成 below。

十、回到 22:31

現在可以回答開頭的問題了。登入後用 ssar load5s -z 一查,22:31:49 有一行 zstate Z:load5s 從 0 跳到 40,其中 R 16 個、D 24 個,持續了 6 個 5 秒週期。ssar load2p -c 2026-10-21T22:31:49 給出了這 40 個執行緒:16 個 stress-ng-cpu 在搶 4 個 CPU,24 個 stuck 卡在 __do_sys_vfork 裡。22:34 讀到的那個 2.90,只是 30 秒尖峰過去兩分鐘後的餘溫。

小結

  1. 預設配置的 sar 能看清一次 30 秒的尖峰嗎? 不能。10 分鐘一次的取樣點落在尖峰兩側,改成 60 秒也有一半的機會錯過,而且 blocked 只算等塊裝置 IO 的任務。
  2. 為什麼各家的 load 數字差這麼多? load1 是核心每 5 秒更新一次的指數平均,舊值保留 92%,追不上也退不下來;各工具在不同時刻讀到的只是這條曲線上的不同點。
  3. SSAR 的 load5s 是什麼? 每 100 毫秒盯一次 /proc/loadavg,在核心每次更新後,用相鄰兩次 load1 反推出那一刻的 R + D 執行緒數,精度 5 秒,不需要核心模組。
  4. 是誰? atop 能告訴你誰花了 CPU,below 每 5 秒存一份程序狀態,SSAR 每分鐘存一份程序快照;但只有 SSAR 的 load2p 在超閾值時按執行緒抓現場,並帶上 D 狀態執行緒的核心棧。
  5. 事後想到的指標還能查嗎? SSAR 存的是 /proc 原檔案,查詢時才解析,只要檔案採了,任何欄位都能事後查。
  6. 代價和取捨? 見第八、九節:SSAR 的強項在 load 和執行緒現場,短板在 cgroup、維護活躍度和部署方式;容器化、cgroup v2 的機器上 below 更合適,要全公司統一的指標體系就看 PCP。

參考資料

參考資料

  1. sysstat:Performance monitoring tools for LinuxGitHubgithub.com ↗sadc 採集、sar 查詢。文中用 Ubuntu 24.04 的 12.6.1,發行版預設每 10 分鐘採一次(systemd 定時器 *:00/10 或 cron)。https://github.com/sysstat/sysstat ↩

  2. atop:Advanced System & Process Monitoratoptool.nlatoptool.nl ↗文中用 2.10.0。預設 LOGINTERVAL=600;藉助程序記賬,區間內已退出的程序也會以 <名字> 的形式列出累計資源。https://www.atoptool.nl/ ↩ ↩2

  3. below: a time traveling resource monitoring toolDaniel Xu · Meta Open Source · 2021-09-21developers.facebook.com ↗開發 below 的原因:atop 缺少 cgroup 支援,增量壓縮的儲存格式在壓力大時容易丟點;below 每個樣本完整儲存並用 zstd 壓縮。https://developers.facebook.com/blog/post/2021/09/21/below-time-travelling-resource-monitoring-tool/ ↩ ↩2

  4. Performance Co-Pilotpcp.iopcp.io ↗pmcd + 各類 PMDA 採集,pmlogger 寫歸檔。文中用 Ubuntu 24.04 的 6.2.0,config.default 裡大部分指標 60 秒一次,程序記憶體類 5 分鐘一次。https://pcp.io/ ↩

  5. collectlSourceForgecollectl.sourceforge.net ↗文中用 4.3.1,按 /etc/collectl.conf 的 DaemonCommands(-f … -m -F60 -s+YZ)執行:系統指標 1 秒、程序 60 秒。https://collectl.sourceforge.net/ ↩

  6. nmon for LinuxSourceForgenmon.sourceforge.io ↗文中用 16p,按常見的 nmon -f -T -s 300 -c 288(每 5 分鐘一次、一天一個檔案)執行。https://nmon.sourceforge.io/ ↩

  7. alibaba/tsar:Taobao System Activity ReporterGitHubgithub.com ↗C 編寫、模組化,cron 每分鐘 tsar --cron 一次;只有系統級指標。倉庫最後一次提交是 2022-02-18。ssar 自帶的 tsar2 就是照著它的輸出格式寫的。https://github.com/alibaba/tsar ↩

  8. anolis/ssar:SRE System Activity ReporterGitee · 2025-02-24gitee.com ↗龍蜥社群倉庫,MulanPSL-2.0。文中用的是 1.0.5(最後一次提交 2025-02-24),README_zh-cn.md 是完整的中文手冊。https://gitee.com/anolis/ssar ↩ ↩2 ↩3 ↩4

  9. Linux 原始碼:fs/proc/stat.cGitHubgithub.com ↗/proc/stat 的 procs_blocked 輸出的是 nr_iowait(),只統計在等塊裝置 IO 的任務,不是全部 D 狀態執行緒。https://github.com/torvalds/linux/blob/master/fs/proc/stat.c ↩

  10. Linux 原始碼:kernel/sched/loadavg.cGitHubgithub.com ↗calc_load():newload = load * exp + active * (FIXED_1 - exp);EXP_1 = 1884、FIXED_1 = 2048、LOAD_FREQ = 5*HZ+1 定義在 include/linux/sched/loadavg.h。https://github.com/torvalds/linux/blob/master/kernel/sched/loadavg.c ↩

  11. Linux Load Averages: Solving the MysteryBrendan Gregg · 2017-08-08brendangregg.com ↗Linux 的 load average 為什麼把 D 狀態(不可中斷)也算進去,以及指數衰減平均的來歷。https://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html ↩

  12. ssar 原始碼:sresar/sresar.cGitee · 2025-02-24gitee.com ↗load1_thread 每 100 ms 讀一次 /proc/loadavg,第一列變化時用 1884 和 164 反推 load5s,超過 CPU 核數 × load5s_threshold 就透過 eventfd 叫醒 loadrd_thread 去掃描 R / D 執行緒和核心棧。https://gitee.com/anolis/ssar/blob/master/sresar/sresar.c ↩ ↩2

  13. 直播回顧:準確性提升到 5 秒級,ssar 獨創的 load5s 指標有多硬核?闻茂泉 · OpenAnolis 龍蜥社群 · 2022-01-26segmentfault.com ↗load5s 即核心的全域性 active 值(R + D 執行緒數),在使用者態以工程方法採集,不依賴核心模組;load2p 的六個檢視;執行緒數耗盡和記憶體直接回收兩個案例。https://segmentfault.com/a/1190000041343974 ↩ ↩2

  14. facebookincubator/belowGitHubgithub.com ↗Rust 編寫,預設 5 秒取樣,只支援 cgroup v2。文中用原始碼編譯的 0.11.0(提交 bbddf87)。https://github.com/facebookincubator/below ↩