﻿---
title: "SSAR 与同类工具对比：一次 30 秒的 load 尖峰，事后还能看清吗"
description: "一台 4 核机器在 22:31 抖了半分钟，几分钟后登录上去，load average 已经回落，一切正常。为了弄清刚才发生了什么，我们把龙蜥社区的 SSAR 和 sar、atop、below、PCP、collectl、nmon、tsar 装在同一台机器上，用同一个尖峰去考它们：默认采样点有没有落进去，load1 为什么总是慢半拍，SSAR 的 load5s 怎么从 /proc/loadavg 反推出每 5 秒的真实线程数，卡在 D 状态的是哪个进程、哪个线程、卡在内核哪一行，事后才想到的指标还能不能查，以及这些记录各要花多少 CPU 和磁盘。"
pubDate: 2026-10-21
tags: ["Linux", "运维", "性能分析", "工具"]
hero: "./hero.svg"
refs:
  - id: ssar
    title: "anolis/ssar：SRE System Activity Reporter"
    url: https://gitee.com/anolis/ssar
    site: Gitee
    date: 2025-02-24
    note: "龙蜥社区仓库，MulanPSL-2.0。文中用的是 1.0.5（最后一次提交 2025-02-24），README_zh-cn.md 是完整的中文手册。"
  - id: ssar-src
    title: "ssar 源码：sresar/sresar.c"
    url: https://gitee.com/anolis/ssar/blob/master/sresar/sresar.c
    site: Gitee
    date: 2025-02-24
    note: "`load1_thread` 每 100 ms 读一次 `/proc/loadavg`，第一列变化时用 1884 和 164 反推 load5s，超过 `CPU 核数 × load5s_threshold` 就通过 eventfd 叫醒 `loadrd_thread` 去扫描 R / D 线程和内核栈。"
  - id: ssar-talk
    title: "直播回顾：准确性提升到 5 秒级，ssar 独创的 load5s 指标有多硬核？"
    url: https://segmentfault.com/a/1190000041343974
    author: 闻茂泉
    site: OpenAnolis 龙蜥社区
    date: 2022-01-26
    note: "load5s 即内核的全局 active 值（R + D 线程数），在用户态以工程方法采集，不依赖内核模块；load2p 的六个视图；线程数耗尽和内存直接回收两个案例。"
  - id: kernel-loadavg
    title: "Linux 源码：kernel/sched/loadavg.c"
    url: https://github.com/torvalds/linux/blob/master/kernel/sched/loadavg.c
    site: GitHub
    note: "`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。"
  - id: kernel-procstat
    title: "Linux 源码：fs/proc/stat.c"
    url: https://github.com/torvalds/linux/blob/master/fs/proc/stat.c
    site: GitHub
    note: "`/proc/stat` 的 `procs_blocked` 输出的是 `nr_iowait()`，只统计在等块设备 IO 的任务，不是全部 D 状态线程。"
  - id: gregg-load
    title: "Linux Load Averages: Solving the Mystery"
    url: https://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html
    author: Brendan Gregg
    date: 2017-08-08
    note: "Linux 的 load average 为什么把 D 状态（不可中断）也算进去，以及指数衰减平均的来历。"
  - id: sysstat
    title: "sysstat：Performance monitoring tools for Linux"
    url: https://github.com/sysstat/sysstat
    site: GitHub
    note: "sadc 采集、sar 查询。文中用 Ubuntu 24.04 的 12.6.1，发行版默认每 10 分钟采一次（systemd 定时器 `*:00/10` 或 cron）。"
  - id: atop
    title: "atop：Advanced System & Process Monitor"
    url: https://www.atoptool.nl/
    site: atoptool.nl
    note: "文中用 2.10.0。默认 `LOGINTERVAL=600`；借助进程记账，区间内已退出的进程也会以 `<名字>` 的形式列出累计资源。"
  - id: below-blog
    title: "below: a time traveling resource monitoring tool"
    url: https://developers.facebook.com/blog/post/2021/09/21/below-time-travelling-resource-monitoring-tool/
    author: Daniel Xu
    site: Meta Open Source
    date: 2021-09-21
    note: "开发 below 的原因：atop 缺少 cgroup 支持，增量压缩的存储格式在压力大时容易丢点；below 每个样本完整存储并用 zstd 压缩。"
  - id: below
    title: "facebookincubator/below"
    url: https://github.com/facebookincubator/below
    site: GitHub
    note: "Rust 编写，默认 5 秒采样，只支持 cgroup v2。文中用源码编译的 0.11.0（提交 bbddf87）。"
  - id: pcp
    title: "Performance Co-Pilot"
    url: https://pcp.io/
    site: pcp.io
    note: "pmcd + 各类 PMDA 采集，pmlogger 写归档。文中用 Ubuntu 24.04 的 6.2.0，`config.default` 里大部分指标 60 秒一次，进程内存类 5 分钟一次。"
  - id: collectl
    title: "collectl"
    url: https://collectl.sourceforge.net/
    site: SourceForge
    note: "文中用 4.3.1，按 /etc/collectl.conf 的 DaemonCommands（`-f … -m -F60 -s+YZ`）运行：系统指标 1 秒、进程 60 秒。"
  - id: nmon
    title: "nmon for Linux"
    url: https://nmon.sourceforge.io/
    site: SourceForge
    note: "文中用 16p，按常见的 `nmon -f -T -s 300 -c 288`（每 5 分钟一次、一天一个文件）运行。"
  - id: tsar
    title: "alibaba/tsar：Taobao System Activity Reporter"
    url: https://github.com/alibaba/tsar
    site: GitHub
    note: "C 编写、模块化，cron 每分钟 `tsar --cron` 一次；只有系统级指标。仓库最后一次提交是 2022-02-18。ssar 自带的 tsar2 就是照着它的输出格式写的。"
---

设想一台 4 核的机器，晚上 22:31 业务报了一次响应变慢，持续不到一分钟。几分钟后，22:34 我们登录上去，`/proc/loadavg` 的三个数是：

```text
2.90 2.87 1.41
```

load 已经在往下走了，`top` 里也没有任何异常进程。可刚才那半分钟到底发生了什么？是 CPU 不够用，还是有一批线程卡在了磁盘或锁上？如果是后者，是哪个进程、卡在哪里？

这种“事后回看”的需求，Linux 上有一整个家族的工具在做：sysstat 的 sar[^sysstat]、atop[^atop]、Meta 的 below[^below-blog]、Red Hat 主推的 PCP[^pcp]、collectl[^collectl]、nmon[^nmon]、淘宝的 tsar[^tsar]，以及本文的主角，阿里贡献给龙蜥社区的 SSAR[^ssar]。它们都在后台按固定间隔把系统状态写进磁盘，出事以后再拿出来翻。区别在于记什么、多久记一次、记下的东西能回答多少问题。

我们没有去比较功能列表，而是在同一台机器上把它们全部装好，按各自的默认配置跑起来，然后人为制造开头那样一次 30 秒的尖峰，看事后每个工具能说出什么。上面那三个数，和下文所有的输出，都来自这次实验。

## 一、实验：同一个尖峰，八个记录工具

### 这一家族的工具

