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 很高。

再跑 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 列直接决定排查方向:

%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

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。

hung_task_detector 触发了:jbd2 进程阻塞超过 120 秒。内核告诉你"有任务卡住了",但 dmesg 默认被很多人忽略。
排坑:
dmesg -T的时间戳格式才是人类可读的。加不加-T的区别是:[345678.123456]vs[Mon Jul 27 02:30:15 2026]。加到 shell alias 里。
第四步:谁在 D 态 + 内核栈

# 找 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
内核栈从下往上读——被调用的顺序是自底向上:
- 应用写文件(日志/数据文件)→ 内核将 inode 标记为脏(
__mark_inode_dirty) - → ext4 启动一个事务(
ext4_journal_start_sb),准备记录这次修改 - → JBD2 尝试启动事务句柄(
jbd2_journal_start→start_this_handle) - → 卡在
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:

不包含 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 日志锁竞争

验证思路:修复不是改了参数就完事的。需要对比修复前后的指标: - 修复前: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 核。