凌晨两点,告警响了:某台机器 CPU 使用率 98%,持续十分钟。
我见过太多人的第一反应是三连:ssh 上去 → top 看一眼 → 重启服务。
重启确实管用。十分钟后一切正常,群里一片"解决了"。然后下周同一时间,它又响了。
因为重启什么都没解决,它只是把证据销毁了。
今天把一套能复用的排查流程写清楚。不依赖任何花哨工具,从 Linux 自带命令出发,最后再讲可选的增强手段。你不需要背下所有参数,只需要记住这条链:进程 → 线程 → 栈 → 代码行。
一、先说三件"千万别急着做"的事
1. 别急着重启。
至少先留三份东西:top -bn1 的快照、top -Hp <pid> 的线程快照、jstack(如果是 Java)。这几条命令都是秒级的,成本远低于一次无头绪的重启。
2. 别一上来就上 heavy 工具。
strace -p 挂到一个高并发进程上,可能直接把 QPS 打穿;arthas trace 不加条件过滤,同样会拖垮服务。观察工具的开销,必须远小于问题的代价。
3. 别只看"总使用率"。
top 第一行的 %Cpu(s) 里藏着整道题的答案,但很多人只盯着那个 98.0 us。请把这一行拆开看:
%Cpu(s): 92.3 us, 3.1 sy, 0.0 ni, 2.8 id, 0.0 wa, 0.0 hi, 1.8 si, 0.0 st
- us(用户态)高 → 你的代码在算东西。多半是死循环、正则回溯、序列化、加解密、大量对象创建导致 GC 狂飙。→ 走「线程级」排查。
- sy(内核态)高 → 系统调用太多。典型是频繁上下文切换、锁争用走内核 futex、大量短连接、
gettimeofday满天飞。→ 走pidstat -w/vmstat/strace -c。 - wa(iowait)高 → 这不是 CPU 问题,是磁盘 IO 问题 。CPU 其实在 idle 等待,只是被记成"忙"。这时候去查 CPU 是白费功夫,该查
iotop、慢 SQL、日志刷盘。 - si(软中断)高 → 网络包打断点。常见于高吞吐网络服务、网卡中断集中在一个核。→ 查
/proc/interrupts、RSS 中断亲和性。 - st(steal)高 → 问题不在你。宿主机超卖了,你在和邻居抢物理核。这种时候你在容器里做什么都没用,得找运维/云厂商。这一条能帮你少背一半锅。
一句话:先看分类,再决定往哪条路走。这一步做对,后面省两小时。
二、主干流程:五步,从机器到代码行
第 1 步:定位进程
bash
top -c # -c 显示完整命令行,避免分不清哪个 java 是哪个服务
关注两个细节:
- 按
P按 CPU 排序,确认是不是你以为的那个进程; - 看
%MEM和TIME+:TIME+累计时间异常陡增,说明这个进程长期在高负载跑,不是偶发。
如果确认是容器环境,注意 top 看到的可能是宿主机的核数视角,建议配合:
bash
docker top <container>
# 或进容器内看 cgroup 限制
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us
第 2 步:定位到线程
bash
top -Hp <pid> # H=线程模式,p=指定进程
这会列出该进程下所有线程的 CPU 占用。记下最耗 CPU 的那几个 TID(线程 ID,十进制)。
顺带看一件事:是一个线程吃满,还是几十个线程一起高?
- 单线程高 → 大概率是某个具体逻辑(死循环、单次重计算、正则)。好查。
- 全体线程都高 → 通常是整体负载过高、GC、或者线程池被打满。这是另一类问题,别往单线程思路上走。
第 3 步:把 TID 转成十六进制
bash
printf "%x\n" <tid>
# 例如 12345 → 3039
因为 JVM 栈里的 nid 是十六进制的。这一步忘转,后面 grep 不到任何东西------我踩过,希望你不用踩。
第 4 步:抓栈
Java:
bash
jstack <pid> > /tmp/stack.$(date +%s).txt
grep -A 30 "0x3039" /tmp/stack.*.txt
拿到栈之后,重点不是看方法名,是看状态:
| 线程状态 | 含义 | 方向 |
|---|---|---|
RUNNABLE |
真在跑(或在 native 里跑) | 看业务方法,大概率就是它 |
BLOCKED |
等 synchronized 锁 | 搜同一个 monitor 地址,找持有者 |
WAITING / TIMED_WAITING |
等在 lock/park/sleep | 通常不是 CPU 元凶,除非数量异常 |
停在 GCTaskThread / VM Thread |
GC 导致的 CPU | 转 GC 日志分析,别在业务代码里找 |
如果不是 Java:
bash
# 通用:采样调用图
perf top -p <pid> # 实时,看热点函数
perf record -g -p <pid> -- sleep 30 && perf report # 离线,可留存证据
# 怀疑系统调用过多
strace -p <pid> -c -T # -c 汇总计数,-T 显示每次耗时(开销大,短时取样)
# 怀疑上下文切换 / 调度
pidstat -p <pid> 1 5 # 每秒一次,看 %usr/%sys
pidstat -w -p <pid> 1 5 # 看 cswch/nvcswch(自愿/非自愿切换)
# 怀疑卡在内核(sy 高、用户态栈很浅)
cat /proc/<pid>/stack
容器 / K8s 额外一条 :如果 Pod 设置了 resources.limits.cpu,而 CPU 使用率看起来不高却明显变慢,很可能是 CPU throttling:
bash
cat /sys/fs/cgroup/cpu/cpu.stat
# 看 nr_throttled 和 throttled_time ------ 一直在涨,说明被限额掐住了
这种情况的表现是"响应慢但 CPU 没满",很容易被误判成别的毛病。
第 5 步:验证,而不是直接改
找到可疑代码后,先在测试环境复现,或者用只读方式验证:
- 加一个计数器/日志确认循环次数;
- 用
arthas watch/trace看入参和耗时(注意加条件表达式限制采样量); - 用正则/SQL 的 explain 单独跑那条语句。
**改之前问自己一句:这个解释能不能同时解释"什么时候开始"和"为什么是这个时间点"。**能对上的,才值得发版。很多"看起来很像"的根因,其实只是巧合。
三、四种高频根因,对照着查
这是我这几年遇到最多的四类,按出现频率排:
1. 死循环 / 空转(us 高,单线程)
经典形态:while (flag) 里没有 sleep、重试次数没上限、List.remove 时索引没动、BigDecimal 除不尽没设 scale 抛异常后被吞掉又重试。
特征:那个线程的栈永远停在同一个位置,TIME+ 疯涨。
2. 正则回溯(us 高,单线程,栈停在 regex)
输入稍微长一点、带嵌套的量词(比如 (.*?)+、([a-z]+)+$),复杂度指数级上升。一个请求就能吃满一个核。
对策:给输入长度加硬上限;用 possessive 量词或改写表达式;线上直接拒绝超长输入。
3. GC 导致的 CPU(us+sy 都高,jstack 里一堆 GC 线程)
表现很有欺骗性:业务线程其实都在等,CPU 却很高,RT 抖动剧烈。
判断:jstat -gcutil <pid> 1000 看 Full GC 频率;有 GC 日志就直接看日志。老年代反复接近 100% 然后骤降,就是典型的内存泄漏前兆------这时候换大堆只是续命。
4. 锁争用 / 线程池打满(sy 高,BLOCKED 多,或 nvcswch 高)
严格说这不完全是"CPU 不够",而是"线程太多在抢"。表现为线程数暴涨、pidstat -w 里非自愿切换极高、平均 RT 拉长。
对策:查线程池队列是不是无界的、有没有在 pool 里再调 pool(嵌套异步)、synchronized 是不是锁了太大的范围。
四、三个我亲手翻过的车
车一:在 prod 上 strace -p 跟了二十分钟。
当时想看看它在干什么系统调用,忘了 -c 汇总,直接裸挂。结果 QPS 掉了四成,差点把偶发卡顿变成真实事故。
现在我的规矩:任何带侵入性的工具,先限时、先采样、先在预发试一遍开销。
车二:只看了 us,忽略了 st。
一台云主机周期性卡顿,查了半天应用层,最后发现 st 长期 5%---15%。是宿主机超卖,邻居在挖矿。那天我学到的:先把"不是我的问题"排除掉,再去承认是自己的问题。
车三:找到了热点,改错了地方。
perf 显示某个 JSON 序列化方法占 40% CPU,于是我们换了更快的库,提升微乎其微。后来才发现它是被调用了三百万次,真正的根因是上层一个循环里重复序列化同一个对象。
教训:占比高 ≠ 该优化。占比 = 单次耗时 × 调用次数。两个因子都要看,否则你会去优化一个本身已经很快、只是被叫得太勤的方法。
五、一张可以贴在显示器旁边的速查卡
| 现象 | 第一反应命令 | 大概率方向 |
|---|---|---|
| us 高,单线程 | top -Hp → printf "%x" → jstack/perf |
死循环、正则、重计算 |
| us 高,全体线程 | jstat -gcutil、看线程数 |
GC、整体过载 |
| sy 高 | pidstat -w、vmstat 1、strace -c(短时) |
锁争用、上下文切换、系统调用风暴 |
| wa 高 | iotop、iostat -x 1、慢 SQL |
磁盘 IO,不是 CPU 问题 |
| si 高 | cat /proc/interrupts |
网络软中断、中断亲和性 |
| st 高 | --- | 宿主机超卖,找运维 |
| CPU 不满但慢 | 查 cgroup cpu.stat 的 nr_throttled |
K8s CPU limit 限流 |
| 线程数暴涨 | `ps -eLf | grep |
最后补一句比技术更重要的话:
每次处理完,写一份不超过三百字的事故小结:现象 → 关键证据(附上那几条命令的输出)→ 根因 → 怎么发现早一点(加什么监控项)。
这份小结的价值在于:下一次凌晨两点告警响的时候,你不用再从头推理一遍。排查能力的复利,来自记录,不来自经历。
你遇到过最离谱的 CPU 100% 根因是什么?
欢迎在评论区说说(最好带上 top 那行 %Cpu(s) 的分布和你的最终结论)。我把自己那条"邻居在挖矿"的经历放上面了,期待有人比我更惨。也欢迎补充非 Java 技术栈的定位姿势------Go 的 pprof、Python 的 py-spy、Node 的 0x / clinic,这些我没展开,评论区补全比正文更有价值。
⚠️ 几点说明:
- 文中命令为通用思路,不同发行版、内核版本、JDK 版本、容器运行时(cgroup v1/v2)路径与输出可能有差异 ,请以你实际环境为准;cgroup v2 下 CPU stat 路径为
/sys/fs/cgroup/cpu.stat。 - 生产环境操作请遵守变更规范:优先在只读/低侵入模式下取证,涉及重启、扩缩容、参数调整请走审批与灰度流程。
- 本文不构成性能优化的唯一标准答案;同一现象在不同架构下可能有完全不同的根因,以实际观测数据为准,不要套用结论。