設想一臺 4 核的機器,晚上 22:31 業務報了一次響應變慢,持續不到一分鐘。幾分鐘後,22:34 我們登入上去,/proc/loadavg 的三個數是:
2.90 2.87 1.41load 已經在往下走了,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 上最老牌的一個,C | 600 秒(發行版定時任務) | 不記(pidstat 只能實時看) | 二進位制 sa 檔案 |
| atop 2.10.0 | Atoptool,C | 600 秒 | 區間內每個程序的累計資源,含已退出程序 | 二進位制,增量壓縮 |
| below 0.11.0 | Meta,2021 年開源;Rust | 5 秒 | 每次完整快照:程序狀態、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 秒。
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 sa60022:19:36 runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked22:29:36 1 556 0.16 0.30 0.34 022:39:36 0 556 0.07 0.97 0.99 0發行版預設每 10 分鐘採一次,兩個點一個在尖峰前兩分鐘,一個在尖峰後七分鐘。只有 ldavg-5 和 ldavg-15 微微抬頭,暗示中間“有過點什麼”。
第一次嘗試:把間隔調到 60 秒
很多人會把 sar 的間隔改成 1 分鐘。我們同時跑了一份 60 秒的:
$ sar -q -f sa6022:29:36 runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked22:31:36 0 552 0.03 0.20 0.30 022:32:36 0 552 11.37 3.76 1.53 022:32:36 的 ldavg-1 跳到了 11.37,看得出出過事。可是 runq-sz 是 0,blocked 也是 0,因為這個取樣點比尖峰結束晚了 21 秒,runq-sz 是取樣那一瞬間的值。一個 30 秒的事件,用 60 秒的間隔去採,大約一半的機會會漏掉。
每個工具的取樣點落在哪裡
把八個工具(sar 和 atop 各有 600 秒、60 秒兩份)的取樣時刻畫在同一條時間軸上,就能看清誰“碰上了”、誰沒碰上:
能穩穩抓住這 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:
這裡 1884/2048 ≈ 0.92,約等於 。每次更新,舊值保留 92%,新數進來 8%。把 D 狀態也算進 load 是 Linux 特有的做法,本意是讓“等磁碟”的壓力也能體現出來11。
於是 load1 就像一輛車的車速,active 是油門。油門一下踩到 40,車速要好幾分鐘才能追上;油門鬆開,車速也是慢慢降下來。我們這次的 30 秒尖峰裡,load1 只爬到 15.77;尖峰結束 20 秒後 sar 讀到 11.37,此時真實的活躍執行緒已經回到 1 個左右:
所以 sar 的 11.37 既不是峰值,也不是取樣時的狀態,而是“過去幾分鐘加權後的餘溫”。PCP 那個 0.02 更極端:22:31:49 尖峰已經開始了 4 秒,核心還沒輪到下一次更新。
能不能把那一次 active 找回來
既然核心每 5 秒用一個精確的 active 去更新 load1,公式又是公開的,那隻要拿到相鄰兩次 load1,就能把 active 反解出來:
難點在於“相鄰兩次”。核心的 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 只保留兩位小數帶來的取整偏差。拿尖峰開始後的第一次更新來算一遍:
結果:5 秒一行,起止都對得上
查詢端用 ssar load5s 讀出來:
$ ssar load5scollect_datetime threads load1 runq load5s stype sstate zstate act act_rto actr actr_rto actd2026-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% 242026-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 state2026-10-21 22:31:53 19492 stuck UNINTERRUP2026-10-21 22:31:53 19493 stuck UNINTERRUP2026-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 狀態。各家事後的記錄是這樣的:
| 工具 | 事後讀到的 | 能定位到執行緒嗎 |
|---|---|---|
| SSAR load5s | 22:45:55 起每 5 秒都是 24,其中 actr 0、actd 24 | — |
| SSAR load2p | 24 個 D 執行緒,全在 pid 24317,執行緒名 pool-worker,逐個列出 tid 和所在 CPU;核心棧都是 __do_sys_vfork | 能,帶核心棧 |
| SSAR procs | javaapp 狀態 S,執行緒數 25 | 不能 |
| below | 5 個取樣點裡 javaapp 都是 SLEEPING,執行緒數 25;沒有任何程序是 UNINTERRUPTIBLE | 不能 |
| atop | javaapp 和退出的子程序 <pool-worker>,CPU 都是 0% | 不能 |
| PCP | 22:45:49 的 proc.runq.blocked 是 0 | 不能 |
| sar · tsar | load1 6.77 / 5.32,runq 0,blocked 0 | 不能 |
below 和 SSAR procs 記錄的是程序,狀態取自主執行緒;PCP 22:45:49 的取樣已經線上程卡住 3 秒之後,仍然是 0,看來它也是按程序統計的(推斷)。
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。
查詢時用 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 blocked2026-10-21T22:31:00 13.07 2 02026-10-21T22:32:00 14.30 18 02026-10-21T22:33:00 10.98 3 0metric=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 秒和每天的磁碟佔用:
| 採集端(間隔) | CPU 秒 / 小時 | 常駐記憶體 | 磁碟 / 天 |
|---|---|---|---|
| SSAR sresar(60 秒,load 5 秒) | 4.1 | 4.4 MB | 50 MB |
| below(5 秒,--compress) | 32.6 | 12.7 MB | 362 MB |
| below(5 秒,不壓縮) | 41.9 | 15.5 MB | 4.0 GB |
| collectl(10 秒 / 程序 60 秒) | 7.6 | 31 MB | 112 MB |
| PCP(pmcd + PMDA + pmlogger) | 0.5 | 46 MB | 51 MB |
| atop(600 秒) | 0.2 | 18 MB | 8.0 MB |
| atop(60 秒) | 1.5 | 11 MB | 28 MB |
| tsar(cron 60 秒) | 1.1 | — | 1.2 MB |
| sar(600 秒 / 60 秒) | <0.1 | 2.5 MB | 0.3 / 3.4 MB |
| nmon(300 秒) | 0.2 | 2.3 MB | 0.4 MB |
CPU 是採集程序及其子程序的 utime + stime 之和,視窗 22:40–23:20(40 分鐘);collectl 按服務方式的 10 秒間隔重啟後測了 37 分鐘;帶 --compress 的 below(官方 systemd 單元的引數)另測了 15 分鐘。tsar 每分鐘由 cron 拉起一次,沒有常駐程序。一小時 3600 CPU 秒是一個核跑滿。
幾點值得說一下:
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,機器上有幾千個執行緒時,開銷會成比例上漲。
九、對照與選擇
把前面的結果放進一張表:
| 工具 | 何時、多大 | 是誰 | 哪個執行緒、卡在哪 | 事後查新指標 | cgroup |
|---|---|---|---|---|---|
| SSAR | load5s,5 秒精度 | 每分鐘程序快照 | load2p:按執行緒 + 核心棧 | 存原檔案,事後解析 | 不記 |
| below | 5 秒的執行 / 阻塞計數,沒有 load | 每 5 秒程序狀態 | 只到程序 | 固定的資料模型 | 以 cgroup v2 為中心 |
| atop | 區間末尾的 load 平均 | 區間累計,含已退出程序 | 不記 | 固定欄位 | 程序所屬 cgroup v2 |
| PCP | 60 秒的 load 平均和 R / D 計數 | 預設只有程序記憶體,可另配 | 不記 | 寫 PMDA 外掛,要事先配 | 可配 |
| collectl | 10 秒的 RunQ 和 load 平均 | 每分鐘程序快照 | 不記 | 固定欄位 | 不記 |
| tsar | 每分鐘 load 平均和 runq | 不記 | 不記 | 寫 C 模組 | 不記 |
| sar | 10 分鐘一次 load 平均 | 不記 | 不記 | 固定欄位 | 不記 |
| nmon | 5 分鐘一次 | 當時的 top 程序 | 不記 | 固定欄位 | 不記 |
“D 狀態計數”只有 PCP 和 below 的程序掃描是真的全部 D,sar、collectl、tsar 讀的 procs_blocked 只含等塊裝置 IO 的任務。
從這張表看,沒有一個工具能覆蓋所有格子,選擇取決於機器上最常出的是哪類問題:
物理機、大規模同構叢集,問題集中在 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 秒尖峰過去兩分鐘後的餘溫。
小結
- 預設配置的 sar 能看清一次 30 秒的尖峰嗎? 不能。10 分鐘一次的取樣點落在尖峰兩側,改成 60 秒也有一半的機會錯過,而且
blocked只算等塊裝置 IO 的任務。 - 為什麼各家的 load 數字差這麼多? load1 是核心每 5 秒更新一次的指數平均,舊值保留 92%,追不上也退不下來;各工具在不同時刻讀到的只是這條曲線上的不同點。
- SSAR 的 load5s 是什麼? 每 100 毫秒盯一次
/proc/loadavg,在核心每次更新後,用相鄰兩次 load1 反推出那一刻的 R + D 執行緒數,精度 5 秒,不需要核心模組。 - 是誰? atop 能告訴你誰花了 CPU,below 每 5 秒存一份程序狀態,SSAR 每分鐘存一份程序快照;但只有 SSAR 的 load2p 在超閾值時按執行緒抓現場,並帶上 D 狀態執行緒的核心棧。
- 事後想到的指標還能查嗎? SSAR 存的是
/proc原檔案,查詢時才解析,只要檔案採了,任何欄位都能事後查。 - 代價和取捨? 見第八、九節:SSAR 的強項在 load 和執行緒現場,短板在 cgroup、維護活躍度和部署方式;容器化、cgroup v2 的機器上 below 更合適,要全公司統一的指標體系就看 PCP。
參考資料
參考資料
-
sysstat:Performance monitoring tools for Linuxgithub.com ↗sadc 採集、sar 查詢。文中用 Ubuntu 24.04 的 12.6.1,發行版預設每 10 分鐘採一次(systemd 定時器
*:00/10或 cron)。https://github.com/sysstat/sysstat ↩ -
atop:Advanced System & Process Monitoratoptool.nl ↗文中用 2.10.0。預設
LOGINTERVAL=600;藉助程序記賬,區間內已退出的程序也會以<名字>的形式列出累計資源。https://www.atoptool.nl/ ↩ ↩2 -
below: a time traveling resource monitoring tooldevelopers.facebook.com ↗開發 below 的原因:atop 缺少 cgroup 支援,增量壓縮的儲存格式在壓力大時容易丟點;below 每個樣本完整儲存並用 zstd 壓縮。https://developers.facebook.com/blog/post/2021/09/21/below-time-travelling-resource-monitoring-tool/ ↩ ↩2
-
Performance Co-Pilotpcp.io ↗pmcd + 各類 PMDA 採集,pmlogger 寫歸檔。文中用 Ubuntu 24.04 的 6.2.0,
config.default裡大部分指標 60 秒一次,程序記憶體類 5 分鐘一次。https://pcp.io/ ↩ -
collectlcollectl.sourceforge.net ↗文中用 4.3.1,按 /etc/collectl.conf 的 DaemonCommands(
-f … -m -F60 -s+YZ)執行:系統指標 1 秒、程序 60 秒。https://collectl.sourceforge.net/ ↩ -
nmon for Linuxnmon.sourceforge.io ↗文中用 16p,按常見的
nmon -f -T -s 300 -c 288(每 5 分鐘一次、一天一個檔案)執行。https://nmon.sourceforge.io/ ↩ -
alibaba/tsar:Taobao System Activity Reportergithub.com ↗C 編寫、模組化,cron 每分鐘
tsar --cron一次;只有系統級指標。倉庫最後一次提交是 2022-02-18。ssar 自帶的 tsar2 就是照著它的輸出格式寫的。https://github.com/alibaba/tsar ↩ -
anolis/ssar:SRE System Activity Reportergitee.com ↗龍蜥社群倉庫,MulanPSL-2.0。文中用的是 1.0.5(最後一次提交 2025-02-24),README_zh-cn.md 是完整的中文手冊。https://gitee.com/anolis/ssar ↩ ↩2 ↩3 ↩4
-
Linux 原始碼:fs/proc/stat.cgithub.com ↗
/proc/stat的procs_blocked輸出的是nr_iowait(),只統計在等塊裝置 IO 的任務,不是全部 D 狀態執行緒。https://github.com/torvalds/linux/blob/master/fs/proc/stat.c ↩ -
Linux 原始碼:kernel/sched/loadavg.cgithub.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 ↩ -
Linux Load Averages: Solving the Mysterybrendangregg.com ↗Linux 的 load average 為什麼把 D 狀態(不可中斷)也算進去,以及指數衰減平均的來歷。https://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html ↩
-
ssar 原始碼:sresar/sresar.cgitee.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 -
直播回顧:準確性提升到 5 秒級,ssar 獨創的 load5s 指標有多硬核?segmentfault.com ↗load5s 即核心的全域性 active 值(R + D 執行緒數),在使用者態以工程方法採集,不依賴核心模組;load2p 的六個檢視;執行緒數耗盡和記憶體直接回收兩個案例。https://segmentfault.com/a/1190000041343974 ↩ ↩2
-
facebookincubator/belowgithub.com ↗Rust 編寫,預設 5 秒取樣,只支援 cgroup v2。文中用原始碼編譯的 0.11.0(提交 bbddf87)。https://github.com/facebookincubator/below ↩