CPU 占用 100% 线上排查实战:从 top 到火焰图

半夜告警响起:某台生产机器的 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 GCGC 线程占满调堆与回收器,查内存泄漏
锁竞争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 超时 的幕后推手,因为后端算不过来,网关自然等超时。

十、排障速查表(照着做)

步骤命令目的
1top -H -p PID找高 CPU 线程 TID
2printf “0x%x” TID转十六进制 nid
3jstack / jcmd Thread.print拿线程栈
4perf record + FlameGraph可视化热点
5定位宽塔 → 改代码 → 验证根治

回到开头那句话:CPU 100% 不可怕,可怕的是你一上来就重启,把唯一的现场抹掉。记住这条主线——先采样,再重启;先定位,再改码。掌握 top、jstack、perf 和火焰图这四板斧,下一次告警响起时,你会比大多数人更快让机器安静下来。

上一篇 Elasticsearch 实战:用倒排索引实现全文搜索
下一篇 CompletableFuture 实战:Java 异步编程与任务编排
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]