第一步:分清是 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 → 定位 → 止血 → 根治