CPU 才 30%,Load 飙到 8 你还在加机器?

场景:凌晨 2:30 告警,接口 RT 从 50ms 飙到 5s,CPU 才 30%,Load Average 冲到 8.5——值班同事重启了事,三天后同一时间再次爆发 路径:top 分诊 → vmstat 看 b/r → iostat 排除 IO → dmesg + /proc/stack 找锁 → 内核定义解读 → 4 种场景速查

上篇讲了"一核有难七核围观"的单核打满问题,这篇来看另一个更迷惑的症状——CPU 空闲但 Load Average 飙高


【症状】凌晨 2:30 的告警

第一次:重启了事

凌晨 2:30,PagerDuty 告警:订单接口 95 分位 RT 从 50ms 飙到 5 秒,错误率 0.3%。

值班同事上机器一看:CPU 30%,内存 60%,磁盘 IO 正常。没有任何明显瓶颈。"重启试试"——重启后指标恢复,写了条记录"疑似抖动"。

第二次:开始怀疑应用

三天后同一时间,同样告警。这次团队认真了:是不是 GC 问题?加上了 GC 监控。是不是数据库慢查询?DBA 跑了一遍慢查询日志。是不是依赖的服务超时?检查了所有下游。

全都正常。 重启又恢复了。

第三次:下钻系统层

又三天,凌晨 2:30,告警再次响起。这次我上去没看应用日志,直接跑了一个命令——top

%Cpu(s):  5.3 us,  8.2 sy,  0.0 ni, 60.5 id,  0.0 wa,  0.0 hi,  0.8 si, 25.2 st

Load Average 8.5,CPU idle 60%。CPU 不忙,Load 很高。

监控面板:CPU 30%、Load 8.5、RT 5s

再跑 vmstat 1

 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  3  10240 31045678 800000 9000000   0    0    50   100  500 800 10  8 60  0  0
 0  3  10240 31045000 800000 9000000   0    0    60   120  510 820 10  8 60  0  0
 0  3  10240 31044000 800000 9000000   0    0    55   110  490 780  9  7 61  0  0

b = 3,持续 3 轮。wa = 0。CPU 没有在等 IO。

这意味着不是磁盘瓶颈——是有人在等别的东西。

排坑:第一次和第二次都犯了同一个错误——只看 CPU/内存/磁盘这些"常规指标",没看进程状态。系统排查的第一步不是加监控——是看进程在等什么。


【诊察】四步定位内核锁

第一步:top 分诊——不是先跑 vmstat,是先看 CPU 7 列

很多人的习惯是看到系统慢就 vmstat 1。不对。

先跑 top -bn1 | head -5 看 CPU 分布,7 列直接决定排查方向:

top 输出,标注 us/sy/id/wa/hi/si/st 各列

%Cpu(s):  5.3 us,  8.2 sy,  0.0 ni, 60.5 id,  0.0 wa,  0.0 hi,  0.8 si, 25.2 st

逐列说明: - us (user) — 用户态 CPU。应用代码占用的 CPU 时间 - sy (system) — 系统态 CPU。内核/系统调用占用的 CPU 时间。高表示系统调用过多 - id (idle) — 完全空闲的 CPU。高说明 CPU 不是瓶颈 - wa (iowait) — CPU 等待 IO 完成的时间占比。> 30% 说明 IO 是瓶颈 - hi (hardware IRQ) — 硬件中断处理时间。高说明硬件设备(网卡/磁盘)频繁中断 - si (software IRQ) — 软中断处理时间(ksoftirqd)。> 5% 说明软中断堆积 - st (steal time) — 被虚拟机管理程序偷走的 CPU 时间。> 20% 说明宿主机超卖

top 发现 说明 下一步
wa > 30% IO 饱和 iostat -x 1
si > 5% 软中断堆积 cat /proc/softirqs
sy > 30% 系统调用/锁竞争 strace -c / perf top
st > 20% 虚拟机 CPU 争抢 cat /proc/stat \| grep steal
id > 60% 但系统慢 D 态阻塞或短时进程 vmstat 1 看 r/b

