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 1vmstat 1cs

第三步:抓栈

不同运行时抓栈方式不同,目的都是把线程号与栈对上。

运行环境命令要点
Javajstack <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 序列化栈顶集中在序列化库,常见于大对象减小对象、换更快的库、避免重复序列化
频繁 GCus 高、栈里出现 GC 线程,jstat 显示 Full GC 频繁调堆与 GC 参数,查对象泄漏
大量小 syscallsy 高,栈顶为 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 占比,平顶即可疑函数;处置优先级是摘流量、限流、降级,重启前一定先留下线程栈与火焰图。

笔记加载中…