半夜告警响起:某台生产机器的 CPU 占用率 突然冲到 100%,接口延迟飙升、健康检查开始失败。这种「CPU 100% 排查」是后端工程师最高频的线上事故之一。很多人的第一反应是重启,但重启会销毁现场,让你永远查不到根因。本文给出一套从 top、jstack、perf 到火焰图(Flame Graph)的标准排查流程,帮你在不重启的前提下,十分钟定位到真正吃 CPU 的那行代码。
一、先别急着重启,稳住现场
CPU 飙到 100% 时,最忌讳的就是「先重启再说」。重启之后进程回到初始状态,调用栈、热点函数、线程状态全部消失,你只会看到「好了」,却不知道为什么好,更不知道它什么时候再炸。正确的第一步是把现场留下来:记录出问题的进程 PID、当前时间、最近一次发版记录,以及任何能解释「为什么是现在」的线索(比如定时任务、流量高峰、新上线代码)。只要进程还活着,后面所有采样工具都能 attach 上去。
如果这台机器上还跑着别的业务,且 CPU 已经高到 SSH 都卡顿,可以先用 nice 或 cgroups 临时限制一下问题进程的 CPU 配额,给排查留出操作空间,而不是一刀切地 kill。稳住现场,是所有线上排查的黄金法则。
二、用 top/htop 找到吃 CPU 的进程
先确认到底是哪个进程在吃 CPU。用 top 进入后按 P 可按 CPU 占用排序;如果知道是大进程(比如 Java 服务),可以只监控它:
# 只看某个名字的进程,按 CPU 排序
top -c -p $(pgrep -d, -f "java -jar")
# 进入 top 后常用键
P # 按 CPU 占用排序
M # 按内存占用排序
1 # 展开多核,看是不是单核打满
# 全局负载与上下文切换
vmstat 1
# 关注 r(运行队列)、cs(上下文切换)、us/sy/id
top 的默认刷新是 3 秒,盯几屏就能看出是「一个进程长期占满」还是「多个进程轮流吃」。下面这张表帮你快速读懂关键列:
| 列 | 含义 | 怎么用 |
| PID | 进程号 | 下一步 attach 的目标 |
| %CPU | 该进程 CPU 占比 | 接近 100×核数 说明基本打满 |
| %MEM | 内存占比 | 排除内存换页牵连 CPU |
| COMMAND | 启动命令 | 确认是不是预期服务 |
| TIME+ | 累计 CPU 时间 | 持续增长说明是真热点 |
2.1 只看某个进程里的线程
一个 Java 进程里往往上百个线程,光看进程不够,要看到线程级。加 -H 参数即可显示线程:
# -H 显示线程,按 CPU 排序后找高占用 TID
top -H -p 18321
# 等价地用 htop(更直观,支持树状展开)
htop -p 18321
三、用户态还是内核态?决定排查方向
top 头部那行 %Cpu(s) 藏着关键分诊信息:us(用户态)、sy(内核态)、wa(IO 等待)、id(空闲)。它是整个排查的方向盘。
%Cpu(s): 85.2 us, 8.1 sy, 0.0 ni, 5.3 id, 0.0 wa, 0.0 hi, 1.4 si
| 指标 | 高代表 | 排查方向 |
| us 高 | 应用代码在算 | 业务死循环、正则回溯、序列化、GC |
| sy 高 | 内核在忙 | 大量系统调用、锁竞争、频繁上下文切换 |
| wa 高 | 等 IO | 磁盘慢、网络阻塞(参考 内存 swap 导致性能下降) |
| si 高 | 软中断 | 网卡中断、大量小包 |
绝大多数「业务代码 bug 导致」的 CPU 100%,都是 us 高。少数情况是 sy 高,比如线程数爆炸引发调度开销,或频繁 文件描述符耗尽 后反复重试系统调用。
四、Java 应用:top -H + jstack 抓热点线程
确认是某个 Java 进程的用户态 CPU 高之后,下一步是用线程号反查调用栈。思路是:在 top -H 里找到那个高 CPU 线程的 TID(十进制),转成十六进制,再去 jstack 输出里匹配 nid。
# 1) 找到高 CPU 线程的 TID,假设是 18345
top -H -p 18321
# 2) 十进制 TID 转十六进制 nid(小写)
printf "0x%x\n" 18345 # 输出 0x47b9
# 3) 抓取线程栈并定位该 nid
jstack 18321 | grep -A 30 '0x47b9'
在容器化环境里,jstack 可能因 PID namespace 不符而失败,这时改用 jcmd 更稳:
# 容器内首选,避免 PID namespace 问题
jcmd 18321 Thread.print > thread_dump.txt
# 高版本 JDK 也可用
jstack -l 18321 > thread_dump.txt
如果看到大量线程卡在同一行业务代码,那就是热点;如果满屏是 GC task thread,说明是频繁 GC 把 CPU 吃光,要先去看堆和 GC 日志,而不是业务代码。
五、perf 采样:不依赖语言,直接看热点函数
jstack 只能看 Java 栈,而且采样瞬间有偏差。Linux 自带的 perf 是更底层的「显微镜」:它对任意进程做定时采样,告诉你 CPU 时间到底花在哪个函数上,连 C/C++ 原生代码和 JVM 内部的开销都能看到。
# 实时看热点符号(按方向键退出)
perf top -p 18321
# 采 30 秒火焰图原料(-F 采样频率,99 避开整数倍;-g 带调用栈)
perf record -F 99 -p 18321 -g -- sleep 30
perf report # 交互式查看
# 统计模式:看整体 CPU 事件
perf stat -p 18321 -- sleep 10
| 子命令 | 用途 | 场景 |
| perf top | 实时热点 | 快速确认是不是真有热点 |
| perf record | 离线采样 | 生成火焰图原料 |
| perf report | 分析采样 | 交互式钻取函数 |
| perf stat | 计数器统计 | 看 IPC、缓存命中率 |
六、火焰图:把采样变成一眼看懂的图
perf report 的字符界面不够直观。把采样数据交给 Brendan Gregg 的 FlameGraph 工具,就能生成一张火焰图:横轴是采样数量(约等于 CPU 时间),纵轴是调用栈深度,越宽的顶层方块,就是越吃 CPU 的函数。一眼扫过去,最宽的那个尖塔往往就是元凶。
# 1) 导出调用栈
perf script > out.perf
# 2) 折叠调用栈(需用 FlameGraph 仓库里的脚本)
stackcollapse-perf.pl out.perf > out.folded
# 3) 生成 SVG 火焰图
flamegraph.pl out.folded > cpu.svg
# 一行版:直接管道
perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg
| 火焰图特征 | 读法 |
| 方块宽度 | 占用 CPU 时间越多越宽 |
| 顶层尖塔 | 最宽的那座就是热点函数 |
| 颜色 | 仅用于区分,无性能含义 |
| 平顶山 | 大量相同栈,通常是循环/批量 |
拿到 cpu.svg 后用浏览器打开,点击任意方块可放大该调用路径。通常你会看到某一座异常宽的塔,点进去就是「正则匹配」「JSON 序列化」或「加密解密」之类的根因函数。
七、常见根因清单(附排查动作)
| 根因 | 典型特征 | 一句话动作 |
| 业务死循环 | 单线程 us 100% | 火焰图定位循环体,加退出条件 |
| 正则灾难性回溯 | 单请求 CPU 暴涨 | 改非贪婪/限定长度,预编译 Pattern |
| 大对象序列化 | GC 线程吃 CPU | 缩小对象、换二进制协议 |
| 频繁 Full GC | GC 线程占满 | 调堆与回收器,查内存泄漏 |
| 锁竞争 | sy 偏高、线程 BLOCKED | 缩小锁粒度,参考 数据库死锁实战 |
| 密集加解密/压缩 | 单核打满 | 限流、异步化、换硬件加速 |
八、一次真实复盘:正则把 CPU 吃满了
曾经有个网关服务,每到整点 CPU 就 100%。火焰图一开,最宽的塔是一座叫 Pattern.matcher 的山。追进去发现是一段日志脱敏正则:用 .* 去跨行匹配整段报文,遇到超长且不匹配的输入时触发灾难性回溯,复杂度从线性退化成指数级。修复方式是:先把报文按行切分、给正则限定长度上限、并把 Pattern 预编译成静态常量复用。改完之后整点尖峰消失,CPU 平稳在 20% 以下。教训是:只要把「用户输入」喂给带 .* / (a+)+ 这类结构的正则,就必须假设它会回溯到天荒地老。
九、预防与监控:别等炸了才查
最好的排查是不发生。把 CPU 监控接进 Prometheus + Grafana,给每台机器设阈值告警,比半夜被叫醒强得多。配合 Prometheus + Grafana 监控面板 里的基础指标,再加一条 host CPU 使用率规则即可:
# prometheus.yml 抓取 node_exporter
scrape_configs:
- job_name: "node"
static_configs:
- targets: ["192.168.1.10:9100"]
# 告警规则:单核或整机 CPU 持续 > 85%
- alert: HighCpuUsage
expr: 100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 5m
labels: { severity: warning }
此外,上线前用 wrk 或 ab 做一次简单压测,往往能在预发环境就暴露「某接口一压就满 CPU」的问题。同时把 CPU 飙升的连锁反应纳入预案——它常常是导致上游 Nginx 504 超时 的幕后推手,因为后端算不过来,网关自然等超时。
十、排障速查表(照着做)
| 步骤 | 命令 | 目的 |
| 1 | top -H -p PID | 找高 CPU 线程 TID |
| 2 | printf “0x%x” TID | 转十六进制 nid |
| 3 | jstack / jcmd Thread.print | 拿线程栈 |
| 4 | perf record + FlameGraph | 可视化热点 |
| 5 | 定位宽塔 → 改代码 → 验证 | 根治 |
回到开头那句话:CPU 100% 不可怕,可怕的是你一上来就重启,把唯一的现场抹掉。记住这条主线——先采样,再重启;先定位,再改码。掌握 top、jstack、perf 和火焰图这四板斧,下一次告警响起时,你会比大多数人更快让机器安静下来。