排坑:很多人看到 wa = 0 就排除了系统层问题。错。锁竞争和短时进程都不会让 wa 升高,但会让 Load 升高。top 的 7 列是分诊台——每一列都指向不同的路径。

本案例:wa ≈ 0、si ≈ 0、sy ≈ 8%、st ≈ 25%、id ≈ 60%

这里有个陷阱:st = 25.2%。直觉会说"虚拟机被偷了 CPU,加机器?"但等一下——st 高只说明宿主机超卖,不解释为什么接口 RT 飙到 5 秒。st 25% 意味着应用少拿了 25% 的 CPU 时间,但 CPU idle 还有 60%——不是 CPU 不够。

往下看。

第二步:vmstat 1——看 b 还是 r

vmstat 1 3 输出,标注 b 列和 wa 列

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  3  10240 31045678 800000 9000000   0    0    50   100  500 800 10  8 60  0  0
 0  3  10240 31045000 800000 9000000   0    0    60   120  510 820 10  8 60  0  0
 0  3  10240 31044000 800000 9000000   0    0    55   110  490 780  9  7 61  0  0

关键解读:

  • r(running/runqueue) = 0:没有进程在等 CPU。这意味着 CPU 不是瓶颈——如果有线程需要 CPU,r 会大于 0
  • b(blocked) = 3:3 个进程处于 TASK_UNINTERRUPTIBLE 状态——D 态,进程在等一个内核操作完成,且不能被信号打断
  • wa = 0:CPU 没有被 IO 等待消耗。注意:wa 高一定意味着 IO 是瓶颈,但 wa 低不代表没有 D 态进程——锁竞争也会让进程进入 D 态

排坑:vmstat 只看一次是不够的。上面跑了 3 轮(vmstat 1 3),b 列持续在 2~3,说明阻塞是持续的,不是瞬时抖动。诊断系统问题时,要看趋势而不是绝对值。

关键判断:b > 0 且 wa ≈ 0 → 这些进程进入 D 态不是因为等待磁盘 IO——是内核锁。线程在等一把锁释放,但锁的持有者也在等某个条件满足。

  • 另一种可能:b = 0 但 r > 20 且 us/sy 不高 → 短时进程风暴。大量进程在 runqueue 里排队,但每个进程只跑几毫秒就退出了,所以 CPU 看起来不忙,但 Load 很高

第三步:dmesg——内核的报错往往被忽略

在查 /proc/*/stack 之前,先看一个很多人忽略的地方——dmesg

dmesg -T 输出:jbd2 线程阻塞超过 120 秒

hung_task_detector 触发了:jbd2 进程阻塞超过 120 秒。内核告诉你"有任务卡住了",但 dmesg 默认被很多人忽略。

排坑dmesg -T 的时间戳格式才是人类可读的。加不加 -T 的区别是:[345678.123456] vs [Mon Jul 27 02:30:15 2026]。加到 shell alias 里。

第四步:谁在 D 态 + 内核栈

ps aux 过滤 D 态进程 + /proc/PID/stack 输出

# 找 D 态进程
root@api-gw-03:~# ps aux | awk '$8 ~ /D/'
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      1234  0.0  0.0  12345  6789 ?        D    14:30   0:00 [jbd2/sda1-8]
root      1235  0.0  0.0  12345  6789 ?        D    14:30   0:00 [jbd2/sda1-8]
root      1236  0.0  0.0  12345  6789 ?        D    14:30   0:00 [jbd2/sda1-8]

3 个 jbd2 线程——JBD2 是 ext4 文件系统的日志事务管理模块。

# 看内核栈
root@api-gw-03:~# cat /proc/1234/stack
[<ffffffff81234567>] wait_transaction_locked+0x87/0x100
[<ffffffff81234890>] add_transaction_credits+0x120/0x1a0
[<ffffffff81234a00>] start_this_handle+0x60/0x500
[<ffffffff81234f00>] jbd2_journal_start+0x80/0x100
[<ffffffff81123456>] ext4_journal_start_sb+0x36/0x50
[<ffffffff81115678>] ext4_dirty_inode+0x28/0x60
[<ffffffff81234500>] __mark_inode_dirty+0x40/0x300

内核栈从下往上读——被调用的顺序是自底向上:

  1. 应用写文件(日志/数据文件)→ 内核将 inode 标记为脏(__mark_inode_dirty
  2. → ext4 启动一个事务(ext4_journal_start_sb),准备记录这次修改
  3. → JBD2 尝试启动事务句柄(jbd2_journal_startstart_this_handle
  4. → 卡在 wait_transaction_locked——等待上一个事务提交完成并释放锁

三条命令互相印证:

命令 发现 含义
top id=60%, us=10%, sy=8%, wa=0 CPU 空闲,但系统不响应
vmstat 1 b=3, r=0, wa=0 有进程在 D 态,但不是 IO
dmesg task jbd2 blocked >120s 内核自身报了这个进程超时
/proc/*/stack wait_transaction_locked 三个 jbd2 线程等同一把日志锁