先把参赛的工具介绍一下。它们都分成“采集”和“查询”两半：采集端常驻或由定时任务拉起，把数据写进本地文件；查询端事后读这些文件。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="sst0-title">参加对比的八个工具（都按默认或最常见的配置运行）</div>
<div class="dt-scroll">
<table class="dt dt-sticky" aria-labelledby="sst0-title">
<colgroup><col style="width:15%"><col style="width:22%"><col style="width:21%"><col><col style="width:18%"></colgroup>
<thead>
<tr><th scope="col">工具</th><th scope="col">出处 · 语言</th><th scope="col">默认采样间隔</th><th scope="col">进程级记录</th><th scope="col">落盘格式</th></tr>
</thead>
<tbody>
<tr><th scope="row" class="acc">SSAR 1.0.5</th><td>阿里，2021 年开源到龙蜥社区；C 采集，C++ 查询，Python 扩展脚本</td><td>整机 60 秒，<span class="acc">load 5 秒</span>，进程 60 秒</td><td>每分钟全部进程快照；load 超阈值时按<span class="acc">线程</span>抓现场和内核栈</td><td><span class="mono">/proc</span> 原文件，进程数据 gzip</td></tr>
<tr><th scope="row">sar（sysstat 12.6.1）</th><td>Linux 上最老牌的一个，C</td><td>600 秒（发行版定时任务）</td><td class="bad">不记（pidstat 只能实时看）</td><td>二进制 sa 文件</td></tr>
<tr><th scope="row">atop 2.10.0</th><td>Atoptool，C</td><td>600 秒</td><td>区间内每个进程的累计资源，含已退出进程</td><td>二进制，增量压缩</td></tr>
<tr><th scope="row">below 0.11.0</th><td>Meta，2021 年开源；Rust</td><td>5 秒</td><td>每次完整快照：进程状态、cgroup、PSI</td><td>每个样本完整存储，zstd 压缩</td></tr>
<tr><th scope="row">PCP 6.2.0</th><td>源自 SGI，现由 Red Hat 主导；C，插件式 PMDA</td><td>大部分 60 秒，进程内存 300 秒</td><td class="warn">默认只有进程内存，可另配</td><td>带元数据的归档</td></tr>
<tr><th scope="row">collectl 4.3.1</th><td>源自 HP；Perl</td><td>系统 10 秒（服务方式）、进程 60 秒</td><td>进程快照</td><td>gzip 的原始文本</td></tr>
<tr><th scope="row">nmon 16p</th><td>源自 IBM；C</td><td>常用 300 秒</td><td>加 <span class="mono">-T</span> 时记下当时的 top 进程</td><td>CSV 文本</td></tr>
<tr><th scope="row">tsar</th><td>淘宝，C，模块化</td><td>60 秒（cron）</td><td class="bad">不记</td><td>文本，每分钟一行</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">算上 sar 和 atop 各多跑的一份 60 秒配置，一共十路记录同时运行。collectl 在尖峰实验里没有加 <span class="mono">-D</span>，系统指标实际是 1 秒一次；开销测量时改回了服务方式的 10 秒。</p>
</div>
<figcaption>图 1 · sar 家族的工具：都分采集和查询两半，差别在记什么、多久记一次</figcaption>
</figure>

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：

