服务覆盖:昆明·曲靖·玉溪·保山·昭通·丽江·普洱·临沧·楚雄·红河·文山·西双版纳·大理·德宏·怒江·迪庆

Linux服务器CPU 100%排查实录:从top到火焰图

eycit 2026-04-19 -3 次阅读 系统安装
---

theme: default themeName: "默认主题" title: "Linux服务器CPU 100%排查实录:从top到火焰图"


Linux服务器CPU 100%排查实录:从top到火焰图

周五下午五点半,正准备收工。运维群里弹了一条消息:"生产服务器prod-web-03的CPU持续100%,已经跑了20分钟了。"

打开监控面板一看,8核的服务器,每个核心都跑满了,系统负载飙到45。SSH连上去后终端响应都卡顿了,说明CPU确实被榨干了。

这种情况最忌讳上来就`kill -9`。万一杀的是主业务进程,问题没解决反而制造了新的故障。排查要稳,按照"定位进程→定位线程→定位函数→定位代码"的链条一步步来。

第一层:top定位异常进程

top -c -o %CPU

`-c`显示完整命令行,`-o %CPU`按CPU使用率排序。输出很直接:

PID   USER   %CPU  %MEM  COMMAND

12483 app 98.2 2.1 java -jar /opt/app/payment-service.jar

一个Java进程吃了接近98%的单核CPU。按`1`看各个核心的使用情况,发现是Core 3被这个进程打满了,其他核心还有余量。

到这里,问题范围已经从"哪台服务器"收窄到"哪个进程"。

第二层:top -H定位异常线程

同一个进程里可能有上百个线程,不是所有线程都在疯狂消耗CPU。用`top -H`看线程级别的CPU分布:

top -H -p 12483

输出显示有两个线程特别突出:

PID   USER   %CPU  %MEM  COMMAND

12495 app 52.1 0.3 java 12501 app 46.3 0.3 java

线程ID是12495和12501。记下这两个数字,把PID转成十六进制,后面要用:

printf "%x\n" 12495

输出: 30cf

printf "%x\n" 12501

输出: 30d5

第三层:jstack抓线程堆栈

既然是Java进程,直接用`jstack`看线程在干什么:

jstack 12483 > /tmp/jstack_12483.txt

grep -A 50 "0x30cf" /tmp/jstack_12483.txt grep -A 50 "0x30d5" /tmp/jstack_12483.txt

两个线程的堆栈几乎一模一样:

"pool-thread-12" #12495 daemon prio=5 os_prio=0 tid=0x00007f8a3c001000 nid=0x30cf runnable [0x00007f8a24000000]

java.lang.Thread.State: RUNNABLE at com.example.payment.service.OrderValidator.validate(OrderValidator.java:87) at com.example.payment.service.OrderValidator.loopOrders(OrderValidator.java:63) at com.example.payment.service.OrderValidator$1.run(OrderValidator.java:45) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

线程卡在`OrderValidator.validate`的第87行,状态是RUNNABLE。这说明线程没有在等锁、没有在等IO,它就是在疯狂执行代码。结合接近100%的CPU使用率,基本可以确定这是一个死循环或者高密度计算。

到这里有经验的Java开发已经可以直接去看代码了,但我想进一步确认——用性能分析工具验证一下函数级别的热点分布。

第四层:perf抓火焰图数据

perf是Linux内核自带的性能分析工具,能精确到函数级别的CPU时间分布。先安装依赖:

# CentOS/RHEL

yum install perf -y

Ubuntu/Debian

apt install linux-tools-common linux-tools-$(uname -r) -y

然后用perf记录CPU profile数据,持续30秒:

perf record -F 99 -p 12483 -g -- sleep 30

参数说明:

  • `-F 99`:采样频率99Hz,每秒采样99次。不要设成1000Hz或更高,采样本身也会消耗CPU,会干扰结果。
  • `-p 12483`:只采样这个进程。
  • `-g`:记录调用栈。没有这个参数就只能看到函数名,看不到是谁调的。
  • `-- sleep 30`:采样30秒。

采样结束后用`perf report`查看交互式报告:

perf report --stdio

输出(截取关键部分):

# Overhead  Command   Object   Symbol

........ ....... ...... ......

48.23% java payment [j] com.example.payment.service.OrderValidator.validate 31.67% java payment [j] com.example.payment.service.OrderValidator.loopOrders 12.45% java payment [j] java.util.regex.Pattern$GroupHead.match 5.89% java payment [j] java.lang.String.substring

`OrderValidator.validate`占了48%的CPU时间,加上它上层的`loopOrders`,这两个函数加起来占了将近80%。问题函数已经锁死了。

第五层:生成火焰图

`perf report`的文字报告能看,但火焰图更直观。用Brendan Gregg的FlameGraph工具:

git clone https://github.com/brendangregg/FlameGraph.git /opt/FlameGraph

把perf数据转成火焰图兼容格式

perf script -i perf.data/opt/FlameGraph/stackcollapse-perf.pl > /tmp/out.perf-folded

生成火焰图SVG

/opt/FlameGraph/flamegraph.pl /tmp/out.perf-folded > /tmp/flamegraph.svg

把SVG下载到本地浏览器打开,你会看到一张漂亮的调用栈图。图中越宽的部分说明CPU花的时间越多。在我的案例中,火焰图的主体就是`OrderValidator.validate`和`loopOrders`那一整块,像一面墙一样矗在那里,非常显眼。