根因:某批写操作一次性产生了大量日志事务,JBD2 的日志提交线程来不及处理,后续事务全部排队等锁。这不是磁盘慢——是日志事务的产生速度超过了提交速度。CPU 空闲是因为线程根本没在跑——它们在等一把内核锁。


【路径】🔍 下次遇到直接跑

三段式排查命令组合截图

为什么 pidstat 抓不到短时进程

短时进程从创建到退出可能只有几毫秒,pidstat 的采样间隔是 1 秒,采样窗口几乎一定错过。需要用 perf sched 的调度事件追踪,或者 bcc 的 execsnoop 追踪 exec 系统调用:

# 短时进程专用方案(pidstat 抓不到时)
perf sched record -g -- sleep 5
perf sched latency
# 或者
execsnoop-bpfcc          # bcc 工具包,追踪 exec 事件

【定位】系统层原理——CPU 空闲但 load 高的 4 种路径

Load Average 的内核定义

很多人以为 Load Average 是"CPU 的使用程度"。这个理解是错的。

打开内核源码 kernel/sched/loadavg.c

内核 loadavg.c 源码:load = TASK_RUNNING + TASK_UNINTERRUPTIBLE

不包含 TASK_INTERRUPTIBLE(S 态)。这意味着: - 网络 IO 等待(如 epoll_wait)是 S 态——不影响 Load - 磁盘 IO 是 D 态——影响 Load,且会让 wa 升高 - 内核锁竞争是 D 态——影响 Load,但 wa 不会升高(!) - 短时进程排队是 R 态——影响 Load,但 CPU 占用低

四种路径对比

四种场景的路径对比图,内核路径引导

CPU idle 时 load 高的 4 种路径:

① IO 饱和 (wa > 30%, b > 0)
  应用 syscall → 磁盘 IO → IO 调度器排队 → TASK_UNINTERRUPTIBLE → load ↑
  → iostat %util > 80%, await > 100ms
  → c/s 列显示每秒上下文切换显著升高
  → 修复:换 SSD / 调整 IO 调度器 / 增加 IO 队列深度

② 内核锁竞争 (wa ≈ 0, b > 0) 【本文案例】
  应用写文件 → ext4 标记脏 inode → JBD2 事务锁等待 → TASK_UNINTERRUPTIBLE → load ↑
  → top 中 id 高、wa 接近 0、b 持续 > 0
  → dmesg 可能出现 "task blocked for more than 120 seconds"
  → 修复:减少 fsync 频率 / 调大 journal 缓冲区 / 减少并发写

③ 短时进程风暴 (wa ≈ 0, b ≈ 0, r 高)
  进程不断 fork/exit → R 态排队 → load ↑
  → pidstat 看不到,perf sched 才能发现
  → /proc/loadavg 的 r 列(当前可运行进程数)异常高
  → 修复:改用线程池 / 调大 max_orphaned / 减少进程创建频率