```text
$ 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 秒的：

```text
$ 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 秒两份）的采样时刻画在同一条时间轴上，就能看清谁“碰上了”、谁没碰上：

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 478" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="sstl-title sstl-desc">
<title id="sstl-title">同一个 30 秒尖峰落在各工具采样点上的位置</title>
<desc id="sstl-desc">时间轴 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 在这段时间没有采样。右侧一列写着每个工具事后能读到的内容。</desc>
<rect class="dd-frame" width="800" height="478" rx="10"/>
<text class="t-zone" x="24" y="40">每个工具的采样点落在哪里</text>
<text class="t-note" x="608" y="40">事后能读到</text>
<rect class="dd-derived" x="297.3" y="62" width="136.0" height="356" rx="4"/>
<text class="t-note" x="365.3" y="76" text-anchor="middle">尖峰 30 秒</text>
<text class="t-name" x="24" y="111">真实情况</text>
<rect class="dd-fill-acc" x="297.3" y="102.0" width="136.0" height="10" rx="2"/>
<text class="t-val" x="608" y="111">16 R + 24 D，持续 30 秒</text>
<line class="dd-hair" x1="24" y1="122" x2="776" y2="122"/>
<text class="t-name t-acc" x="24" y="141">ssar load5s</text>
<text class="t-note" x="176" y="141" text-anchor="end">5 秒</text>
<circle class="dd-track" cx="292.8" cy="137" r="4.5"/>
<circle class="dd-fill-acc" cx="315.5" cy="137" r="5"/>
<circle class="dd-fill-acc" cx="338.1" cy="137" r="5"/>
<circle class="dd-fill-acc" cx="365.3" cy="137" r="5"/>
<circle class="dd-fill-acc" cx="388.0" cy="137" r="5"/>
<circle class="dd-fill-acc" cx="410.7" cy="137" r="5"/>
<circle class="dd-fill-acc" cx="433.3" cy="137" r="5"/>
<circle class="dd-track" cx="456.0" cy="137" r="4.5"/>
<circle class="dd-track" cx="478.7" cy="137" r="4.5"/>
<circle class="dd-track" cx="224.8" cy="137" r="4.5"/>
<circle class="dd-track" cx="501.3" cy="137" r="4.5"/>
<circle class="dd-track" cx="524.0" cy="137" r="4.5"/>
<circle class="dd-track" cx="546.7" cy="137" r="4.5"/>
<circle class="dd-track" cx="569.3" cy="137" r="4.5"/>
<text class="t-val" x="608" y="141">每 5 秒一个 40</text>
<line class="dd-hair" x1="24" y1="152" x2="776" y2="152"/>
<text class="t-name t-acc" x="24" y="171">ssar load2p</text>
<text class="t-note" x="176" y="171" text-anchor="end">超阈值触发</text>
<circle class="dd-fill-acc" cx="315.5" cy="167" r="5"/>
<text class="t-val" x="608" y="171">40 个线程 + 内核栈</text>
<line class="dd-hair" x1="24" y1="182" x2="776" y2="182"/>
<text class="t-name t-acc" x="24" y="201">ssar procs</text>
<text class="t-note" x="176" y="201" text-anchor="end">60 秒</text>
<circle class="dd-fill-acc" cx="365.3" cy="197" r="5"/>
<text class="t-val" x="608" y="201">进程状态快照</text>
<line class="dd-hair" x1="24" y1="212" x2="776" y2="212"/>
<text class="t-name" x="24" y="231">below</text>
<text class="t-note" x="176" y="231" text-anchor="end">5 秒</text>
<circle class="dd-track" cx="197.6" cy="227" r="4.5"/>
<circle class="dd-track" cx="220.3" cy="227" r="4.5"/>
<circle class="dd-track" cx="242.9" cy="227" r="4.5"/>
<circle class="dd-track" cx="265.6" cy="227" r="4.5"/>
<circle class="dd-track" cx="288.3" cy="227" r="4.5"/>
<circle class="dd-fill-acc" cx="310.9" cy="227" r="5"/>
<circle class="dd-fill-acc" cx="333.6" cy="227" r="5"/>
<circle class="dd-fill-acc" cx="356.3" cy="227" r="5"/>
<circle class="dd-fill-acc" cx="378.9" cy="227" r="5"/>
<circle class="dd-fill-acc" cx="401.6" cy="227" r="5"/>
<circle class="dd-fill-acc" cx="424.3" cy="227" r="5"/>
<circle class="dd-track" cx="446.9" cy="227" r="4.5"/>
<circle class="dd-track" cx="469.6" cy="227" r="4.5"/>
<circle class="dd-track" cx="492.3" cy="227" r="4.5"/>
<circle class="dd-track" cx="514.9" cy="227" r="4.5"/>
<circle class="dd-track" cx="537.6" cy="227" r="4.5"/>
<circle class="dd-track" cx="560.3" cy="227" r="4.5"/>
<circle class="dd-track" cx="582.9" cy="227" r="4.5"/>
<text class="t-val" x="608" y="231">16 运行 + 24 不可中断</text>
<line class="dd-hair" x1="24" y1="242" x2="776" y2="242"/>
<text class="t-name" x="24" y="261">PCP pmlogger</text>
<text class="t-note" x="176" y="261" text-anchor="end">60 秒</text>
<circle class="dd-fill-acc" cx="315.5" cy="257" r="5"/>
<text class="t-val" x="608" y="261">只有计数 19 / 24</text>
<line class="dd-hair" x1="24" y1="272" x2="776" y2="272"/>
<text class="t-name" x="24" y="291">tsar</text>
<text class="t-note" x="176" y="291" text-anchor="end">60 秒</text>
<circle class="dd-fill-acc" cx="365.3" cy="287" r="5"/>
<text class="t-val" x="608" y="291">runq 16，load1 8.88</text>
<line class="dd-hair" x1="24" y1="302" x2="776" y2="302"/>
<text class="t-name" x="24" y="321">collectl</text>
<text class="t-note" x="176" y="321" text-anchor="end">1 秒 / 进程 60 秒</text>
<line class="dd-hair" x1="184.0" y1="311" x2="184.0" y2="323"/>
<line class="dd-hair" x1="188.5" y1="311" x2="188.5" y2="323"/>
<line class="dd-hair" x1="193.1" y1="311" x2="193.1" y2="323"/>
<line class="dd-hair" x1="197.6" y1="311" x2="197.6" y2="323"/>
<line class="dd-hair" x1="202.1" y1="311" x2="202.1" y2="323"/>
<line class="dd-hair" x1="206.7" y1="311" x2="206.7" y2="323"/>
<line class="dd-hair" x1="211.2" y1="311" x2="211.2" y2="323"/>
<line class="dd-hair" x1="215.7" y1="311" x2="215.7" y2="323"/>
<line class="dd-hair" x1="220.3" y1="311" x2="220.3" y2="323"/>
<line class="dd-hair" x1="224.8" y1="311" x2="224.8" y2="323"/>
<line class="dd-hair" x1="229.3" y1="311" x2="229.3" y2="323"/>
<line class="dd-hair" x1="233.9" y1="311" x2="233.9" y2="323"/>
<line class="dd-hair" x1="238.4" y1="311" x2="238.4" y2="323"/>
<line class="dd-hair" x1="242.9" y1="311" x2="242.9" y2="323"/>
<line class="dd-hair" x1="247.5" y1="311" x2="247.5" y2="323"/>
<line class="dd-hair" x1="252.0" y1="311" x2="252.0" y2="323"/>
<line class="dd-hair" x1="256.5" y1="311" x2="256.5" y2="323"/>
<line class="dd-hair" x1="261.1" y1="311" x2="261.1" y2="323"/>
<line class="dd-hair" x1="265.6" y1="311" x2="265.6" y2="323"/>
<line class="dd-hair" x1="270.1" y1="311" x2="270.1" y2="323"/>
<line class="dd-hair" x1="274.7" y1="311" x2="274.7" y2="323"/>
<line class="dd-hair" x1="279.2" y1="311" x2="279.2" y2="323"/>
<line class="dd-hair" x1="283.7" y1="311" x2="283.7" y2="323"/>
<line class="dd-hair" x1="288.3" y1="311" x2="288.3" y2="323"/>
<line class="dd-hair" x1="292.8" y1="311" x2="292.8" y2="323"/>
<line class="dd-hair" x1="297.3" y1="311" x2="297.3" y2="323"/>
<line class="dd-hair" x1="301.9" y1="311" x2="301.9" y2="323"/>
<line class="dd-hair" x1="306.4" y1="311" x2="306.4" y2="323"/>
<line class="dd-hair" x1="310.9" y1="311" x2="310.9" y2="323"/>
<line class="dd-hair" x1="315.5" y1="311" x2="315.5" y2="323"/>
<line class="dd-hair" x1="320.0" y1="311" x2="320.0" y2="323"/>
<line class="dd-hair" x1="324.5" y1="311" x2="324.5" y2="323"/>
<line class="dd-hair" x1="329.1" y1="311" x2="329.1" y2="323"/>
<line class="dd-hair" x1="333.6" y1="311" x2="333.6" y2="323"/>
<line class="dd-hair" x1="338.1" y1="311" x2="338.1" y2="323"/>
<line class="dd-hair" x1="342.7" y1="311" x2="342.7" y2="323"/>
<line class="dd-hair" x1="347.2" y1="311" x2="347.2" y2="323"/>
<line class="dd-hair" x1="351.7" y1="311" x2="351.7" y2="323"/>
<line class="dd-hair" x1="356.3" y1="311" x2="356.3" y2="323"/>
<line class="dd-hair" x1="360.8" y1="311" x2="360.8" y2="323"/>
<line class="dd-hair" x1="365.3" y1="311" x2="365.3" y2="323"/>
<line class="dd-hair" x1="369.9" y1="311" x2="369.9" y2="323"/>
<line class="dd-hair" x1="374.4" y1="311" x2="374.4" y2="323"/>
<line class="dd-hair" x1="378.9" y1="311" x2="378.9" y2="323"/>
<line class="dd-hair" x1="383.5" y1="311" x2="383.5" y2="323"/>
<line class="dd-hair" x1="388.0" y1="311" x2="388.0" y2="323"/>
<line class="dd-hair" x1="392.5" y1="311" x2="392.5" y2="323"/>
<line class="dd-hair" x1="397.1" y1="311" x2="397.1" y2="323"/>
<line class="dd-hair" x1="401.6" y1="311" x2="401.6" y2="323"/>
<line class="dd-hair" x1="406.1" y1="311" x2="406.1" y2="323"/>
<line class="dd-hair" x1="410.7" y1="311" x2="410.7" y2="323"/>
<line class="dd-hair" x1="415.2" y1="311" x2="415.2" y2="323"/>
<line class="dd-hair" x1="419.7" y1="311" x2="419.7" y2="323"/>
<line class="dd-hair" x1="424.3" y1="311" x2="424.3" y2="323"/>
<line class="dd-hair" x1="428.8" y1="311" x2="428.8" y2="323"/>
<line class="dd-hair" x1="433.3" y1="311" x2="433.3" y2="323"/>
<line class="dd-hair" x1="437.9" y1="311" x2="437.9" y2="323"/>
<line class="dd-hair" x1="442.4" y1="311" x2="442.4" y2="323"/>
<line class="dd-hair" x1="446.9" y1="311" x2="446.9" y2="323"/>
<line class="dd-hair" x1="451.5" y1="311" x2="451.5" y2="323"/>
<line class="dd-hair" x1="456.0" y1="311" x2="456.0" y2="323"/>
<line class="dd-hair" x1="460.5" y1="311" x2="460.5" y2="323"/>
<line class="dd-hair" x1="465.1" y1="311" x2="465.1" y2="323"/>
<line class="dd-hair" x1="469.6" y1="311" x2="469.6" y2="323"/>
<line class="dd-hair" x1="474.1" y1="311" x2="474.1" y2="323"/>
<line class="dd-hair" x1="478.7" y1="311" x2="478.7" y2="323"/>
<line class="dd-hair" x1="483.2" y1="311" x2="483.2" y2="323"/>
<line class="dd-hair" x1="487.7" y1="311" x2="487.7" y2="323"/>
<line class="dd-hair" x1="492.3" y1="311" x2="492.3" y2="323"/>
<line class="dd-hair" x1="496.8" y1="311" x2="496.8" y2="323"/>
<line class="dd-hair" x1="501.3" y1="311" x2="501.3" y2="323"/>
<line class="dd-hair" x1="505.9" y1="311" x2="505.9" y2="323"/>
<line class="dd-hair" x1="510.4" y1="311" x2="510.4" y2="323"/>
<line class="dd-hair" x1="514.9" y1="311" x2="514.9" y2="323"/>
<line class="dd-hair" x1="519.5" y1="311" x2="519.5" y2="323"/>
<line class="dd-hair" x1="524.0" y1="311" x2="524.0" y2="323"/>
<line class="dd-hair" x1="528.5" y1="311" x2="528.5" y2="323"/>
<line class="dd-hair" x1="533.1" y1="311" x2="533.1" y2="323"/>
<line class="dd-hair" x1="537.6" y1="311" x2="537.6" y2="323"/>
<line class="dd-hair" x1="542.1" y1="311" x2="542.1" y2="323"/>
<line class="dd-hair" x1="546.7" y1="311" x2="546.7" y2="323"/>
<line class="dd-hair" x1="551.2" y1="311" x2="551.2" y2="323"/>
<line class="dd-hair" x1="555.7" y1="311" x2="555.7" y2="323"/>
<line class="dd-hair" x1="560.3" y1="311" x2="560.3" y2="323"/>
<line class="dd-hair" x1="564.8" y1="311" x2="564.8" y2="323"/>
<line class="dd-hair" x1="569.3" y1="311" x2="569.3" y2="323"/>
<line class="dd-hair" x1="573.9" y1="311" x2="573.9" y2="323"/>
<line class="dd-hair" x1="578.4" y1="311" x2="578.4" y2="323"/>
<line class="dd-hair" x1="582.9" y1="311" x2="582.9" y2="323"/>
<line class="dd-hair" x1="587.5" y1="311" x2="587.5" y2="323"/>
<line class="dd-hair" x1="592.0" y1="311" x2="592.0" y2="323"/>
<circle class="dd-track" cx="261.1" cy="317" r="4.5"/>
<circle class="dd-track" cx="533.1" cy="317" r="4.5"/>
<text class="t-val" x="608" y="321">RunQ 16，BlkT 0</text>
<line class="dd-hair" x1="24" y1="332" x2="776" y2="332"/>
<text class="t-name" x="24" y="351">atop</text>
<text class="t-note" x="176" y="351" text-anchor="end">60 / 600 秒</text>
<line class="dd-edge" x1="256.5" y1="347" x2="528.5" y2="347"/>
<circle class="dd-track" cx="256.5" cy="347" r="4.5"/>
<circle class="dd-track" cx="528.5" cy="347" r="4.5"/>
<text class="t-val" x="608" y="351">各进程累计 CPU</text>
<line class="dd-hair" x1="24" y1="362" x2="776" y2="362"/>
<text class="t-name" x="24" y="381">sar</text>
<text class="t-note" x="176" y="381" text-anchor="end">60 秒</text>
<circle class="dd-track" cx="256.5" cy="377" r="4.5"/>
<circle class="dd-track" cx="528.5" cy="377" r="4.5"/>
<text class="t-val" x="608" y="381">load1 11.37，runq 0</text>
<line class="dd-hair" x1="24" y1="392" x2="776" y2="392"/>
<text class="t-name" x="24" y="411">sar · nmon</text>
<text class="t-note" x="176" y="411" text-anchor="end">600 / 300 秒</text>
<text class="t-val" x="608" y="411">没有记录</text>
<line class="dd-edge" x1="184" y1="428" x2="592" y2="428"/>
<line class="dd-edge" x1="229.3" y1="424" x2="229.3" y2="432"/><text class="t-note" x="229.3" y="448" text-anchor="middle">22:31:30</text>
<line class="dd-edge" x1="297.3" y1="424" x2="297.3" y2="432"/><text class="t-note" x="297.3" y="448" text-anchor="middle">22:31:45</text>
<line class="dd-edge" x1="365.3" y1="424" x2="365.3" y2="432"/><text class="t-note" x="365.3" y="448" text-anchor="middle">22:32:00</text>
<line class="dd-edge" x1="433.3" y1="424" x2="433.3" y2="432"/><text class="t-note" x="433.3" y="448" text-anchor="middle">22:32:15</text>
<line class="dd-edge" x1="501.3" y1="424" x2="501.3" y2="432"/><text class="t-note" x="501.3" y="448" text-anchor="middle">22:32:30</text>
<line class="dd-edge" x1="569.3" y1="424" x2="569.3" y2="432"/><text class="t-note" x="569.3" y="448" text-anchor="middle">22:32:45</text>
<circle class="dd-fill-acc" cx="30" cy="464" r="5"/><text class="t-note" x="42" y="468">采样落在尖峰里</text>
<circle class="dd-track" cx="170" cy="464" r="4.5"/><text class="t-note" x="182" y="468">采样落在尖峰外</text>
</svg>
</div>
<figcaption>图 2 · 同一个 30 秒尖峰落在各工具的采样点上：只有 5 秒一采的 SSAR load5s 和 below 稳稳碰上，其余要么靠运气，要么只留下区间累计值</figcaption>
</figure>

