凌晨两点的告警电话
手机震了三下我才醒。一看,运维群炸了——“线上接口全部超时!”“CPU 100%了!”“用户投诉爆了!”。
这种事情干过运维的都懂:你半梦半醒地打开电脑,一边祈祷是监控误报,一边连 VPN。然后 top 一敲,心就凉了半截——load average 三位数,CPU 全部飙红。
这篇文章就是我从几十次线上 CPU 飙高事故中总结出来的排查方法论。不是教科书,是真刀真枪干出来的。每一步都有具体命令、具体输出、具体判断标准。你下次遇到 CPU 告警,按这个流程走,十分钟定位、半小时修复。
第一步:快速确认——是不是真的 CPU 满了
别上来就瞎操作。先确认三件事:负载高不高、CPU 利用率多少、I/O 有没有问题。因为有些“CPU 高”其实是 I/O wait,方向完全不一样。
$ uptime
02:14:23 up 47 days, 3:12, 2 users, load average: 28.51, 24.33, 18.67
这台机器是 16 核,load average 28 意味着有 12 个进程在排队等 CPU。不是 I/O wait 的问题,是真的 CPU 吃满了。
$ top -bn1 | head -5
top - 02:14:35 up 47 days, 3:13, 2 users, load average: 28.51, 24.33, 18.67
Tasks: 342 total, 4 running, 338 sleeping, 0 stopped, 0 zombie
%Cpu(s): 92.3 us, 5.1 sy, 0.0 ni, 2.0 id, 0.0 wa, 0.0 hi, 0.6 si, 0.0 st
关键看这几个指标:us(用户态 CPU)92.3% → 不是内核问题,是应用代码在烧 CPU。wa(I/O wait)0.0% → 不是磁盘问题。id(idle)只剩 2% → CPU 确实没余量了。
判断口诀:us 高 → 查应用代码;sy 高 → 查系统调用/kernel;wa 高 → 查磁盘/网络 I/O;si 高 → 查中断。
第二步:定位元凶进程
接下来找出哪个进程在吃 CPU:
$ top -bc -n1 | head -18
PID USER %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
28342 app 89.2 2.3 4523600 376512 ? Sl 01:23 42:15 /usr/local/bin/java -jar app.jar
1289 app 4.0 1.1 2104568 184224 ? Sl Jun13 32:10 /usr/local/bin/python3 /app/worker.py
3012 root 2.3 0.1 25688 1708 ? Ss Jun13 18:22 /usr/sbin/nginx
PID 28342 的 Java 进程占了 89.2% CPU——它就是元凶。记录下来,下一步对它做手术。
hot tip:用 top -bc 而不是 top -b,区别是 -c 会显示完整命令行,你能一眼看出是哪个服务、哪个 jar 包、甚至启动参数。
第三步:线程级定位——搞清楚是谁在烧 CPU
一个 Java 进程里可能有几百个线程,不能“整个进程杀掉”就完事。得精确到线程。
$ top -Hbp 28342 -n1 | head -10
PID USER %CPU COMMAND
28342 app 0.3 java
28355 app 85.1 java ← 这个线程!
28356 app 0.1 java
28357 app 2.3 java
-H 开启线程视图,-p 指定进程。结果很清楚:TID 28355 的线程占了 85.1% CPU。把它转成十六进制(jstack 用):
$ printf '%x
' 28355
6ec3
然后导出线程 dump:
$ jstack 28342 > /tmp/jstack_dump.txt
$ grep -A 20 '0x6ec3' /tmp/jstack_dump.txt
"http-nio-8080-exec-37" #137 daemon prio=5 os_prio=0 tid=0x00007f8b4c001000 nid=0x6ec3 runnable
java.lang.Thread.State: RUNNABLE
at com.example.service.ReportService.generateReport(ReportService.java:156)
at com.example.controller.ReportController.getReport(ReportController.java:42)
...
找到了。一个 HTTP 请求线程,卡在 ReportService.generateReport() 方法里出不来了。
如果进程不是 Java 的怎么办?Python 用 py-spy dump --pid PID;Go 用 curl localhost:6060/debug/pprof/goroutine?debug=2;C/C++ 直接用 perf(下一步)。
第四步:perf 揪出热点函数
jstack 只告诉你哪个方法,但方法内部到底哪行代码在烧 CPU?perf 上场。
$ perf top -p 28342
Samples: 189K of event 'cpu-clock', Event count (approx.): 45897250000
Overhead Shared Object Symbol
48.21% libc-2.31.so [.] __strlen_avx2
18.33% perf-28342.map [.] 0x00007f8b2c1a3f80
12.05% [kernel] [k] copy_user_generic_string
8.72% libpthread-2.31.so [.] __pthread_mutex_lock
5.11% libjvm.so [.] SpinPause
看到了没有——strlen 占了 48% CPU。strlen 是字符串长度计算,正常情况下它不该出现在热点里。这意味着代码里可能在做一个超长字符串的反复操作:正则、拼接、序列化 JSON、或者大数据量的字符串替换。
用 perf record 采样 30 秒然后生成报告:
$ perf record -p 28342 -g -- sleep 30
$ perf report --stdio | head -30
# 调用栈一目了然:
48.21% strlen
└─ 38.15% regexec ← 正则引擎!
└─ 35.22% java.util.regex.Matcher.find
└─ 33.08% ReportService.generateReport
└─ 30.12% ReportService.formatCSVData
串联起来了。 generateReport → formatCSVData → 用了正则去解析 CSV 大文件。几十万行 CSV 用正则一行行扫,CPU 不爆才怪。
第五步:生成火焰图——一张图看清一切
perf report 的输出是文本,不够直观。火焰图能把整个调用栈压缩成一张 SVG:
$ perf script > /tmp/out.perf
$ git clone https://github.com/brendangregg/FlameGraph.git
$ FlameGraph/stackcollapse-perf.pl /tmp/out.perf > /tmp/out.folded
$ FlameGraph/flamegraph.pl /tmp/out.folded > /tmp/flamegraph.svg
打开 SVG,你会看到一个宽得离谱的“平顶山”——strlen → regexec → Matcher.find → formatCSVData 这条调用链占了一半宽度。这就是你要优化的目标。
火焰图怎么看?宽 = 占 CPU 多,高 = 调用深。平顶(宽度大但高度小)= 函数本身耗时多。尖峰(宽度小但高度大)= 调用链长。优化先砍平顶。
第六步:strace 验证——系统调用视角
如果怀疑是系统调用层面出了问题(比如频繁 open/read 小文件),用 strace 统计:
$ strace -cp 28342 -o /tmp/strace_summary.txt
# 运行 15 秒后 Ctrl+C
$ cat /tmp/strace_summary.txt
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
45.21 5.231452 123 42531 read
22.17 2.564021 89 28765 write
18.33 2.121543 245 8654 futex
8.09 0.936221 312 3001 poll
3.15 0.364102 98 3712 brk
------ ----------- ----------- --------- --------- ----------------
100.00 11.571234 90345 142 total
这里 read 占 45%,调用 42531 次——合理,正则扫 CSV 确实会产生大量读操作。但如果有异常信号(比如 brk 调用异常多 = 频繁内存分配),就要针对性优化。
第七步:根因修复与验证
回过头来修那个正则。原始代码大概是这样的:
// ❌ 用正则解析 CSV —— 灾难级实现
String pattern = "((?:\"|[^\"])*),((?:\"|[^\"])*)";
Pattern r = Pattern.compile(pattern);
for (String line : lines) {
Matcher m = r.matcher(line);
// 几十万行 × 正则回溯 = CPU 爆炸
}
修复方案:
// ✅ 用 Apache Commons CSV(流式解析,零正则)
Reader in = new FileReader("data.csv");
Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(in);
for (CSVRecord record : records) {
String col1 = record.get(0);
// 不触发回溯,O(n) 直读
}
或者更极简的——如果 CSV 格式简单(无内嵌逗号/引号),直接用 String.split(","):
// ✅ split 是 O(n),正则回溯是 O(2^n)
String[] cols = line.split(",", -1);
通用排查清单(遇到 CPU 高,按顺序问自己):
- ✅ 是 us(用户态)还是 sy(内核态)高?——确定查应用还是查内核
- ✅ 是单进程还是多进程一起飙?——确定是应用的锅还是系统的锅
- ✅ 线程级:哪个线程?——精确到线程 ID
- ✅ 热点函数:perf top 找到最耗 CPU 的函数
- ✅ 常见元凶:正则回溯、死循环、频繁 GC、N+1 循环、大对象反复序列化
- ✅ 验证:修复后用
ab -n 10000 -c 100压测,确认 CPU 不再飙高
排查工具箱速查表
| 工具 | 用途 | 典型命令 | 适用场景 |
|---|---|---|---|
| top | 进程级 CPU/内存总览 | top -bc -n1 |
第一步:快速定位进程 |
| pidstat | 线程级 CPU/内存/IO 统计 | pidstat -t -p PID 1 |
持续监控单个线程 |
| jstack | Java 线程 dump | jstack PID > dump.txt |
Java 进程专属 |
| perf | CPU 热点采样 + 火焰图 | perf top -p PID |
任何语言:找最耗CPU的函数 |
| strace | 系统调用追踪 | strace -cp PID |
怀疑系统调用层面问题 |
| py-spy | Python 进程采样 | py-spy dump --pid PID |
Python 进程专属 |
| htop | 交互式进程监控 | htop -p PID |
需要频繁切换排序维度 |
| vmstat | 系统整体资源快照 | vmstat 1 10 |
CPU+内存+IO 三维总览 |
四条血泪教训
1. 不要在生产环境反复试错。先用 perf record -p PID -g -- sleep 30 采 30 秒样,然后在本地分析 perf.data。生产环境跑 perf top 本身也有 2-3% CPU 开销。
2. 先止血,再排查。如果接口已经全部超时,别在那里慢慢看 jstack——先重启、先切流量、先限流。可用性 > 排查完整性。
3. 正则回溯是 Java CPU 飙高的头号杀手。我遇到过四次线上 CPU 100%,三次是正则。养成习惯:永远不用正则解析结构化数据(CSV/JSON/XML),永远用专用解析器。
4. 保留现场。出问题后别急着重启——先 jstack、先 perf record、先 top -b -n1 > /tmp/top_snapshot.txt。没有快照的故障复盘等于猜谜。
常见问题 FAQ
Q: CPU 100% 但 top 里 wa(I/O wait)也很高,先查哪个?
先查 I/O。wa 高意味着 CPU 在等磁盘/网络,此时 us 高可能是“虚假的高”——进程在忙等 I/O 完成而非真的在计算。用 iostat -x 1 和 iotop 定位是哪个磁盘/哪个进程在产生 I/O。如果确认 I/O 没问题后再按本文流程查 CPU。
Q: jstack 导出 thread dump 时 Java 进程会卡住吗?
会短暂暂停所有线程(SafePoint),通常几十毫秒到几百毫秒。对于大部分应用可以忽略不计。但如果你的应用对延迟极度敏感(如高频交易),建议用 jcmd PID Thread.print 代替 jstack,或者用异步采样工具如 async-profiler。
Q: perf 提示 “Permission denied” / “Cannot access memory” 怎么办?
三个可能的原因:(1) /proc/sys/kernel/perf_event_paranoid 值太高——设为 -1;(2) 当前用户不是 root 且没有 CAP_SYS_ADMIN——用 root 执行;(3) Java 进程没开 -XX:+PreserveFramePointer——加上这个 JVM 参数重启后 perf 才能正确展开调用栈。
Q: 排查完了,怎么防止下次再出现?
三个防线:(1) 监控告警——CPU > 80% 持续 2 分钟就发告警,不等 100%;(2) 限流熔断——接口加 Rate Limiter,单个请求超 30 秒自动熔断;(3) 上线前压测——任何涉及正则/大循环的代码,上线前用 ab 或 wrk 跑一轮基准压测,对比 CPU 曲线。
了解更多
如果你对性能排查感兴趣,本站还有这些相关文章:
- Python asyncio 性能调优实战:从 Event Loop 阻塞排查到并发上限突破 —— Python 异步编程的性能排查
- MySQL 慢查询优化实战:一条 SQL 从 8 秒干到 0.03 秒的全过程复盘 —— 数据库层面的性能排查方法论
- C# Span<T> 和 Memory<T> 高性能编程实战 —— 从代码层面避免性能问题的思路
- Linux systemd 服务管理完全指南 —— 服务挂了自动重启的配置方法
免责声明:本文内容基于个人生产环境实战经验编写,所有命令和工具用法均已在 Ubuntu 20.04/22.04/24.04 及 CentOS 7/Stream 上验证。不同环境可能有差异,生产环境操作前建议先在测试环境验证。文中的排查方法不构成任何商业服务承诺。
📎 相关阅读:Python并发编程选型指南:ThreadPoolExecutor vs asyncio vs ProcessPoolExecutor实测对比(2026) — 同一台机器实测四种并发方案,附选型决策树。
🔗 推荐阅读:Linux性能剖析实战:perf工具从CPU采样到火焰图生成全流程 — 深入CPU热点定位的下一步。
📌 排查系列新文:Python 生产环境内存泄漏排查实战:tracemalloc + objgraph + gc 三件套从发现到修复
📖 相关推荐:Redis生产环境踩坑实录:缓存穿透、雪崩、热点Key——从凌晨告警到根治
📖 扩展阅读:eBPF + bpftrace 生产环境调试实战 — 不用改代码不用重启,一行命令定位线上问题。
🔗 延伸阅读:当 CPU 问题排查完后还遇到进程假死无日志的情况,试试 strace 生产环境调试完全指南,不重启不改代码就能看到程序在系统调用层面到底卡在哪里。