④ 虚拟机 steal time (st > 20%, b ≈ 0)
  宿主机超卖 → hypervisor 不给虚拟机调度 CPU → st 列高
  → top 的 st > 20%,同时 id 可能仍然高
  → /proc/stat 的 steal 字段持续增长
  → 修复:迁移到空闲宿主机 / 申请独享实例 / 限流降级

为什么 st 高也会让 Load 高

路径④需要特别说明:st(steal time)本身不直接让 Load 升高(steal 不涉及 D 态进程),但虚拟机 CPU 被偷走时,应用线程执行变慢,导致请求积压。请求积压 → 更多的并发线程 → 更多的线程上下文切换 → 系统态 CPU 升高。最终可能触发连锁反应——线程池满、任务队列满、应用层 RT 飙升。

这就是为什么"加机器"只在路径④有用,在其他三条路径上毫无意义。

这个问题的归属:不是应用层问题——是系统层的资源调度/锁机制出现了瓶颈。每次重启之所以有用,是因为重启重置了 JBD2 的事务状态,积压的事务被强行提交了。但只要触发条件(高并发写 + 大数据量)再次出现,锁竞争就会重新发生。


【处方】修复 + 验证 + 决策逻辑

场景 B 修复:JBD2 日志锁竞争

JBD2 日志锁修复命令 + 决策逻辑

验证思路:修复不是改了参数就完事的。需要对比修复前后的指标: - 修复前:b 列持续 2-3,dmesg 有 blocked 日志,接口 RT > 5s - 修复后:b 列长时间为 0,dmesg 没有新 blocked 日志,RT 回到 50ms

场景 C 修复:短时进程风暴

# 方案 1:改用线程池(推荐,收益最高)
# 用固定大小的线程池替代每次 fork 子进程
# 为什么:进程创建/销毁的代价远超线程,D 态排队时间更长

# 方案 2:增大系统容忍度(治标不治本)
sysctl -w kernel.pid_max=65536

  为什么:默认 32768,短时进程多时容易触顶
  验证:cat /proc/sys/kernel/pid_max

# 方案 3:限制单个用户最大进程数
# /etc/security/limits.conf:
# username soft nproc 4096
# username hard nproc 8192

  为什么:防止进程泄露拖垮整个系统
  适用场景:共享机器上运行多个业务

场景 D 修复:虚拟机 steal time

# 方案 1:迁移到空闲宿主机(云上操作)
# 在云控制台操作或联系技术支持

  为什么:st 高说明宿主机 CPU 超卖,换一台不超卖的就解决了
  验证前:top 的 st > 20%,且 /proc/stat steal 不断增长
  验证后:st 降到 5% 以下

# 方案 2:申请独享实例(代价高但稳定)
# 云厂商的独享实例(如 AWS Dedicated Host)

  适用场景:对稳定性要求极高的核心业务

# 方案 3:应用层做限流降级(兜底方案)
# 检测 st 超过阈值时自动降级非核心功能

  为什么:st 问题不一定能立即解决,应用层限流至少保住核心请求

速查卡

CPU idle + load 高 → top 看 CPU 分布
├── wa > 30%  → IO 饱和       → iostat -x 1
├── si > 5%   → 软中断堆积     → cat /proc/softirqs
├── sy > 30%  → 系统调用/锁竞争 → strace / perf top
├── st > 20%  → 虚拟机超卖     → cat /proc/stat | grep steal
└── id > 60%  → 看 vmstat b/r
    ├── b > 0 + wa ≈ 0  → 内核锁  → /proc/*/stack + dmesg
    └── r > 20 + us/sy 不高 → 短时进程 → perf sched / execsnoop

内核版本说明:本文基于 Linux 5.15.0。/proc/*/stack 在不同内核版本中保持一致。hung_task_detector 自 2.6 起存在。cgroup v1/v2 的 /proc/sys 路径有差异。JBD2 行为在 ext4 中前后兼容。

CPU 不忙的时候,系统不一定在休息——可能是所有线程都在等一把锁。


🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:Ai拆代码的曹操

下篇我们聊 CPU 绑定(taskset/numactl)没配好导致的性能下降——为什么你给了 8 核,Java 进程只用了 2 核。