能稳稳抓住这 30 秒的只有 5 秒一采的两个：SSAR 的 load5s 和 below。PCP、tsar 和 SSAR 的进程快照是碰巧有一个点落在了里面；atop 的点虽然在外面，但它记的是整个区间里每个进程的累计值，已经退出的进程也会以 `<stress-ng-cp>` 这样带尖括号的名字列出来[^atop]，所以照样能看到 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 的任务[^kernel-procstat]。我们的 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 状态的线程数，然后用它去更新三个指数衰减平均[^kernel-loadavg]：

$$\text{load}_{new} = \text{load}_{old}\cdot\frac{1884}{2048} + \text{active}\cdot\frac{164}{2048}$$

这里 1884/2048 ≈ 0.92，约等于 $e^{-5/60}$。每次更新，旧值保留 92%，新数进来 8%。把 D 状态也算进 load 是 Linux 特有的做法，本意是让“等磁盘”的压力也能体现出来[^gregg-load]。

于是 load1 就像一辆车的车速，active 是油门。油门一下踩到 40，车速要好几分钟才能追上；油门松开，车速也是慢慢降下来。我们这次的 30 秒尖峰里，load1 只爬到 15.77；尖峰结束 20 秒后 sar 读到 11.37，此时真实的活跃线程已经回到 1 个左右：

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ssl1-title ssl1-desc">
<title id="ssl1-title">同一个尖峰里 load1 和 load5s 的走势</title>
<desc id="ssl1-desc">横轴 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。</desc>
<rect class="dd-frame" width="800" height="400" rx="10"/>
<text class="t-zone" x="24" y="40">ssar load5s 记录的 5 秒一行（4 核机器）</text>
<rect class="dd-derived" x="121.7" y="80" width="99.4" height="232" rx="4"/>
<text class="t-note" x="125.7" y="74">尖峰 22:31:45–22:32:15</text>
<line class="dd-hair" x1="72" y1="312.0" x2="768" y2="312.0"/>
<text class="t-note" x="64" y="316.0" text-anchor="end">0</text>
<line class="dd-hair" x1="72" y1="256.0" x2="768" y2="256.0"/>
<text class="t-note" x="64" y="260.0" text-anchor="end">10</text>
<line class="dd-hair" x1="72" y1="200.0" x2="768" y2="200.0"/>
<text class="t-note" x="64" y="204.0" text-anchor="end">20</text>
<line class="dd-hair" x1="72" y1="144.0" x2="768" y2="144.0"/>
<text class="t-note" x="64" y="148.0" text-anchor="end">30</text>
<line class="dd-hair" x1="72" y1="88.0" x2="768" y2="88.0"/>
<text class="t-note" x="64" y="92.0" text-anchor="end">40</text>
<text class="t-note" x="72.0" y="332" text-anchor="middle">22:31:30</text>
<text class="t-note" x="171.4" y="332" text-anchor="middle">22:32</text>
<text class="t-note" x="270.9" y="332" text-anchor="middle">22:32:30</text>
<text class="t-note" x="370.3" y="332" text-anchor="middle">22:33</text>
<text class="t-note" x="469.7" y="332" text-anchor="middle">22:33:30</text>
<text class="t-note" x="569.1" y="332" text-anchor="middle">22:34</text>
<text class="t-note" x="668.6" y="332" text-anchor="middle">22:34:30</text>
<text class="t-note" x="768.0" y="332" text-anchor="middle">22:35</text>
<rect class="dd-fill-acc" x="113.4" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="130.0" y="88.0" width="10" height="224.0"/>
<rect class="dd-fill-acc" x="146.5" y="88.0" width="10" height="224.0"/>
<rect class="dd-fill-acc" x="166.4" y="88.0" width="10" height="224.0"/>
<rect class="dd-fill-acc" x="183.0" y="88.0" width="10" height="224.0"/>
<rect class="dd-fill-acc" x="199.6" y="88.0" width="10" height="224.0"/>
<rect class="dd-fill-acc" x="216.1" y="88.0" width="10" height="224.0"/>
<rect class="dd-fill-acc" x="232.7" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="249.3" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="265.9" y="306.4" width="10" height="5.6"/>
<rect class="dd-fill-acc" x="282.4" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="299.0" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="315.6" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="332.1" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="348.7" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="365.3" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="381.9" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="398.4" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="415.0" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="431.6" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="448.1" y="306.4" width="10" height="5.6"/>
<rect class="dd-fill-acc" x="464.7" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="481.3" y="306.4" width="10" height="5.6"/>
<rect class="dd-fill-acc" x="497.9" y="306.4" width="10" height="5.6"/>
<rect class="dd-fill-acc" x="514.4" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="531.0" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="547.6" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="564.1" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="580.7" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="597.3" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="613.9" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="630.4" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="647.0" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="663.6" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="680.1" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="696.7" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="713.3" y="306.4" width="10" height="5.6"/>
<rect class="dd-fill-acc" x="729.9" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="746.4" y="310.5" width="10" height="1.5"/>
<rect class="dd-fill-acc" x="763.0" y="306.4" width="10" height="5.6"/>
<polyline class="dd-edge" style="stroke-width:2.2" fill="none" points="118.4,311.9 135.0,293.9 151.5,277.4 171.4,262.3 188.0,248.3 204.6,235.5 221.1,223.7 237.7,230.7 254.3,237.2 270.9,242.8 287.4,248.3 304.0,253.4 320.6,258.1 337.1,262.4 353.7,266.4 370.3,270.1 386.9,273.4 403.4,276.5 420.0,279.4 436.6,282.0 453.1,283.9 469.7,286.2 486.3,287.8 502.9,289.3 519.4,291.1 536.0,292.8 552.6,294.3 569.1,295.8 585.7,297.0 602.3,298.2 618.9,299.3 635.4,300.4 652.0,301.3 668.6,302.1 685.1,302.9 701.7,303.7 718.3,303.9 734.9,304.6 751.4,305.1 768.0,305.2"/>
<circle class="dd-dot" cx="118.4" cy="311.9" r="2.6"/>
<circle class="dd-dot" cx="135.0" cy="293.9" r="2.6"/>
<circle class="dd-dot" cx="151.5" cy="277.4" r="2.6"/>
<circle class="dd-dot" cx="171.4" cy="262.3" r="2.6"/>
<circle class="dd-dot" cx="188.0" cy="248.3" r="2.6"/>
<circle class="dd-dot" cx="204.6" cy="235.5" r="2.6"/>
<circle class="dd-dot" cx="221.1" cy="223.7" r="2.6"/>
<circle class="dd-dot" cx="237.7" cy="230.7" r="2.6"/>
<circle class="dd-dot" cx="254.3" cy="237.2" r="2.6"/>
<circle class="dd-dot" cx="270.9" cy="242.8" r="2.6"/>
<circle class="dd-dot" cx="287.4" cy="248.3" r="2.6"/>
<circle class="dd-dot" cx="304.0" cy="253.4" r="2.6"/>
<circle class="dd-dot" cx="320.6" cy="258.1" r="2.6"/>
<circle class="dd-dot" cx="337.1" cy="262.4" r="2.6"/>
<circle class="dd-dot" cx="353.7" cy="266.4" r="2.6"/>
<circle class="dd-dot" cx="370.3" cy="270.1" r="2.6"/>
<circle class="dd-dot" cx="386.9" cy="273.4" r="2.6"/>
<circle class="dd-dot" cx="403.4" cy="276.5" r="2.6"/>
<circle class="dd-dot" cx="420.0" cy="279.4" r="2.6"/>
<circle class="dd-dot" cx="436.6" cy="282.0" r="2.6"/>
<circle class="dd-dot" cx="453.1" cy="283.9" r="2.6"/>
<circle class="dd-dot" cx="469.7" cy="286.2" r="2.6"/>
<circle class="dd-dot" cx="486.3" cy="287.8" r="2.6"/>
<circle class="dd-dot" cx="502.9" cy="289.3" r="2.6"/>
<circle class="dd-dot" cx="519.4" cy="291.1" r="2.6"/>
<circle class="dd-dot" cx="536.0" cy="292.8" r="2.6"/>
<circle class="dd-dot" cx="552.6" cy="294.3" r="2.6"/>
<circle class="dd-dot" cx="569.1" cy="295.8" r="2.6"/>
<circle class="dd-dot" cx="585.7" cy="297.0" r="2.6"/>
<circle class="dd-dot" cx="602.3" cy="298.2" r="2.6"/>
<circle class="dd-dot" cx="618.9" cy="299.3" r="2.6"/>
<circle class="dd-dot" cx="635.4" cy="300.4" r="2.6"/>
<circle class="dd-dot" cx="652.0" cy="301.3" r="2.6"/>
<circle class="dd-dot" cx="668.6" cy="302.1" r="2.6"/>
<circle class="dd-dot" cx="685.1" cy="302.9" r="2.6"/>
<circle class="dd-dot" cx="701.7" cy="303.7" r="2.6"/>
<circle class="dd-dot" cx="718.3" cy="303.9" r="2.6"/>
<circle class="dd-dot" cx="734.9" cy="304.6" r="2.6"/>
<circle class="dd-dot" cx="751.4" cy="305.1" r="2.6"/>
<circle class="dd-dot" cx="768.0" cy="305.2" r="2.6"/>
<text class="t-mono t-acc" x="231.1" y="92.0">load5s = 40（16 R + 24 D）</text>
<text class="t-mono" x="231.1" y="217.7">load1 最高 15.77</text>
<text class="t-mono" x="299.4" y="240.3">← 22:32:35 尖峰已过，load1 还有 11.37</text>
<text class="t-mono" x="768.0" y="295.2" text-anchor="end">22:35:00 仍有 1.21</text>
<rect class="dd-fill-acc" x="24" y="360" width="10" height="12"/><text class="t-note" x="40" y="370">load5s：ssar 由相邻两次 load1 反推的 R+D 线程数</text>
<circle class="dd-dot" cx="440" cy="366" r="3"/><line class="dd-edge" x1="428" y1="366" x2="452" y2="366"/><text class="t-note" x="460" y="370">load1：/proc/loadavg 第一列</text>
</svg>
</div>
<figcaption>图 3 · load1 是指数平均：40 个活跃线程持续 30 秒，load1 只爬到 15.77，尖峰过后又拖了几分钟；load5s 每 5 秒给出一次真实值</figcaption>
</figure>

