CPU 飙高定位与火焰图
CPU 飙高是最容易看到、也最容易被误判的故障:看到 100% 就扩容,往往只是把一个死循环复制成了三份。本章给出一条固定路线——机器 → 进程 → 线程 → 函数栈,并用火焰图把“哪个函数在烧 CPU”这件事量化。
定位路线
告警:某机器 CPU > 90%
↓ 1. 确认是不是真的高(top 看 us/sy/wa 构成)
↓ 2. 确认哪个进程(top 按 %CPU 排序)
↓ 3. 确认哪个线程(top -Hp <pid>)
↓ 4. 确认哪个函数(栈 / perf / 火焰图)
↓ 5. 处置:限流、降级、摘流量、重启前保存现场
顺序不能跳。跳过第 1 步会犯“把 IO 等待当成 CPU 忙”的错误;跳过第 3 步会在多线程进程里瞎猜。
第一步:分清用户态还是内核态
| top 表现 | 结论 | 典型原因 |
|---|---|---|
us 高 | 业务代码在算 | 死循环、复杂计算、正则回溯、序列化 |
sy 高 | 内核态消耗大 | 频繁 syscall、锁争用、内存分配回收、上下文切换 |
sy 高且 si 高 | 软中断 | 网络小包、丢包重传、网卡队列不均衡 |
hi 高 | 硬中断 | 磁盘或网卡中断过多、中断未做多队列 |
wa 高,us/sy 低 | 不是 CPU 问题 | 磁盘或网络存储慢,去查 IO |
st 高 | 不是你的问题 | 云主机宿主机超卖,联系平台方 |
us 高找代码,sy 高找 syscall 与锁——这是两条完全不同的路,先分叉再深入。
第二步:进程到线程
top -b -n 1 -o %CPU | head -20 # 找出高 CPU 进程
top -Hp <pid> -b -n 1 -o %CPU | head -25 # 找出高 CPU 线程(SPID 即 TID)
printf '%x\n' <tid> # 线程号转十六进制,用于比对线程栈
| 现象 | 含义 | 下一步 |
|---|---|---|
| 单个线程接近 100% | 单线程死循环或单线程计算热点 | 抓该线程的栈 |
| 多个线程同时接近 100% | 线程池在满负荷跑,可能是流量问题 | 看 QPS 是否同步上涨 |
| 线程数与 CPU 数接近且都满 | 真实算力不足 | 扩容 + 找可优化点 |
| 线程频繁切换、无热点线程 | 锁竞争或上下文切换过多 | pidstat -w 1、vmstat 1 看 cs |
第三步:抓栈
不同运行时抓栈方式不同,目的都是把线程号与栈对上。
| 运行环境 | 命令 | 要点 |
|---|---|---|
| Java | jstack <pid> | 搜 nid=0x<十六进制tid>,看该线程栈顶 |
| C/C++/Go 进程 | gdb -p <pid> -batch -ex "thread apply all bt" | 需要调试符号,会短暂暂停进程 |
| 任意进程 | pstack <pid> | 部分发行版需额外安装,输出可读性好 |
| 查看阻塞点 | cat /proc/<pid>/task/<tid>/stack | 看线程在内核里的等待位置 |
| syscall 层面 | strace -cp <pid> | 统计各 syscall 调用次数与耗时占比 |
# 连续抓三次栈,间隔 5 秒:三次都落在同一处的线程才是真热点
for i in 1 2 3; do
echo "===== sample $i ====="
jstack <pid> > /tmp/stack_$i.txt
sleep 5
done
grep -A 12 "nid=0x<hex-tid>" /tmp/stack_*.txt
第四步:火焰图
# 采样 30 秒,99Hz(避开 100Hz 的时钟采样谐波)
perf record -F 99 -p <pid> -g -- sleep 30
perf script > /tmp/out.perf
# 折叠栈并生成 SVG(FlameGraph 脚本)
/path/to/stackcollapse-perf.pl /tmp/out.perf > /tmp/out.folded
/path/to/flamegraph.pl /tmp/out.folded > /tmp/cpu.svg
没有 perf 权限时,也可以先用 perf top -g -p <pid> 实时看热点,或对 Java 应用使用 async-profiler 直接产出火焰图。
| 读图规则 | 含义 |
|---|---|
| 横条宽度 = 该栈在采样中的占比 | 最宽的横条就是最耗 CPU 的路径 |
| 纵轴 = 调用栈深度,从上到下是调用关系 | 顶层是被调用者,底层是入口 |
| 平顶(顶部宽而平) | 该函数本身就是热点,看函数名即可定位代码 |
| 尖顶(底部宽、顶部细) | 热点分散在多个子调用,看分支谁更宽 |
| 颜色 | 无语义,只用于区分,不要按颜色判断 |
出现 [kernel] 栈 | 属于内核态消耗,去看 syscall 与锁 |
搜索技巧:在 SVG 里按 Ctrl+F 搜函数名,命中的横条会被高亮,能直接看出该函数占了多少宽度。
常见根因与特征
| 根因 | 火焰图特征 | 处置 |
|---|---|---|
| 死循环 / 忙等待 | 单个线程 100%,栈顶固定在同一函数 | 摘流量、重启止血,然后修代码 |
| 正则回溯爆炸 | 栈顶是正则匹配函数,某接口 QPS 很低但 CPU 很高 | 换非回溯引擎、限制输入长度 |
| JSON 序列化 | 栈顶集中在序列化库,常见于大对象 | 减小对象、换更快的库、避免重复序列化 |
| 频繁 GC | us 高、栈里出现 GC 线程,jstat 显示 Full GC 频繁 | 调堆与 GC 参数,查对象泄漏 |
| 大量小 syscall | sy 高,栈顶为 read/write/stat | 批量化、加缓冲、减少系统调用 |
| 日志同步刷盘 | sy + wa 同时高,栈顶是 write/fsync | 降日志级别、改异步日志 |
| 锁竞争自旋 | sy 高、cs 暴涨、无热点函数 | 缩小锁粒度、改用无锁结构 |
| 加密/压缩算法 | 栈顶在 CPU 密集库函数,QPS 正常 | 换算法或降强度,必要时扩容 |
处置与现场保存
- 先摘流量或少部分实例,把 CPU 从 100% 降到 70%,再慢慢查,不要边查边救。
- 重启前必须保存:三份线程栈、火焰图 SVG、
top -Hp输出、当前日志尾部。 - 如果是流量型过载,限流优先于扩容;如果是代码型热点,扩容只是把总量翻倍。
- 临时缓解与长期修复分开记录,临时补丁要写明失效条件。
预防与巡检
- 核心服务常态开启 CPU 使用率与 P99 双指标告警,避免只看 CPU。
- 每次压测后固化一份火焰图基线,新版本上线后对比宽度变化可提前发现劣化。
- 生产环境保留 perf 权限或预装 async-profiler,故障时现装工具往往来不及。
- 代码评审关注循环边界、正则复杂度与大对象序列化,这三类问题最容易在生产爆发。
小结:CPU 飙高按“机器、进程、线程、函数”四步收敛,先用 us/sy 分清用户态与内核态,再用 top -Hp 找到线程并转十六进制对栈;火焰图的横条宽度就是 CPU 占比,平顶即可疑函数;处置优先级是摘流量、限流、降级,重启前一定先留下线程栈与火焰图。