设想一台 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。
参考资料
Footnotes
-
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 ↩