所以 sar 的 11.37 既不是峰值，也不是采样时的状态，而是“过去几分钟加权后的余温”。PCP 那个 0.02 更极端：22:31:49 尖峰已经开始了 4 秒，内核还没轮到下一次更新。

### 能不能把那一次 active 找回来

既然内核每 5 秒用一个精确的 active 去更新 load1，公式又是公开的，那只要拿到相邻两次 load1，就能把 active 反解出来：

$$\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，记成一行[^ssar-src]。源码里就是这一行：

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

公式里多出来的 `-10` 和 `-1024`，是在补偿内核定点数运算和 `/proc/loadavg` 只保留两位小数带来的取整偏差。拿尖峰开始后的第一次更新来算一遍：

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ssd-title ssd-desc">
<title id="ssd-title">从两次 load1 反推出 load5s</title>
<desc id="ssd-desc">左卡片：内核在 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。</desc>
<defs><marker id="ssd-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker></defs>
<rect class="dd-frame" width="800" height="300" rx="10"/>
<rect class="dd-card" x="24" y="32" width="232" height="190" rx="6"/>
<path class="dd-card-head" d="M30 32 H250 Q256 32 256 38 V64 H24 V38 Q24 32 30 32 Z"/>
<text class="t-name" x="40" y="53">内核 calc_load()</text>
<text class="t-note" x="240" y="53" text-anchor="end">每 5 秒</text>
<text class="t-mono" x="40" y="88">active = R + D</text>
<text class="t-mono t-acc" x="40" y="110">       = 16 + 24 = 40</text>
<line class="dd-hair" x1="24" y1="124" x2="256" y2="124"/>
<text class="t-val" x="40" y="148">load1 = 0.02 × 1884/2048</text>
<text class="t-val" x="40" y="170">      + 40 × 164/2048</text>
<text class="t-mono" x="40" y="198">      ≈ 3.23</text>
<rect class="dd-card" x="284" y="32" width="232" height="190" rx="6"/>
<path class="dd-card-head" d="M290 32 H510 Q516 32 516 38 V64 H284 V38 Q284 32 290 32 Z"/>
<text class="t-name" x="300" y="53">/proc/loadavg</text>
<text class="t-note" x="500" y="53" text-anchor="end">每 100 ms 读一次</text>
<text class="t-val" x="300" y="88">22:31:44  0.02 0.20 0.30</text>
<line class="dd-hair" x1="284" y1="100" x2="516" y2="100"/>
<text class="t-mono" x="300" y="124">22:31:49  <tspan class="t-acc">3.23</tspan> 0.86 0.51</text>
<line class="dd-hair" x1="284" y1="136" x2="516" y2="136"/>
<text class="t-note" x="300" y="166">第一列变了 → 内核刚更新过</text>
<text class="t-note" x="300" y="190">没变就不记，接着等</text>
<rect class="dd-card" x="544" y="32" width="232" height="190" rx="6"/>
<path class="dd-card-head" d="M550 32 H770 Q776 32 776 38 V64 H544 V38 Q544 32 550 32 Z"/>
<text class="t-name" x="560" y="53">sresar 反推 load5s</text>
<text class="t-mono" x="560" y="88">(3.23×2048 − 0.02×1884)</text>
<text class="t-mono" x="560" y="110">  ÷ 164</text>
<text class="t-mono t-acc" x="560" y="132">  = 40.1 → 40</text>
<line class="dd-hair" x1="544" y1="146" x2="776" y2="146"/>
<text class="t-note" x="560" y="170">下一次 22:31:54：</text>
<text class="t-val" x="560" y="192">(6.17×2048 − 3.23×1884)</text>
<text class="t-val" x="560" y="212">  ÷ 164 = 39.9 → 40</text>
<line class="dd-edge-acc" x1="256" y1="127" x2="282" y2="127" marker-end="url(#ssd-arrow)"/>
<line class="dd-edge-acc" x1="516" y1="127" x2="542" y2="127" marker-end="url(#ssd-arrow)"/>
<text class="t-say" x="24" y="256">1884/2048 ≈ 0.92：每次更新旧值保留 92%，新的 active 只进来 8%（164/2048）。</text>
<text class="t-say" x="24" y="280">知道了相邻两次的 load1，就能把这 8% 里的 active 解出来。</text>
</svg>
</div>
<figcaption>图 4 · 内核每次更新只让新的 active 进来 8%，拿到相邻两次 load1 就能把它解出来</figcaption>
</figure>

