CPU 100% 本身不是问题,它是一个症状

凌晨两点,告警响了:某台机器 CPU 使用率 98%,持续十分钟。

我见过太多人的第一反应是三连:ssh 上去 → top 看一眼 → 重启服务。

重启确实管用。十分钟后一切正常,群里一片"解决了"。然后下周同一时间,它又响了。

因为重启什么都没解决,它只是把证据销毁了。

今天把一套能复用的排查流程写清楚。不依赖任何花哨工具,从 Linux 自带命令出发,最后再讲可选的增强手段。你不需要背下所有参数,只需要记住这条链:进程 → 线程 → 栈 → 代码行。

一、先说三件"千万别急着做"的事

1. 别急着重启。

至少先留三份东西:top -bn1 的快照、top -Hp <pid> 的线程快照、jstack(如果是 Java)。这几条命令都是秒级的,成本远低于一次无头绪的重启。

2. 别一上来就上 heavy 工具。

strace -p 挂到一个高并发进程上,可能直接把 QPS 打穿;arthas trace 不加条件过滤,同样会拖垮服务。观察工具的开销,必须远小于问题的代价。

3. 别只看"总使用率"。

top 第一行的 %Cpu(s) 里藏着整道题的答案,但很多人只盯着那个 98.0 us。请把这一行拆开看:

复制代码
%Cpu(s): 92.3 us,  3.1 sy,  0.0 ni,  2.8 id,  0.0 wa,  0.0 hi,  1.8 si,  0.0 st
  • us(用户态)高 → 你的代码在算东西。多半是死循环、正则回溯、序列化、加解密、大量对象创建导致 GC 狂飙。→ 走「线程级」排查。
  • sy(内核态)高 → 系统调用太多。典型是频繁上下文切换、锁争用走内核 futex、大量短连接、gettimeofday 满天飞。→ 走 pidstat -w / vmstat / strace -c。
  • wa(iowait)高 → 这不是 CPU 问题,是磁盘 IO 问题 。CPU 其实在 idle 等待,只是被记成"忙"。这时候去查 CPU 是白费功夫,该查 iotop、慢 SQL、日志刷盘。
  • si(软中断)高 → 网络包打断点。常见于高吞吐网络服务、网卡中断集中在一个核。→ 查 /proc/interrupts、RSS 中断亲和性。
  • st(steal)高 → 问题不在你。宿主机超卖了,你在和邻居抢物理核。这种时候你在容器里做什么都没用,得找运维/云厂商。这一条能帮你少背一半锅。

一句话:先看分类,再决定往哪条路走。这一步做对,后面省两小时。

二、主干流程:五步,从机器到代码行
第 1 步:定位进程
bash 复制代码
top -c            # -c 显示完整命令行,避免分不清哪个 java 是哪个服务

关注两个细节:

  • 按 P 按 CPU 排序,确认是不是你以为的那个进程;
  • 看 %MEM 和 TIME+ :TIME+ 累计时间异常陡增,说明这个进程长期在高负载跑,不是偶发。

如果确认是容器环境,注意 top 看到的可能是宿主机的核数视角,建议配合:

bash 复制代码
docker top <container>
# 或进容器内看 cgroup 限制
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us
第 2 步:定位到线程
bash 复制代码
top -Hp <pid>     # H=线程模式,p=指定进程

这会列出该进程下所有线程的 CPU 占用。记下最耗 CPU 的那几个 TID(线程 ID,十进制)。

顺带看一件事:是一个线程吃满,还是几十个线程一起高?

  • 单线程高 → 大概率是某个具体逻辑(死循环、单次重计算、正则)。好查。
  • 全体线程都高 → 通常是整体负载过高、GC、或者线程池被打满。这是另一类问题,别往单线程思路上走。
第 3 步:把 TID 转成十六进制
bash 复制代码
printf "%x\n" <tid>
# 例如 12345 → 3039

因为 JVM 栈里的 nid 是十六进制的。这一步忘转,后面 grep 不到任何东西------我踩过,希望你不用踩。

第 4 步:抓栈

Java:

