2331 字
约 7 分钟
16
S05-A15 · CPU 100% 的四种成因:从现象到区分
2024-12-31
2026-11-10

一次报警:某实例 CPU 打满。值班的人重启了进程,恢复。第二天同一时间又打满。第三次终于有人去看火焰图,发现是定时任务在同一分钟内启动了三个线程池的预热——CPU 打满不是"算得太慢",而是四个不同问题的共同表现,而它们的分辨方法完全不同。

还有一种被误当成"第五种"的情形:CPU 限额与配额。容器里 cpu.max 限流不会让 CPU 打满,而是让线程拿不到时间片——表现为延迟升高、CPU 看起来不高,与这四种正好相反,所以排障时先要确认"是不是真的打满"。

四种成因:真实计算密集、锁竞争与自旋、GC 消耗、系统调用与内核态。区分它们只需要看两个数:用户态与内核态的比例,以及GC 占用的时间占比。


1. 四种成因的指纹

成因 指纹
计算密集 用户态高、GC 正常、单线程饱和
锁竞争/自旋 用户态高、上下文切换高、线程数多
GC 消耗 GC 时间占比高、内存分配速率高
系统调用/内核 内核态高、syscall 次数多

这里有个容易走错的分支:看到 CPU 高就先去查代码效率,而实际上代码效率问题通常表现为"CPU 高但请求量不高",锁竞争则表现为"请求量一上来 CPU 就满"。先用请求量做一次分流,能省掉一半时间。

第一项与第二项在 top 上看起来一样(都是用户态 100%),差别在上下文切换(cs)与线程状态:锁竞争会让大量线程处于 runnable 而拿不到锁。

第三项的特征是"CPU 高但业务吞吐不高":GC 线程在消耗 CPU,而应用线程在等 GC。

// 一次看全四项指纹
top -H -b -n1 | head -30                      # 线程级占用
pidstat -u -w -t -p $(pgrep -f app.jar) 1 3   # 用户/内核 + 上下文切换
jstat -gcutil $(pgrep -f app.jar) 1000 3      # GC 占比
pidstat -d -p $(pgrep -f app.jar) 1 3         # IO 与内核态关联

2. 锁竞争的识别

现象 说明
大量线程处于 runnable 抢锁而非阻塞
cs 每秒数万次 自旋或唤醒频繁
单线程占用异常高 某热点锁的持有者
吞吐随并发上升而下降 典型的争用曲线

第四项是最有诊断力的:加并发应该提吞吐,如果反而下降,几乎一定是争用。而它在监控上表现为"CPU 上升、QPS 下降"。

与自旋相邻的一种是被"忙等"掩盖的问题:轮询式重试(while (!ok) { } 或高频重试循环)会让 CPU 高得毫无意义,而在栈里看起来只是在循环里调用一个方法,不像 bug。

自旋锁(如 ConcurrentHashMap 的短自旋)在争用激烈时会把 CPU 白白烧掉,而不是阻塞——所以"没有阻塞"不等于"没有争用"。

// 抓三次线程栈,看是否集中在同一把锁
for i in 1 2 3; do jstack $(pgrep -f app.jar) | grep -A3 'BLOCKED\|WAITING' | head -20; sleep 2; done
// 同一栈帧反复出现 → 该处是热点

3. GC 消耗的识别

指标 阈值
GC 时间占比 > 10% 需关注
分配速率 与堆大小不匹配
Full GC 频率 任何非零都需解释
停顿与 CPU 峰值是否同步 同步则 GC 是主因

第四项是最直接的因果验证:CPU 峰值与 GC 停顿在时间上重合,说明 CPU 花在回收上。不重合则要往别处找。

第一项的计算方式很简单:一分钟内 GC 总耗时 / 60 秒。若超过 10%,说明应用线程可用 CPU 已被显著挤占。

// GC 占比:从统一日志里算
grep -oE 'Pause.*[0-9]+\.[0-9]+ms' gc.log | awk -F'ms' '{s+=$1} END {print "total_ms", s}'
// 与运行时长对比即可得占比

4. 内核态的识别