### 结果：5 秒一行，起止都对得上

查询端用 `ssar load5s` 读出来：

```text
$ 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 是车速”[^ssar-talk]。

它也有两个小毛病。一是精度受 `/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 秒存一份完整的进程快照，包括状态[^below]。在尖峰的 6 个采样点上，它都记下了 16 个 `RUNNING` 的 `stress-ng-cpu` 和 24 个 `UNINTERRUPTIBLE` 的 `stuck`，单就这一次实验，它和 SSAR 打了个平手。
- **SSAR** 的 `ssar procs` 每分钟存一份全部进程的快照，22:32:00 这一份正好落在尖峰里，状态里能看到 24 个 `D` 的 `stuck`。但这是运气，换个时刻发生，一分钟一次的快照就会错过。

```text
$ 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 状态。各家事后的记录是这样的：

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="sst2-title">一个进程里 24 个线程卡在 D 状态（22:45:45–22:46:15）</div>
<div class="dt-scroll">
<table class="dt" aria-labelledby="sst2-title">
<colgroup><col style="width:24%"><col><col style="width:20%"></colgroup>
<thead>
<tr><th scope="col">工具</th><th scope="col">事后读到的</th><th scope="col">能定位到线程吗</th></tr>
</thead>
<tbody>
<tr><th scope="row" class="acc">SSAR load5s</th><td>22:45:55 起每 5 秒都是 <span class="mono acc">24</span>，其中 actr 0、actd 24</td><td>—</td></tr>
<tr><th scope="row" class="acc">SSAR load2p</th><td>24 个 D 线程，全在 pid 24317，线程名 <span class="mono">pool-worker</span>，逐个列出 tid 和所在 CPU；内核栈都是 <span class="mono">__do_sys_vfork</span></td><td class="good">能，带内核栈</td></tr>
<tr><th scope="row">SSAR procs</th><td><span class="mono">javaapp</span> 状态 S，线程数 25</td><td class="bad">不能</td></tr>
<tr><th scope="row">below</th><td>5 个采样点里 <span class="mono">javaapp</span> 都是 <span class="mono">SLEEPING</span>，线程数 25；没有任何进程是 UNINTERRUPTIBLE</td><td class="bad">不能</td></tr>
<tr><th scope="row">atop</th><td><span class="mono">javaapp</span> 和退出的子进程 <span class="mono">&lt;pool-worker&gt;</span>，CPU 都是 0%</td><td class="bad">不能</td></tr>
<tr><th scope="row">PCP</th><td>22:45:49 的 <span class="mono">proc.runq.blocked</span> 是 0</td><td class="bad">不能</td></tr>
<tr><th scope="row">sar · tsar</th><td>load1 6.77 / 5.32，runq 0，blocked 0</td><td class="bad">不能</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">below 和 SSAR procs 记录的是进程，状态取自主线程；PCP 22:45:49 的采样已经在线程卡住 3 秒之后，仍然是 0，看来它也是按进程统计的（推断）。</p>
</div>
<figcaption>图 6 · 卡住的是线程而不是进程时，只有按线程扫描的 load2p 认得出来</figcaption>
</figure>

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>/stack`[^ssar-src]。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 456" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="ssa-title ssa-desc">
<title id="ssa-title">sresar 的几个线程和它写下的文件</title>
<desc id="ssa-desc">左边是常驻进程 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 现场解析这些文件。</desc>
<defs>
<marker id="ssa-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="ssa-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="456" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="376" height="424" rx="8"/>
<rect class="dd-zone" x="408" y="16" width="376" height="424" rx="8"/>
<text class="t-zone" x="32" y="44">sresar（常驻采集进程）</text>
<text class="t-zone" x="424" y="44">/var/log/sre_proc/data/2026102122/</text>
<rect class="dd-card" x="32" y="60" width="344" height="112" rx="6"/>
<path class="dd-card-head" d="M38 60 H370 Q376 60 376 66 V92 H32 V66 Q32 60 38 60 Z"/>
<text class="t-name" x="48" y="81">load1_thread</text>
<text class="t-note" x="360" y="81" text-anchor="end">每 100 ms</text>
<text class="t-val" x="48" y="115">读 /proc/loadavg，第一列变了就反推 load5s</text>
<text class="t-val" x="48" y="139">追加一行到 _load5s</text>
<text class="t-mono t-acc" x="48" y="163">load5s &gt; 4 核 × 1 且在上涨 → 叫醒</text>
<line class="dd-edge-acc" x1="204" y1="172" x2="204" y2="194" marker-end="url(#ssa-arrow-acc)"/>
<text class="t-note t-acc" x="214" y="188">eventfd</text>
<rect class="dd-card" x="32" y="196" width="344" height="112" rx="6"/>
<path class="dd-card-head" d="M38 196 H370 Q376 196 376 202 V228 H32 V202 Q32 196 38 196 Z"/>
<text class="t-name" x="48" y="217">loadrd_thread</text>
<text class="t-note" x="360" y="217" text-anchor="end">平时睡着</text>
<text class="t-val" x="48" y="251">扫 /proc/*/task/*/stat，只留 R 和 D</text>
<text class="t-val" x="48" y="275">每个线程的 tid、pid、CPU 号 → _loadrd</text>
<text class="t-val" x="48" y="299">抽 ≤100 个 D 读 /proc/&lt;tid&gt;/stack → _stack</text>
<rect class="dd-card" x="32" y="332" width="344" height="92" rx="6"/>
<path class="dd-card-head" d="M38 332 H370 Q376 332 376 338 V364 H32 V338 Q32 332 38 332 Z"/>
<text class="t-name" x="48" y="353">snapsys / sresar / cmdinfo 线程</text>
<text class="t-note" x="360" y="353" text-anchor="end">每分钟</text>
<text class="t-val" x="48" y="387">原样拷贝 stat、meminfo、vmstat、snmp…</text>
<text class="t-val" x="48" y="411">全部进程 → sresar25.gz、cmdline.gz</text>
<rect class="dd-card" x="424" y="60" width="344" height="244" rx="6"/>
<path class="dd-card-head" d="M430 60 H762 Q768 60 768 66 V92 H424 V66 Q424 60 430 60 Z"/>
<text class="t-name" x="440" y="81">一小时一个目录</text>
<text class="t-note" x="752" y="81" text-anchor="end">节选</text>
<text class="t-mono t-acc" x="440" y="113">2026102122_load5s</text>
<text class="t-note" x="752" y="113" text-anchor="end">5 秒一行</text>
<line class="dd-hair" x1="424" y1="122" x2="768" y2="122"/>
<text class="t-mono t-acc" x="440" y="139">20261021223149_loadrd</text>
<text class="t-note" x="752" y="139" text-anchor="end">触发时</text>
<line class="dd-hair" x1="424" y1="148" x2="768" y2="148"/>
<text class="t-mono t-acc" x="440" y="165">20261021223149_stack</text>
<text class="t-note" x="752" y="165" text-anchor="end">触发时</text>
<line class="dd-hair" x1="424" y1="174" x2="768" y2="174"/>
<text class="t-val" x="440" y="191">20261021223200_stat</text>
<text class="t-note" x="752" y="191" text-anchor="end">原文件</text>
<line class="dd-hair" x1="424" y1="200" x2="768" y2="200"/>
<text class="t-val" x="440" y="217">20261021223200_meminfo</text>
<text class="t-note" x="752" y="217" text-anchor="end">原文件</text>
<line class="dd-hair" x1="424" y1="226" x2="768" y2="226"/>
<text class="t-val" x="440" y="243">20261021223200_vmstat …</text>
<text class="t-note" x="752" y="243" text-anchor="end">原文件</text>
<line class="dd-hair" x1="424" y1="252" x2="768" y2="252"/>
<text class="t-val" x="440" y="269">20261021223200_sresar25.gz</text>
<text class="t-note" x="752" y="269" text-anchor="end">进程</text>
<line class="dd-hair" x1="424" y1="278" x2="768" y2="278"/>
<text class="t-val" x="440" y="295">20261021223200_cmdline.gz</text>
<line class="dd-edge" x1="596" y1="304" x2="596" y2="330" marker-end="url(#ssa-arrow)"/>
<text class="t-note" x="606" y="321">查询时才解析</text>
<rect class="dd-card" x="424" y="332" width="344" height="92" rx="6"/>
<path class="dd-card-head" d="M430 332 H762 Q768 332 768 338 V364 H424 V338 Q424 332 430 332 Z"/>
<text class="t-name" x="440" y="353">ssar（查询端）</text>
<text class="t-note" x="752" y="353" text-anchor="end">按 /etc/ssar/sys.conf</text>
<text class="t-mono" x="440" y="387">ssar load5s · load2p -c · procs</text>
<text class="t-mono" x="440" y="411">ssar -o 'cfile=stat|line_begin=…'</text>
</svg>
</div>
<figcaption>图 5 · sresar 平时只记数字和原文件，load5s 超过阈值时才叫醒另一个线程，按线程抓现场</figcaption>
</figure>