bash 复制代码
jstack <pid> > /tmp/stack.$(date +%s).txt
grep -A 30 "0x3039" /tmp/stack.*.txt

拿到栈之后,重点不是看方法名,是看状态:

线程状态 含义 方向
RUNNABLE 真在跑(或在 native 里跑) 看业务方法,大概率就是它
BLOCKED 等 synchronized 锁 搜同一个 monitor 地址,找持有者
WAITING / TIMED_WAITING 等在 lock/park/sleep 通常不是 CPU 元凶,除非数量异常
停在 GCTaskThread / VM Thread GC 导致的 CPU 转 GC 日志分析,别在业务代码里找

如果不是 Java:

bash 复制代码
# 通用:采样调用图
perf top -p <pid>                     # 实时,看热点函数
perf record -g -p <pid> -- sleep 30 && perf report   # 离线,可留存证据

# 怀疑系统调用过多
strace -p <pid> -c -T               # -c 汇总计数,-T 显示每次耗时(开销大,短时取样)

# 怀疑上下文切换 / 调度
pidstat -p <pid> 1 5                # 每秒一次,看 %usr/%sys
pidstat -w -p <pid> 1 5             # 看 cswch/nvcswch(自愿/非自愿切换)

# 怀疑卡在内核(sy 高、用户态栈很浅)
cat /proc/<pid>/stack

容器 / K8s 额外一条 :如果 Pod 设置了 resources.limits.cpu,而 CPU 使用率看起来不高却明显变慢,很可能是 CPU throttling:

bash 复制代码
cat /sys/fs/cgroup/cpu/cpu.stat
# 看 nr_throttled 和 throttled_time ------ 一直在涨,说明被限额掐住了

这种情况的表现是"响应慢但 CPU 没满",很容易被误判成别的毛病。

第 5 步:验证,而不是直接改

找到可疑代码后,先在测试环境复现,或者用只读方式验证:

  • 加一个计数器/日志确认循环次数;
  • 用 arthas watch/trace 看入参和耗时(注意加条件表达式限制采样量);
  • 用正则/SQL 的 explain 单独跑那条语句。

**改之前问自己一句:这个解释能不能同时解释"什么时候开始"和"为什么是这个时间点"。**能对上的,才值得发版。很多"看起来很像"的根因,其实只是巧合。

三、四种高频根因,对照着查

这是我这几年遇到最多的四类,按出现频率排:

1. 死循环 / 空转(us 高,单线程)

经典形态:while (flag) 里没有 sleep、重试次数没上限、List.remove 时索引没动、BigDecimal 除不尽没设 scale 抛异常后被吞掉又重试。

特征:那个线程的栈永远停在同一个位置,TIME+ 疯涨。

2. 正则回溯(us 高,单线程,栈停在 regex)

输入稍微长一点、带嵌套的量词(比如 (.*?)+、([a-z]+)+$),复杂度指数级上升。一个请求就能吃满一个核。

对策:给输入长度加硬上限;用 possessive 量词或改写表达式;线上直接拒绝超长输入。

3. GC 导致的 CPU(us+sy 都高,jstack 里一堆 GC 线程)

表现很有欺骗性:业务线程其实都在等,CPU 却很高,RT 抖动剧烈。

判断:jstat -gcutil <pid> 1000 看 Full GC 频率;有 GC 日志就直接看日志。老年代反复接近 100% 然后骤降,就是典型的内存泄漏前兆------这时候换大堆只是续命。

4. 锁争用 / 线程池打满(sy 高,BLOCKED 多,或 nvcswch 高)

严格说这不完全是"CPU 不够",而是"线程太多在抢"。表现为线程数暴涨、pidstat -w 里非自愿切换极高、平均 RT 拉长。

对策:查线程池队列是不是无界的、有没有在 pool 里再调 pool(嵌套异步)、synchronized 是不是锁了太大的范围。

四、三个我亲手翻过的车

车一:在 prod 上 strace -p 跟了二十分钟。

当时想看看它在干什么系统调用,忘了 -c 汇总,直接裸挂。结果 QPS 掉了四成,差点把偶发卡顿变成真实事故。

