线上 CPU 飙高排查手册
总览:五步定位法
- 确认是 CPU 问题(而非 IO / 锁 / GC 等待)
- 定位到进程
- 定位到线程
- 定位到函数(Java 看线程栈,Go 看 pprof)
- 定位到代码行
第 1 步:确认瓶颈类型
# 先看整体负载构成
top -bn1 | head -5
# 看是用户态还是内核态高
vmstat 1 5
# 关注 r(运行队列) / b(阻塞) / us / sy / wa
解读:
| 现象 |
结论 |
| us 高,sy 低 |
应用自身计算密集 |
| sy 高 |
系统调用频繁(上下文切换、IO) |
| wa 高 |
IO 瓶颈,不是 CPU 问题 |
| r 远大于核数 |
真正在排队,需要扩核或优化 |
第 2 步:定位进程
# 按 CPU 排序,取前 10
ps -eo pid,ppid,pcpu,pmem,etime,comm --sort=-pcpu | head -11
# 持续观察某个进程
pidstat -p <pid> 1 10
pidstat 的价值在于它显示的是区间平均值,而 ps 显示的是生命周期平均值——后者会把瞬时飙高抹平。
第 3 步:定位线程
# 进程内各线程的 CPU
top -H -p <pid>
# 记录 TID,转成 16 进制(Java 线程栈需要)
printf "%x\n" <tid>
第 4 步:定位函数
Java
# 打印前 50 行线程栈
jstack <pid> > /tmp/stack.txt
# 用第 3 步得到的 16 进制 tid 去匹配(注意栈里的 nid 就是 16 进制)
grep -A 30 "nid=0x<hex-tid>" /tmp/stack.txt
连续抓 3 次(间隔 5 秒),如果同一个方法反复出现在栈顶,基本就锁定了。
Go
# 若开启了 pprof
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 交互式命令
(pprof) top10
(pprof) web # 生成火焰图
通用兜底:perf
没有语言运行时支持时,perf 是终极手段。
# 采样 30 秒
perf record -F 99 -p <pid> -g -- sleep 30
perf report --stdio | head -50
-g 采集调用链,输出会直接给出热点函数及其占比。
第 5 步:常见根因清单
排查到函数后,对照下表找模式:
| 症状 |
常见根因 |
| 用户态 CPU 持续 100% 单核 |
死循环、正则回溯爆炸(ReDoS) |
| 多核同时飙高 |
线程池打满 + 请求堆积 |
| 周期性飙高 |
定时任务、GC、日志轮转 |
| sy 特别高 |
频繁系统调用、锁竞争、网络小包 |
| 上下文切换极高 |
线程数过多(vmstat 的 cs 列) |
# 检查上下文切换
pidstat -w -p <pid> 1 5
# 检查线程总数
ls /proc/<pid>/task | wc -l
应急处置 vs 根因修复
应急(先恢复业务):
- 摘除该实例的流量,避免雪崩;
- 保留现场(
jstack、perf 数据、gcore 核心转储);
- 重启或回滚到上一版本。
根因(事后):
- 用保留的现场数据做离线分析;
- 补充该路径的压测用例;
- 增加对应的监控指标与告警。
关键提醒
排障时先保现场再重启。重启能恢复业务,但会销毁所有证据。至少先执行一次 jstack / perf record 并落盘。
小结
- 顺序不能跳:进程 → 线程 → 函数 → 代码行。
pidstat 优于 ps,因为它看的是实时区间。
- 没有运行时支持就用
perf,它几乎总能给出答案。