查询时用 `ssar load5s -z` 找出抓过现场的时刻，再用 `ssar load2p -c` 打开。第一次实验在 22:31:49 抓到的现场是这样的：

```text
$ 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 读写锁的那一组进程[^ssar-talk]。

`--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` 里，查询时现场解析[^ssar]。

所以只要文件采了，任何一个字段都能事后查。比如 fork 速率和 `procs_running`、`procs_blocked`，不改配置，直接在命令行里写：

```text
$ 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 秒和每天的磁盘占用：

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="sst3-title">采集端的开销（约 550 个线程的 4 核机器）</div>
<div class="dt-scroll">
<table class="dt dt-sticky" aria-labelledby="sst3-title">
<colgroup><col style="width:26%"><col><col><col></colgroup>
<thead>
<tr><th scope="col">采集端（间隔）</th><th scope="col" class="num">CPU 秒 / 小时</th><th scope="col" class="num">常驻内存</th><th scope="col" class="num">磁盘 / 天</th></tr>
</thead>
<tbody>
<tr><th scope="row" class="acc">SSAR sresar（60 秒，load 5 秒）</th><td class="num acc">4.1</td><td class="num">4.4 MB</td><td class="num acc">50 MB</td></tr>
<tr><th scope="row">below（5 秒，<span class="mono">--compress</span>）</th><td class="num bad">32.6</td><td class="num">12.7 MB</td><td class="num bad">362 MB</td></tr>
<tr><th scope="row">below（5 秒，不压缩）</th><td class="num bad">41.9</td><td class="num">15.5 MB</td><td class="num bad">4.0 GB</td></tr>
<tr><th scope="row">collectl（10 秒 / 进程 60 秒）</th><td class="num warn">7.6</td><td class="num">31 MB</td><td class="num warn">112 MB</td></tr>
<tr><th scope="row">PCP（pmcd + PMDA + pmlogger）</th><td class="num">0.5</td><td class="num">46 MB</td><td class="num">51 MB</td></tr>
<tr><th scope="row">atop（600 秒）</th><td class="num">0.2</td><td class="num">18 MB</td><td class="num">8.0 MB</td></tr>
<tr><th scope="row">atop（60 秒）</th><td class="num">1.5</td><td class="num">11 MB</td><td class="num">28 MB</td></tr>
<tr><th scope="row">tsar（cron 60 秒）</th><td class="num">1.1</td><td class="num">—</td><td class="num">1.2 MB</td></tr>
<tr><th scope="row">sar（600 秒 / 60 秒）</th><td class="num">&lt;0.1</td><td class="num">2.5 MB</td><td class="num">0.3 / 3.4 MB</td></tr>
<tr><th scope="row">nmon（300 秒）</th><td class="num">0.2</td><td class="num">2.3 MB</td><td class="num">0.4 MB</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">CPU 是采集进程及其子进程的 utime + stime 之和，窗口 22:40–23:20（40 分钟）；collectl 按服务方式的 10 秒间隔重启后测了 37 分钟；带 <span class="mono">--compress</span> 的 below（官方 systemd 单元的参数）另测了 15 分钟。tsar 每分钟由 cron 拉起一次，没有常驻进程。一小时 3600 CPU 秒是一个核跑满。</p>
</div>
<figcaption>图 7 · 记录的代价：SSAR 每小时用 4 秒 CPU、每天 50 MB，比 sar 重，但比 5 秒一采的 below 轻一个数量级</figcaption>
</figure>

