生产环境排查 CPU 飙高的定位路径
白猫连帽
6 min read
生产环境排查 CPU 飙高的定位路径
CPU 飙高不是“重启就能解决”的现象,它背后一定有持续占用计算资源的工作:死循环、频繁 GC、锁竞争、无效重试,或纯粹是流量超出容量。定位路径应当是“先看整体占用,再下钻到线程,最终对照代码逻辑”,而不是上来就猜。每次盲猜重启,都等于丢掉一次定位根因的机会,问题迟早以更大代价回来。真正的线上能力,体现在能在不重启的前提下把热点定位到具体函数。
第一步 确认是不是真的 CPU 问题
top -H -p $(pgrep -f app) # 看进程内各线程的 CPU 占比
先排除内存换页、IO 等待伪装成的“系统慢”。load average 远高于核数、且 %us 占主导,才是用户态 CPU 真高;若是 %sy 高,多半在内核态频繁系统调用或上下文切换;若是 wa 高,其实是 IO 在拖,不是 CPU 本身。分清这三档,方向才不会错,否则可能在错误的方向上优化半天。
第二步 找到最忙的线程
记下高 CPU 线程的 tid(十进制),转成十六进制:
printf '%x\n' 12345 # 输出 3039
用 jstack / 对应语言的栈抓取工具,按 nid 找到对应线程栈:
jstack <pid> | grep -A 20 'nid=0x3039'
栈里能看到这个线程卡在循环、正则回溯、还是锁等待。多数情况是某段被高频调用的代码在空转。对 Go 用 pprof,对原生用 perf,思路一样:先锁定热点再读栈,而不是凭感觉改代码。线程栈要结合代码版本来看,否则只看到符号名也定位不到行。
第三步 用火焰图看全局热点
单看一个线程不够,要全局看谁占 CPU。抓一段时间栈做火焰图:
perf record -F 99 -p <pid> -g -- sleep 30
perf script | ./stackcollapse | ./flamegraph > cpu.svg
火焰图横向是采样占比,越宽的框占用越多。顺着最宽的路径往下钻,能直接定位到函数甚至代码行。比逐个线程看栈高效得多,尤其当热点分散在多个线程时,火焰图一眼就能看出主因。采样时长要覆盖业务波动周期,否则偶发高峰会被平均掉。
第四步 对照调用频率与流量
CPU 高但 QPS 正常,多半是内部循环或重试风暴;CPU 高且 QPS 也高,是容量问题。查日志里是否有大量重复报错触发重试:
# 看单位时间内某错误的出现次数
grep 'TimeoutException' app.log | awk '{print $1}' | uniq -c
频繁超时后的无退避重试,会在短时间内把 CPU 打满,这类“自我放大”最常见,也最容易被误判成业务量上涨。看到错误数和 CPU 同涨,先怀疑重试链路,再怀疑业务代码,顺序错了会绕远路。
第五步 容器场景的坑
容器内 top 看到的是宿主机视角,要用 cgroup 受限的指标:
kubectl top pod <pod> # 看被 limit 约束后的实际占用
cat /sys/fs/cgroup/cpu/cpuacct.usage
CPU limit 设太低,进程会被节流(throttling),表现成“卡”而非“跑满”,这种要调 requests/limits 而非优化代码。看 cpu.stat 里的 throttled_periods 判断是否被限流,限流导致的“慢”优化代码无效,调了一圈才发现是 limit 太低,是最典型的冤枉路。
离线 CPU 与在线 CPU
CPU 时间分“在线”(on-cpu,真正在跑指令)和“离线”(off-cpu,在等锁、等 IO、等调度)。飙高若是 on-cpu,按上面路径查热点函数;若是 off-cpu 高而 on-cpu 不高,问题在等待,要看锁竞争和阻塞栈,常规 CPU 分析会漏掉这类。把两类都看,才不会被表象误导。
协程与线程泄漏
有时 CPU 高不是单点热点,而是成百上千个本该结束的协程/线程没退出,每个占一点累积成山。看线程数或协程数是否随时间只增不减,配合栈看它们卡在哪。这类问题重启能缓解但不能根治,根治要修泄漏点,否则下一次更快涨满。
一个完整排查示例
某服务凌晨 CPU 从三成突增到满。先看 top -H 找到两个高占线程,转十六进制后 jstack 发现都在同一段正则匹配上。结合日志,是个别畸形输入触发正则灾难性回溯。临时用限流挡住,长期改成线性解析。整个过程没重启,靠栈直接定位到函数,从发现到修复不到一小时。这就是“先量化再下结论”的价值。
临时止血与根治
- 立即:对问题副本缩容重启、或对入口限流,先保护整体可用。
- 短期:定位到的热点加缓存、去重、合并批处理。
- 长期:把重试加上指数退避和上限,避免重试风暴;对 CPU 密集路径做基准测试。
踩坑清单
- 只看平均 CPU,被“均值正常”骗过,实则是毛刺型单核打满。要看每核和 P99 时段的峰值。
- GC 频繁造成的 CPU 高,栈里看不到业务代码,要看 GC 日志确认是不是对象分配过快。
- 容器内误读宿主 CPU 核数,程序开了过多线程,上下文切换吞噬算力。
- 限流没生效就急着回滚版本,结果回滚后同样飙高,浪费一次发布窗口。
- 止血重启后没保留现场(栈、堆转储),问题再重现时又要从头摸。
- 把 throttling 当代码慢去优化,调了一圈无果才发现是 limit 太低,白费功夫。
- 火焰图采样时间太短,没覆盖到偶发高峰,结论偏差。采样要覆盖业务波动周期。
CPU 飙高的定位贵在“先量化再下结论”。每一跳都要有数据支撑,而不是凭经验拍脑袋,否则既解决不了问题也学不到东西。
1 view
0 likes
0 comments