系统与运维

Linux CPU 100% 排查:从 top 到线程栈的最短路径

CPU 打满时「重启解决一切」是最贵的选择。本文给出一条可复用的定位路径:分清消耗类型、锁定线程、拿到它在执行的代码,再对症处理——Java、Python、Go 场景各有对应工具。

作者:巧匠团队·8 分钟阅读·更新于 2026-08-30

第一步:分清是 user、system 还是 iowait

先看 `top` 顶部的 CPU 行:us 高是应用代码在烧 CPU(业务计算、死循环、GC);sy 高是内核态开销(系统调用风暴、锁竞争、上下文切换);wa 高其实不是 CPU 忙,而是磁盘 IO 慢拖高了 load——方向完全不同,用错工具会白忙一场。

同时看 load average 对比核数:`nproc` 给出核数,load 持续高于核数才说明真的排队了。

top        # 看 %Cpu(s) 行的 us / sy / wa
nproc      # 核数,用于对照 load average

第二步:锁定进程,再下钻到线程

`top` 按 P(按 CPU 排序)找到吃 CPU 的进程后,多数人止步于此。但真正的热点往往只在一个进程内的个别线程——用 `top -H -p <pid>` 查看该进程的线程视图,记下烧 CPU 的线程 ID。

更省事的方式是 `pidstat -t -p <pid> 1`,逐秒输出各线程的 CPU 占用,且不占终端交互。

top -H -p <pid>          # 该进程的线程级视图
pidstat -t -p <pid> 1    # 逐秒采样各线程 CPU

第三步:拿到线程正在执行的代码

Java:把线程号转十六进制,在 `jstack <pid>` 输出里搜 nid=0x<hex>,直接看到栈帧;连续抓两三次,栈顶不变的帧就是热点。Python:用 py-spy dump --pid 拿调用栈,不用改代码也不用重启进程。Go:程序内置 pprof 时直接抓 CPU profile。通用兜底:`perf top -p <pid>` 看内核态热点符号。

printf '%x\n' <tid>          # 线程号转十六进制
jstack <pid> | grep -A 20 nid=0x<hex>
py-spy dump --pid <pid>     # Python 进程调用栈
perf top -p <pid>           # 通用内核态热点

对着常见根因清单比对

① 死循环或条件永远不满足的 while;② 正则灾难性回溯(嵌套量词匹配长文本);③ 频繁 Young GC / Full GC(内存不足触发回收风暴,本质是内存问题表现为 CPU 问题);④ 加解密、压缩、序列化在主线程做重活;⑤ 日志同步刷盘 + 高 QPS。栈里看到什么,基本就能对号入座。

jstat -gcutil <pid> 1000   # 每秒看 GC 频率,判断是否 GC 风暴

应急止血与根治要分开

线上 CPU 100% 时的止血动作:隔离流量(摘除负载均衡)、限流、或按情况 `kill` 单个失控进程交由 supervisor 拉起。注意 kill 前先把证据抓全——线程栈、GC 日志——否则重启后线索全丢,问题变成「玄学偶发」。

根治则要回到代码:修循环条件、改正则、加缓存、把重活移出主线程、调大堆或修内存泄漏。若根因是流量增长,则是容量规划问题,扩容比改代码更诚实。

jstack <pid> > /tmp/stack-$(date +%s).txt   # 先留证据
kill <pid>                                   # 再重启(supervisor 会拉起)

一页速查:定位链路

top 看 us/sy/wa 分方向 → top -H / pidstat -t 锁线程 → jstack / py-spy / perf 拿栈 → 对照五类根因 → 留证据后止血 → 回代码根治。把这条链路练熟,CPU 问题的平均定位时间能压到十分钟以内。

top → top -H -p <pid> → jstack/py-spy → 定位 → 止血 → 根治