几点值得说一下：

**SSAR 的开销主要花在进程上。** 每分钟把全部进程的状态和命令行压缩落盘，一天 50 MB，默认保留 7 天，也就 350 MB 左右；每小时 4 秒 CPU，大约是一个核的 0.1%。它的 README 说在万台规模上观察到的上限是单核 3%、内存 4 MB[^ssar]，和我们测到的量级一致。load5s 那个每 100 毫秒醒一次的线程几乎不花什么，读一次 `/proc/loadavg` 只是几十个字节。

**below 贵在“每 5 秒一份完整快照”。** 它不存增量，每个样本都完整存储，这正是它能稳稳抓住尖峰、回放体验好的原因[^below-blog]。但不加 `--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`，机器上有几千个线程时，开销会成比例上涨。

## 九、对照与选择

把前面的结果放进一张表：

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="sst4-title">事后排查一次 load 尖峰，各工具能回答到哪一步</div>
<div class="dt-scroll">
<table class="dt dt-sticky" aria-labelledby="sst4-title">
<colgroup><col style="width:12%"><col><col><col><col><col style="width:14%"></colgroup>
<thead>
<tr><th scope="col">工具</th><th scope="col">何时、多大</th><th scope="col">是谁</th><th scope="col">哪个线程、卡在哪</th><th scope="col">事后查新指标</th><th scope="col">cgroup</th></tr>
</thead>
<tbody>
<tr><th scope="row" class="acc">SSAR</th><td class="good">load5s，5 秒精度</td><td class="warn">每分钟进程快照</td><td class="good">load2p：按线程 + 内核栈</td><td class="good">存原文件，事后解析</td><td class="bad">不记</td></tr>
<tr><th scope="row">below</th><td class="warn">5 秒的运行 / 阻塞计数，没有 load</td><td class="good">每 5 秒进程状态</td><td class="bad">只到进程</td><td class="bad">固定的数据模型</td><td class="good">以 cgroup v2 为中心</td></tr>
<tr><th scope="row">atop</th><td class="bad">区间末尾的 load 平均</td><td class="good">区间累计，含已退出进程</td><td class="bad">不记</td><td class="bad">固定字段</td><td class="warn">进程所属 cgroup v2</td></tr>
<tr><th scope="row">PCP</th><td class="warn">60 秒的 load 平均和 R / D 计数</td><td class="warn">默认只有进程内存，可另配</td><td class="bad">不记</td><td class="warn">写 PMDA 插件，要事先配</td><td class="warn">可配</td></tr>
<tr><th scope="row">collectl</th><td class="warn">10 秒的 RunQ 和 load 平均</td><td class="warn">每分钟进程快照</td><td class="bad">不记</td><td class="bad">固定字段</td><td class="bad">不记</td></tr>
<tr><th scope="row">tsar</th><td class="warn">每分钟 load 平均和 runq</td><td class="bad">不记</td><td class="bad">不记</td><td class="warn">写 C 模块</td><td class="bad">不记</td></tr>
<tr><th scope="row">sar</th><td class="bad">10 分钟一次 load 平均</td><td class="bad">不记</td><td class="bad">不记</td><td class="bad">固定字段</td><td class="bad">不记</td></tr>
<tr><th scope="row">nmon</th><td class="bad">5 分钟一次</td><td class="warn">当时的 top 进程</td><td class="bad">不记</td><td class="bad">固定字段</td><td class="bad">不记</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">“D 状态计数”只有 PCP 和 below 的进程扫描是真的全部 D，sar、collectl、tsar 读的 <span class="mono">procs_blocked</span> 只含等块设备 IO 的任务。</p>
</div>
<figcaption>图 8 · 同一个尖峰排查到底：SSAR 在“多大、哪个线程、卡在哪”上最强，below 在“是谁”和 cgroup 上最强，atop 擅长回答谁花了资源</figcaption>
</figure>

从这张表看，没有一个工具能覆盖所有格子，选择取决于机器上最常出的是哪类问题：

**物理机、大规模同构集群，问题集中在 load 尖峰和 D 状态**，SSAR 最合适。load5s 和 load2p 是这个家族里独一份的设计，而且它在阿里内部的万台规模上跑过[^ssar]，开销实测也很低。但要接受它的几个短板：它对 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。

## 参考资料