现在我的规矩:任何带侵入性的工具,先限时、先采样、先在预发试一遍开销。

车二:只看了 us,忽略了 st。

一台云主机周期性卡顿,查了半天应用层,最后发现 st 长期 5%---15%。是宿主机超卖,邻居在挖矿。那天我学到的:先把"不是我的问题"排除掉,再去承认是自己的问题。

车三:找到了热点,改错了地方。

perf 显示某个 JSON 序列化方法占 40% CPU,于是我们换了更快的库,提升微乎其微。后来才发现它是被调用了三百万次,真正的根因是上层一个循环里重复序列化同一个对象。

教训:占比高 ≠ 该优化。占比 = 单次耗时 × 调用次数。两个因子都要看,否则你会去优化一个本身已经很快、只是被叫得太勤的方法。

五、一张可以贴在显示器旁边的速查卡
现象 第一反应命令 大概率方向
us 高,单线程 top -Hp → printf "%x" → jstack/perf 死循环、正则、重计算
us 高,全体线程 jstat -gcutil、看线程数 GC、整体过载
sy 高 pidstat -w、vmstat 1、strace -c(短时) 锁争用、上下文切换、系统调用风暴
wa 高 iotop、iostat -x 1、慢 SQL 磁盘 IO,不是 CPU 问题
si 高 cat /proc/interrupts 网络软中断、中断亲和性
st 高 --- 宿主机超卖,找运维
CPU 不满但慢 查 cgroup cpu.stat 的 nr_throttled K8s CPU limit 限流
线程数暴涨 `ps -eLf grep

最后补一句比技术更重要的话:

每次处理完,写一份不超过三百字的事故小结:现象 → 关键证据(附上那几条命令的输出)→ 根因 → 怎么发现早一点(加什么监控项)。

这份小结的价值在于:下一次凌晨两点告警响的时候,你不用再从头推理一遍。排查能力的复利,来自记录,不来自经历。


你遇到过最离谱的 CPU 100% 根因是什么?

欢迎在评论区说说(最好带上 top 那行 %Cpu(s) 的分布和你的最终结论)。我把自己那条"邻居在挖矿"的经历放上面了,期待有人比我更惨。也欢迎补充非 Java 技术栈的定位姿势------Go 的 pprof、Python 的 py-spy、Node 的 0x / clinic,这些我没展开,评论区补全比正文更有价值。

⚠️ 几点说明:

  1. 文中命令为通用思路,不同发行版、内核版本、JDK 版本、容器运行时(cgroup v1/v2)路径与输出可能有差异 ,请以你实际环境为准;cgroup v2 下 CPU stat 路径为 /sys/fs/cgroup/cpu.stat。
  2. 生产环境操作请遵守变更规范:优先在只读/低侵入模式下取证,涉及重启、扩缩容、参数调整请走审批与灰度流程。
  3. 本文不构成性能优化的唯一标准答案;同一现象在不同架构下可能有完全不同的根因,以实际观测数据为准,不要套用结论。
相关推荐
weixin_440401691 小时前
质朴的可视化绘图+pyecharts
开发语言·python·信息可视化·pyecharts
专业程序开发源1 小时前
springboot外卖系统94294-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
ss2731 小时前
AI全栈实战 | 3.3-01 Python OOP:__init__ 真的是构造函数吗,元类怎么让 Django Model 变魔法
开发语言·python·django
weixin_307779136 小时前
C++代码实现MATLAB中的dlarray函数功能
开发语言·c++·算法·matlab
一水鉴天9 小时前
映射、哈希表与哈斯图:计算机科学的三种基线 20261003(元宝)
开发语言·人工智能
Frank_refuel10 小时前
C++11之一场名为“搬家”的 C++ 之旅
开发语言·c++
Wang's Blog11 小时前
Java 项目实战: 外卖平台优化-Nginx配置文件结构与块层级
java·开发语言·nginx
2601_9620715711 小时前
类变量和全局变量的查找路径有什么区别?
开发语言·python
\光辉岁月/12 小时前
5.java-数组
java·开发语言