前言
性能工具篇讲了"怎么用",这篇讲"怎么破案"——一个 CPU 告警的完整排查复盘,还原每一步的推理过程。
一、案发
22:14 告警: prod-api-03 CPU > 90%, 持续 5 分钟
影响: 该节点上的服务 P99 延迟 300ms → 2.8s
二、第一现场:top 全景
top # 按 P(CPU排序)按 1(展开核)
load average: 14.2, 12.8, 9.5 ← 8核机器, load 14 明显过载
%Cpu(s): 92.3 us, 5.1 sy, 0.0 wa, 0.0 id, 0.0 st
▲
└ 92% 是 us(用户态)! ← 关键线索①: 应用在烧CPU, 不是内核/IO
PID USER %CPU %MEM S COMMAND
8473 app 780.0 8.2 R java ← 元凶进程(多线程吃满约8核)
9102 app 2.1 3.1 S sidecar
推理:
us 高→ 应用代码问题(死循环/正则回溯/GC 风暴),而不是内核态(系统调用/锁)或 IOwa=0, st=0→ 排除磁盘慢与宿主机超卖- 进程 8473 一家吃掉 780% → 线程级下钻
三、线程级下钻
# 看该进程内哪个线程在烧
top -H -p 8473
PID USER %CPU S COMMAND
8501 app 198.0 R java ← 线程 8501 烧了约2核
8502 app 198.0 R java
8503 app 190.0 R java
8473 app 5.2 S java
三个线程几乎打满各 2 核。把线程号转成 16 进制(Java 的线程栈用 nid=0x… 标识):
printf "%x\n" 8501 # 2135
jstack 8473 | grep -A 20 "nid=0x2135"
"worker-3" #123 daemon prio=5 os_prio=0 tid=0x... nid=0x2135 runnable
at com.demo.parser.RegexParser.match(RegexParser.java:87)
at com.demo.parser.RegexParser.parse(RegexParser.java:45)
...
线索指向:正则匹配模块。看代码:
// RegexParser.java:87
private static final Pattern BAD = Pattern.compile("(a+)+b");
// 典型的灾难性回溯(ReDoS 模式)!
// 输入 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaX"(不匹配b)时, 回溯组合爆炸
初步结论:一段恶意/异常输入触发正则回溯风暴,三个 worker 线程全陷进去 → CPU 100% → 处理队列堆积 → 延迟飙升。
四、用 perf 佐证(不是 Java 也能用这招)
# 对进程采样 10 秒
perf top -p 8473
# 或采样落盘
perf record -F 99 -p 8473 -g -- sleep 10
perf report --stdio | head -30
62.41% libjava.so [.] re_match
18.20% libjava.so [.] re_compile
...
re_match
RegexParser.match
...
re_match(正则引擎)占 62% ——与 jstack 结论互相印证。
火焰图(一图看穿):
git clone https://ghfast.top/https://github.com/brendangregg/FlameGraph
perf record -F 99 -p 8473 -g -- sleep 15
perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > cpu.svg
# 浏览器打开: 一片"高原"正对着 re_match → 热点一目了然
五、应急与根治
应急(5 分钟内恢复)
# 1. 隔离: 从负载均衡摘除该节点(别急着重启, 保留现场)
# 2. 抓证据
jstack 8473 > /tmp/jstack-$(date +%F-%H%M).txt # 已抓
perf record ... # 已采样
# 3. 临时止血: 限流该接口 / kill 卡死线程
curl -X POST http://localhost:9090/actuator/throttle -d 'regex_api=off'
根治
// 1. 修正则: 消除嵌套量词
Pattern.compile("a+b"); // 等价且线性时间
// 或用占有优先量词断掉回溯
Pattern.compile("(?>(a+))b");
// 2. 输入长度限制 + 匹配超时兜底
if (input.length() > 1024) throw new BizError("input too long");
// 3. 加监控: 该接口处理耗时 P99 告警(这次是 CPU 告警先响, 属侥幸)
六、方法论沉淀:CPU 高的分流决策树
top 一眼看四数:
├─ us 高 → 应用代码问题
│ ├─ Java: top -H + jstack(16进制nid) / arthas thread -n 5
│ ├─ Go: top -H + pprof(http://x/debug/pprof/profile)
│ └─ C/通用: perf top / 火焰图
├─ sy 高 → 系统调用/上下文切换/锁
│ └─ vmstat 看 cs/in, strace -c -p PID 统计调用, 锁竞争查 /proc/PID/stack
├─ wa 高 → IO 瓶颈(CPU 在等盘)→ 转 iostat 路线(性能篇)
├─ st 高 → 宿主机超卖 → 找云厂商/迁移
└─ si 高 → 软中断(网络包)→ mpstat -P ALL 看, nethogs 找流量源
load 高但 CPU 空闲? → D 状态进程排队(IO/NFS 卡死),vmstat 看 b 列,ps -eo pcpu,stat,wchan -p 查等待点。
七、工具对照卡
| 工具 | 一句话 |
|---|---|
| top -H -p PID | 线程级 CPU 定位第一步 |
| jstack / arthas | Java 栈快照(nid 十六进制对线程) |
| perf top / record | 采样热点函数(内核用户态都行) |
| 火焰图 | 宽 = 热,一眼找宽路 |
| strace -c | 统计系统调用开销(sy 高时用) |
| pprof | Go 程序的 profile 标配 |
八、复盘清单
- 告警先摘流量、保现场,再动手查(这次顺序对了)
- us/sy/wa/st 四象限分流,避免瞎忙
- 线程 → 栈 → 代码行,证据链完整
- perf 与 jstack 交叉验证
- 根治后加"耗时维度"监控(CPU 只是症状放大器)
- 正则这类"埋雷代码"进 code review 检查项
小结
CPU 排查的完整闭环:top 定方向(us/sy/wa/st)→ 线程级下钻(top -H)→ 栈/采样定位(jstack/perf/pprof)→ 代码级根因 → 应急+根治+监控补位。工具不难,难的是每一步都带着假设去验证。
本文是「Linux 运维」系列第 14 篇。