用火焰图的好处是能一眼看到整个调用链的热点分布。如果问题不在一个函数而是在多个分散的热点上,文字报告很难看出来,火焰图一扫就知道。

第六层:定位根因——一段死循环代码

拿着函数名和行号找到开发,拉出`OrderValidator.java`第63行到第90行左右的代码:

public void loopOrders(List orders) {

for (int i = 0; i < orders.size(); i++) { // 第63行 Order order = orders.get(i); validate(order); } }

public void validate(Order order) { int retries = 0; while (retries < MAX_RETRIES) { // 第80行 if (checkValid(order)) { break; } retries++; }

// 第87行附近——问题在这里 String pattern = buildPattern(order.getItems()); // 每次都重新编译正则 Pattern.compile(pattern).matcher(order.getContent()).matches(); }

问题有两个:

其一,正则表达式重复编译。 `Pattern.compile()`在循环里被反复调用,而且`buildPattern`根据订单内容动态生成正则——这意味着每次循环编译的正则都不一样,无法被编译缓存复用。正则编译是CPU密集操作,尤其是包含复杂量词和回溯的模式时,开销巨大。 其二,`while (retries < MAX_RETRIES)`可能死循环。 看`checkValid`方法的实现:
private boolean checkValid(Order order) {

if (order.getStatus() == null) { return false; // 如果状态永远为null,这里永远返回false } return order.getStatus().equals("VALID"); }

如果订单数据的`status`字段因为某种原因(比如上游数据问题)一直为null,`checkValid`永远返回false,retries永远递增但不退出循环——因为`MAX_RETRIES`被设成了`Integer.MAX_VALUE`。这就是一个伪装成有限循环的死循环。

修复方案

开发做了两处改动:

public void validate(Order order) {

// 修复1:限制重试次数为合理值 int retries = 0; int maxRetries = 5; while (retries < maxRetries) { if (checkValid(order)) { break; } retries++; }

// 修复2:预编译正则,使用缓存 String pattern = buildPattern(order.getItems()); Matcher matcher = PATTERN_CACHE.computeIfAbsent(pattern, Pattern::compile) .matcher(order.getContent()); boolean isValid = matcher.matches(); }

加了静态的`PATTERN_CACHE`(`ConcurrentHashMap`),正则只编译一次。同时把`MAX_RETRIES`从`Integer.MAX_VALUE`改成了5,加上了null check。

上线后观察,CPU使用率从100%降到了8%的正常水平。问题彻底解决。

非Java进程的排查思路

如果你遇到的不是Java进程而是C/C++、Go或Python写的服务,排查逻辑一样,只是工具不同:

C/C++: 用`perf`或`gprof`采样,用`gdb` attach到进程查看调用栈:
gdb -p 

(gdb) thread apply all bt

Go: Go自带pprof,直接看:
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.profile

go tool pprof -http=:9090 cpu.profile

浏览器打开后自动生成火焰图,支持交互式缩放和函数级钻取,比原生perf还方便。

Python: 用`py-spy`,不需要改代码也不需要重启进程:
pip install py-spy

py-spy top -p # 实时看函数级CPU分布 py-spy dump -p # 导出当前所有线程的调用栈 py-spy record -o profile.svg -p --duration 30 # 生成火焰图

一张排查流程图

总结一下完整的排查链路:

CPU 100%

│ ├─ top -c -o %CPU → 定位PID │ │ │ ├─ top -H -p PID → 定位线程TID │ │ │ │ │ └─ printf "%x" TID → 十六进制 → jstack/gstack/attach │ │ │ └─ perf record -F 99 -p PID -g -- sleep 30 │ │

│ └─ perf scriptstackcollapseflamegraph.pl → 火焰图

│ └─ strace -cp PID → 看系统调用分布(判断是计算密集还是IO密集)

`strace -cp PID`是一个经常被忽视但非常实用的命令。它会统计进程各系统调用的次数和耗时:

strace -cp 12483

输出类似:

% time   seconds  usecs/call  calls    errors syscall

48.23 0.240000 3 80000 read 31.67 0.158000 2 79000 write 12.45 0.062000 8 7750 futex 5.89 0.029000 15 1933 open

如果大部分时间花在`read`/`write`上,说明是IO密集型,CPU 100%是因为内核在做大量的数据拷贝(比如磁盘IO、网络IO)。如果大部分时间花在用户态且系统调用很少,那就是纯计算问题,跟上面案例一样。

最后提醒一句:`perf`需要root权限才能看到完整的内核符号。如果你用普通用户跑`perf record`,可能看到的全是`[unknown]`。用`sudo perf record`或者配置`kernel.perf_event_paranoid`参数:

sysctl -w kernel.perf_event_paranoid=1

CPU 100%这种问题看着吓人,但只要按流程走,一般30分钟内就能定位到根因。怕的不是排查过程复杂,怕的是一上来就瞎操作——杀进程、重启服务、升级版本——把现场破坏了,再想找原因就只能靠猜了。


【放心,我们兜底】

不管你是自己尝试修复,还是需要专业人员上门,易云城IT服务都给你托底。修不好不收费,修好了质保期内随时找我。

📞 服务热线:13708730161 💬 微信:eyc1689 📧 邮箱:service@eycit.com 🌐 https://www.eycit.com

您身边的IT专家。

上一篇
Nginx 502 Bad Gateway的7种死法和排...
下一篇
Redis内存优化实战:线上从8GB降到2GB的调优之路...