一、凌晨两点,手机震了
凌晨 2:47,手机震了一下。
监控群里弹出一条红色告警:
⚠️ 支付网关集群 CPU 持续 100%,订单创建成功率暴跌至 12.3%
值班工程师的第一反应是 SSH 上去敲了个 top——
%Cpu(s): 98.5 us, 1.2 sy, 0.0 ni, 0.3 id
PID USER %CPU COMMAND
24789 app 774% java
774%。
8 核服务器,正常上限 800%,意味着这个 Java 进程几乎吃光了所有 CPU 核心。
此时距离下一波早高峰支付还有 5 个小时。摆在面前的只有两条路:
- A. 重启了事——眼前能止血,但病因没找到,后天照样崩
- B. 趁着故障还在,现场排查——多花 5 分钟,但能一劳永逸
他选了 B。接下来的排查过程,就是这篇文章要讲的内容。
二、先搞清楚一件事:CPU 高 ≠ 一定是 CPU 的锅
很多人一看到 CPU 99% 就慌了,其实 top 里那行 %Cpu 细分了四种模式,搞清楚到底是哪种"高",方向才不会错:
| 指标 | 含义 | 一句话排查方向 |
|---|---|---|
| us | 用户态——你的应用在跑 | 找具体进程/线程/代码 |
| sy | 内核态——系统在跑 | 频繁系统调用/锁竞争 |
| wa | I/O 等待——CPU 在等磁盘 | 这不是 CPU 的锅,去看磁盘 |
| si | 软中断——网络包处理在烧 CPU | 看网络流量/是否被攻击 |
💡 如果 CPU 只有 10%-20% 但 load 飙到 200+,说明大量进程卡在 I/O 等待上,用
dmesg | tail看看是不是磁盘或 NFS 出问题了。
这篇文章主要讲 us 高的情况——因为这是生产环境 80% 以上 CPU 故事的起因。
三、5 分钟定位四步法(建议收藏)
这四步是我从几十次线上故障里提炼出来的标准流程,不管你是 Java、Python 还是 C 程序,前两步都通用。
Step 1:找到吃 CPU 的进程
top
进入后按 P 键(大写),按 CPU 使用率排序。找到 %CPU 最高的那行,记下 PID。
💡 也可以用一行命令直接搞定:
ps aux --sort=-%cpu | head -5
Step 2:找到吃 CPU 的线程
进程只是个"容器",真正烧 CPU 的是里面的某个线程。
top -Hp 24789
同样按 P 键排序,找到 %CPU 最高的线程 TID。
PID USER %CPU COMMAND
24812 app 95.5% java
💡 一步到位的替代命令:
ps H -eo pid,tid,%cpu --sort=-%cpu | head -5
Step 3:把线程 ID 转成十六进制
Linux 用十进制显示线程 ID,但 Java 堆栈里用的是十六进制,这一步必不可少。
printf "%x\n" 24812
输出 60ec,记下 0x60ec。
Step 4:定位到具体代码行
jstack 24789 | grep "0x60ec" -A 20
输出示例:
"http-nio-8080-exec-6" #25 prio=5
at com.xxx.OrderService.getOrder(OrderService.java:146)
at com.xxx.OrderController.get(OrderController.java:486)
到这里,你已经从"CPU 高了"缩小到了"某行代码有问题"。
剩下的就是让开发同事去看这行代码——是死循环、全量查询、还是锁竞争,代码层面一目了然。
💡 如果服务器上没有
jstack,可以用kill -3 <PID>生成线程 dump 到标准输出,效果一样。
四、回到那个凌晨:银行事故的完整复盘
前半部分讲了排查方法,现在用那晚的真实事故,把四步法串一遍。
事故背景:2024 年 6 月,广州某股份制银行核心支付网关集群,3 台 Pod 同时 CPU 100%。
影响范围:
- 支付成功率从 99.98% 跌到 12.3%
- 平均延迟从 320ms 飙升到 8.4 秒
- Kafka 消费积压暴涨 4200 倍
- 17 分钟内约 14.7 万笔订单创建失败
排查过程
值班工程师按照四步法操作:
Step 1 — top -c → 发现 Java 进程 PID 24789,CPU 774%
Step 2 — top -Hp 24789 → 多个线程 CPU 95%+,不是一个线程的问题
Step 3 — jstack 24789 → 抓取全量线程堆栈
Step 4 — 分析堆栈后发现:
- 应用日志高频出现
java.lang.OutOfMemoryError: Metaspace - JVM 启动参数未配置
-XX:MaxMetaspaceSize - Spring Cloud Gateway 的路由规则热更新持续膨胀元空间
- 元空间溢出 → Liveness Probe 失败 → K8s 滚动重启 → 重启风暴加剧雪崩
看到了吗?这次 CPU 高的根因不是"计算过载",而是"内存泄漏导致的连锁反应"。 CPU 只是表面症状,内存才是真正的病因。
修复过程
# 1. 紧急扩容 + 配置 Metaspace 上限
kubectl patch configmap payment-gw-jvm-config -n payment-gw \
--patch '{"data":{"JVM_OPTS":"-XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=256m"}}'
# 2. 隔离故障 Pod
kubectl drain pod/payment-gw-7f9c4b5d8-2xqz9 \
-n payment-gw --ignore-daemonsets --force
新 Pod 启动后,Metaspace 稳定在 220-380 MB,故障解除。
事后感悟
如果值班工程师当时选了"重启了事":
- ❌ 第二天高峰期同样的问题会再次出现
- ❌ 反复重启被认定为"临时规避",绩效考核受挫
- ❌ 根因分析被无限推迟,团队对问题本质始终模糊
但他完成了完整的排查流程,换班前提交了一份故障报告,第二天早会直接给出了根因和修复方案。
这,才是运维工程师该有的样子。
五、CPU 飙高的 5 大常见根因(速查表)
工具告诉你"哪里"出了问题,根因分析告诉你"为什么"。我把最常见的五种原因整理成了一张表,建议截图保存:
| 根因 | 特征 | 快速定位命令 |
|---|---|---|
| 🔴 死循环 | 单线程 CPU 100%,其他线程正常 | jstack <PID> | grep -A 20 "0x十六进制" 看循环逻辑 |
| 🟠 频繁 Full GC | FGC 每分钟几十次,CPU 在 GC 线程上烧 | jstat -gcutil <PID> 1000 观察 FGC 列 |
| 🟡 线程过多 | vmstat 显示 cs 每秒 10 万+ | vmstat 1 看上下文切换 |
| 🟢 第三方 C 库 | jstack 看不到有用的堆栈信息 | perf top -p <PID> 看底层函数调用 |
| 🔵 消息积压 | CPU 随消息量增长线性飙升 | 检查 MQ 配置的确认机制是否为"自动确认" |
💡 补充一个避坑点:如果
jstack和perf都定位不了,问题可能出在 JNI 层或底层 C 库。某金融客户的 APISIX 网关就遇到过——perf只能看到pkey_rsa_decrypt占了 44.8% CPU,Java 侧完全无感。最后是用 async-profiler 的全栈动态追踪才定位到是自定义插件频繁调用 RSA 解密。常规工具在混合语言栈面前有时会"失明",记得准备备选方案。
六、一张图记住全流程
把今天的排查流程浓缩成一张决策图,下次遇到 CPU 告警直接照着走:
CPU 告警 → top 看 us / sy / wa / si
│
├── us 高(最常见)
│ → Step 1: top / ps 找进程 PID
│ → Step 2: top -Hp 找线程 TID
│ → Step 3: printf 转十六进制
│ → Step 4: jstack 定位代码行
│
├── sy 高
│ → strace -p <PID> -c 找系统调用热点
│
├── wa 高
│ → iostat -x 1 → 这不是 CPU 的锅,去看磁盘
│
└── si 高
→ sar -n DEV 1 → 检查网络流量/是否被攻击
把这四步打印出来贴在工位上——下次凌晨告警响的时候,你不会慌。
七、写在最后
排查 CPU 飙高,本质上是一个不断缩小范围的过程:
进程 → 线程 → 代码行 → 业务逻辑
每缩小一步,离真相就更近一步。
记住这句话——
"先定位,后操作"。重启是最后的手段,不是第一选择。
📌 下期预告:《CPU 排查进阶:用火焰图一眼看穿性能瓶颈》 关注不迷路,下一篇教你用可视化方式定位性能热点,比 jstack 更直观。
我是一名国有银行数据中心运维工程师,每周分享运维实战笔记。
关注「云间豹变」回复【命令手册】获取Linux排查命令速查表。