ElIauk

线上 CPU 100% 排查实录

先分清是谁在烧 CPU 看到 CPU 打满,很多人的第一反应是“业务代码死循环了”,然后去翻最近上线的代码。这个方向只对了一半:CPU 高也可能是 GC 线程在疯狂回收、日志框架在同步刷盘、或者某个正则在大文本上回溯。方向错了,翻一天代码也找不到原因。 排查的第一步不是看代码,是看线程。进程级 CP

评论
线上 CPU 100% 排查实录

先分清是谁在烧 CPU

看到 CPU 打满,很多人的第一反应是“业务代码死循环了”,然后去翻最近上线的代码。这个方向只对了一半:CPU 高也可能是 GC 线程在疯狂回收、日志框架在同步刷盘、或者某个正则在大文本上回溯。方向错了,翻一天代码也找不到原因。

排查的第一步不是看代码,是看线程。进程级 CPU 只是一个总和,只有落到具体线程和栈上,才知道是谁在烧。

五分钟定位到热点线程

假设应用 PID 是 12345,先按线程看 CPU:

top -Hp 12345 -d 1

找到占用最高的线程 ID(十进制),转成十六进制,再去线程栈里搜:

printf '%x\n' 12377
jstack 12345 > /tmp/thread-1.txt
grep -A 30 'nid=0x3059' /tmp/thread-1.txt

jstack 输出里的 nid 是十六进制,和 top -H 的十进制对不上是新手最常见的卡点。也可以直接用 jcmd 12345 Thread.print,效果一样。

一个关键技巧:间隔 5 到 10 秒抓两到三次,看同一个方法是否稳定出现在栈顶。只抓一次,很可能抓到的是正在做 GC 或者刚巧在执行的短方法,误判率很高。

如果条件允许,用 async-profiler 直接出火焰图,比手工对线程高效得多:

./profiler.sh -d 30 -e cpu -f /tmp/cpu.html 12345

30 秒的采样能覆盖大部分抖动,时间太短会漏掉间歇性热点。async-profiler 3.x 把脚本改名为 asprof,参数基本一致,老项目里看到 profiler.sh 也不用奇怪。

如果热点线程是 GC

top -H 里排名靠前的线程名如果是 GC Thread#、G1 Young RemSet 这类,说明 CPU 花在垃圾回收上,业务线程其实在等。这时候再去看业务栈没有意义,要转向内存:

jstat -gcutil 12345 1000 10

看 Old 区使用率和 Full GC 次数。Full GC 后 Old 仍然居高不下,基本就是内存泄漏或堆太小;Young GC 极其频繁但每次回收量很大,可能是分配速率过高,比如循环里创建大对象、日志拼接、JSON 序列化。

还有一种容易被忽略的情况:容器里 top 看到的是宿主机视角,CPU 使用率可能包含其他容器。用 kubectl top pod 或 cgroup 的 cpu.stat 看容器自己的消耗,再结合 JVM 的 -XX:ActiveProcessorCount 确认线程数假设是否成立。

常见根因

死循环或无限重试。 典型是 while 等待某个条件,条件永远不满足;或者重试没有退避,失败后立刻再来一次。栈上会看到同一个方法反复出现。

正则回溯。 一个 (a+)+$ 之类的表达式,配上几 KB 的输入,就能让单核跑满几分钟。栈里通常是 Pattern、Matcher 相关方法,日志里能看到某条校验规则耗时异常。

日志或序列化开销。 在循环里 log.debug 拼字符串、对大对象做 toString、每次请求都全量序列化,CPU 会稳定地高,但火焰图很分散,没有单一热点。

锁竞争与自旋。 高并发下 CAS 失败重试、synchronized 争抢,CPU 上去了吞吐却没涨。线程栈里大量线程处于 BLOCKED 或反复执行 compareAndSet。

热点集合。 ConcurrentHashMap.computeIfAbsent 里放了耗时逻辑,或者用 AtomicLong 做全局计数,都会形成单点竞争。

常见坑

坑一:只看进程不看线程。 top 显示 800%,不知道是 8 个线程各 100% 还是一个线程打满多核,处理方式完全不同。

坑二:只抓一次线程栈。 抓到的是瞬时状态,可能刚好在 GC 或者在做类加载。多抓几次,看重复出现的栈。

坑三:容器里用错 PID。 jstack 要在容器内执行,PID 是容器内的 1 或者 Java 进程号,不是宿主机上的 PID。

坑四:线程 ID 没转十六进制。 十进制直接去 jstack 里搜,永远搜不到。

坑五:忽略 GC 线程。 看到 CPU 高就钻业务代码,结果根因是内存泄漏导致的频繁 Full GC。

坑六:采样时间太短。 10 秒的火焰图可能只覆盖了一次请求,间歇性热点完全看不到。

排查清单

  1. 确认影响范围:单实例还是全部实例,是否与发布、流量、定时任务时间点相关。
  2. top -H 找热点线程,转十六进制,jstack 抓 2 到 3 次。
  3. 热点是 GC 线程就转内存排查,是业务线程就看栈顶方法。
  4. 用 async-profiler 或 Arthas 的 thread -n 3 交叉验证。
  5. 定位到代码后先做最小修复(降级、限流、关开关),再谈根治。
  6. 修复后用同样的压测流量验证 CPU 曲线和 P99 是否回到基线。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者