现象 说明
内核态 > 30% 系统调用密集
sys 高但 us 低 纯 IO 或网络
大量小 read/write 无缓冲的 IO
网络中断集中在少数核 网卡队列不均

第三项常见于"忘了缓冲"的代码:逐行读文件、逐条写 socket,每次都是系统调用。修复方式通常是加缓冲,而不是加机器。

内核态高还有一个"看不到"的来源:软中断处理集中在少数核。网络流量大时,ksoftirqd 会把某些核占满,而业务线程看起来并不忙——这时优化方向是网卡队列与中断亲和性。

第四项在容器里表现为"某些核 100%、其余空闲",用 mpstat -P ALL 1 能立刻看出来。

5. 区分流程:四步收敛

步骤 动作
1 看 us/sy 比例
2 看 GC 占比
3 看线程状态分布
4 抓栈与火焰图

第四步(抓栈与火焰图)有一个实践细节:单次抓栈没有意义,要抓三次以上。因为一次抓到的可能是一个瞬时状态,而三次里重复出现的栈帧才是热点。

第三步是分流点:大量 runnable → 争用;少数线程高占用 → 计算密集;多数线程 WAITING → 等外部。多数线程在等外部时,CPU 高通常另有原因(GC 或后台任务)。

// 四步收敛的判定表
us 高、sy 低、GC < 5%、单线程饱和  → 计算密集(优化算法或加实例)
us 高、sy 低、GC < 5%、多线程 runnable → 锁竞争(拆锁或换结构)
us 高、sy 低、GC > 10%              → GC(调堆或查分配)
sy 高(>30%)                       → 系统调用/IO(加缓冲或改批处理)

排障时还应当区分"瞬时"与"持续":瞬时尖峰(秒级)通常由批量操作触发,持续高位(分钟级以上)才指向结构性问题。而监控通常按分钟聚合,把两者混在一起,于是"尖峰"被读成"持续"。

// 采样粒度决定结论
1 分钟均值:尖峰被平均掉 → 看起来正常(漏报)
5 秒采样:能看到峰值 → 但噪声大
1 秒采样 + P99:既能看峰值又能排除噪声
// 建议:CPU 用 1 秒采样,看 P99 与最大值两个数

6. 排错:五种"CPU 高但查不出原因"

现象 成因 处理方向
重启就好,过几天复发 定时任务或周期性任务 查调度时间
单核打满其余空闲 单线程热点或中断集中 火焰图看那一核
容器内 CPU 高但宿主正常 限额与配额 查 cgroup 限制
业务停了 CPU 还高 后台线程或 GC 看线程清单
加机器后总 CPU 不降 瓶颈在共享资源 查下游与锁

第三项在容器里很常见:cpu.max 限流会造成"看起来没满但拿不到 CPU",表现为延迟升高而非 CPU 100%——与本题相反,但常被混淆。

7. 边界:什么情况 CPU 高是正常的

场景 为什么 做法
批处理窗口 设计如此 只看窗口内其他指标
预热与编译 短时高 忽略
加密与压缩 计算密集 接受或换算法
压测环境 目标就是打满 忽略

最后一条与"容量"有关的边界:周期性的 CPU 尖峰不该被当成故障。定时任务、缓存刷新、日志切割都会造成尖峰;判据是"尖峰是否与业务量相关"——相关是容量问题,不相关是调度问题。

判断口径一句话:这段时间的 CPU 是否换来了等价的产出。换来了(吞吐同步上升),说明它是被用满而非被浪费;没换来,就是四种成因之一。


8. 自测

  1. 四种成因的指纹分别是什么?为什么"用户态 100%"无法区分前两种?
  2. 锁竞争为什么表现为 CPU 高而不是阻塞?cs 与线程状态在其中起什么作用?
  3. 如何用时间重合验证"GC 是 CPU 峰值的主因"?GC 占比怎么算?
  4. 内核态 >30% 最常见的代码原因是什么?为什么修复方式是加缓冲而不是加机器?
  5. 四步收敛流程里,哪一步是分流点?"多数线程 WAITING"说明什么?
  6. 判断"CPU 高是否正常"的那个问题是什么?
S05-A15 · CPU 100% 的四种成因:从现象到区分
https://www.clxhxhhr.top/posts/6471/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。