一、现象与影响面:load 飙了,先别动手
CPU 打满的表象很统一:load 一路爬、请求变慢、ssh 敲字都有延迟,网关那边开始报 504。但表象之下至少有四种完全不同的病,用药完全不同,所以这篇文章的主线不是”答案清单”,而是一条从 top 到火焰图的定位路径——每一层淘汰一批可能,走到最底层时,真凶基本只剩一个函数。
反模式一:CPU 高就 kill 掉最忙的进程。那是毁现场。CPU 问题和 OOM 不同,现场是活的、可反复观测的(OOM 的现场在日志里,CPU 的现场就在进程身上),kill 之后它按原样再来一遍,你还是不知道它忙什么。先观测,后动手。
另有一类”假打满”要先排除:load 高但 CPU 其实闲着,那是 IO 等待或不可中断睡眠在垫高 load——这就是第一层要做的事。
二、第一层:top 的五行字,先分诊
在宿主机上打开 top,第一眼看 %Cpu(s) 那一行。五行字就是五个分诊台:
| 哪项高 | 含义 | 指向 | 下一跳 |
|---|---|---|---|
| us(用户态) | 业务代码在算 | 本文主线:正则、序列化、GC、业务循环 | 第三层进程定位,第四层火焰图 |
| sy(内核态) | 系统调用风暴、锁自旋、频繁上下文切换 | 锁竞争、频繁小 IO、内存频繁缺页 | vmstat 1 看 cs 列;perf 看内核符号;strace -c 数系统调用 |
| si(软中断) | 网络包处理 | 流量突增、扫描、DDoS | mpstat -P ALL 1 看是否单核打满;查网络流量 |
| wa(IO 等待) | CPU 在等磁盘,本身是闲的 | 磁盘瓶颈、swap 抖动 | 不是本文主线;先看 free -h 是否在 swap、iostat 查磁盘 |
| id(空闲)低 | 真·CPU 紧张 | 综合以上各项的占比 | 按占比最大的一项走 |
一个关键提醒:top 里 CPU 显示 100% 不等于”没救了”,要区分单核打满还是全核打满。两条命令:
# 宿主机上;mpstat 来自 sysstat 包,-P ALL 逐核报告
mpstat -P ALL 1
1. %usr 只在 CPU0 上 98%,其余核 %idle 90+ ← 单核打满:串行瓶颈
1. 全部核 %usr 60+ ← 全核打满:真·计算量大
1. 单核打满常见于:单线程正则回溯、锁让所有人排队、GC 单线程阶段
这层分完诊(us 高走主线,sy/si/wa 各奔各路),主线往下:找进程,找线程,最后找函数。每层正常就往下走。
三、第二层:进程与线程,”是谁在忙”
# 宿主机上;top 里按 P 键按 CPU 排序,锁定头名进程
top
1. 记下 PID,进入线程视图(-H 把线程当进程显示)
top -H -p 4837
1. 线程级才能区分"多线程各忙各的"和"一个线程忙死、其他围观"
1. 不进交互界面的等价物:线程级快照排序
ps -eLo pid,tid,pcpu,comm --sort=-pcpu | head -10
1. 连续观测(pidstat 同样来自 sysstat)
pidstat -u -t -p 4837 5
1. -t 显示线程级;%CPU 持续高的 TID 就是下一层 perf 的锚点
1. Java 专用衔接:线程号转十六进制,去 jstack 输出里搜 nid
printf "%x\n" 4837
1. 12e5 → jstack <pid> | grep -A 20 'nid=0x12e5',直接看到线程栈
反模式二:只看进程级 CPU。进程 300%、看起来很忙、无从下手——展开成线程往往发现是一个线程 99% 带 GN(GC 线程)或正则帧,另外两个核是 GC 在给它收拾残局。线程级视图是进程级和函数级之间必经的一站。
四、第三层:函数热点与火焰图
到这层的目标是把”某个线程很忙”精确成”哪段代码在烧 CPU”。主力工具是 perf(Linux 自带剖析器),图谱可视化用 Brendan Gregg 的 FlameGraph:
# 宿主机上;先交互式扫一眼热点,10 秒出直觉
perf top -p 4837
1. 若提示权限,见第五节容器注意事项
1. 确认值得深挖后,正式采样:99Hz 采 30 秒,-g 抓调用栈
perf record -F 99 -p 4837 -g -- sleep 30
1. 生成火焰图(FlameGraph 是一组 perl 脚本,git 拉下来即可)
git clone https://github.com/brendangregg/FlameGraph.git
perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > cpu.svg
1. 浏览器打开 svg:宽 = 耗时占比;找最宽的平台,点开看调用路径
火焰图读法三句话:越宽的框越耗时;塔尖最宽的平台就是热点函数;框的纵深是调用链,从塔尖往下读就是”谁在反复调它”。同屏对比正常与异常时刻的两张图,差异那一段几乎就是答案。
Java 服务别硬用 perf——JIT 编译后的栈,perf 默认看不到有意义的符号。用 async-profiler(启动脚本随版本可能是 profiler.sh 或 asprof,以项目 README 为准):
# 宿主机或容器内(见第五节);30 秒 CPU 采样,直接输出交互式 html 图
./profiler.sh -e cpu -d 30 -f /tmp/flame.html 4837
1. 对 JVM 内的帧(含 GC、JIT 代码)都有符号,省掉 perf 的符号麻烦
1. 老版本 JVM 想用 perf 的话,需开启 -XX:+PreserveFramePointer 才能采到 Java 栈
Node 服务的轻量路线:用 CPU profiler 生成 .cpuprofile,丢进 Chrome DevTools 的 Performance 面板直接看:
# 应用容器内;启动时开启 profiler,跑一段时间后取产物分析
node --cpu-prof server.js
1. 产物在当前目录生成的 *.cpuprofile,Chrome DevTools → Performance → Load profile
五、容器环境的三处不同
| 现象 | 原因 | 做法 |
|---|---|---|
| 容器内 top 显示的 CPU 总量是宿主机的 | /proc/stat 未按 namespace 隔离,容器看到的核数、总量是宿主视角 | 容器内百分比会”显得很低”(单核打满显示为 100/N),判断回到宿主机做 |
| 容器内 perf 报权限错误 | perf 事件受 kernel.perf_event_paranoid 与容器 capability 限制 | 最省事:在宿主机对容器进程直接 perf -p <宿主侧PID>——容器进程在宿主 pid 树里可见,无需进容器 |
| K8s 里想 exec 进容器执行工具 | kubectl exec 等价 docker exec,但工具镜像里通常没有 perf/asprof |
同样在宿主(node)上对目标进程采样;或用带工具的调试容器以特权挂载宿主 pid namespace |
一句话原则:采样在宿主机做,对象是容器进程的 PID——既绕开权限,又绕开镜像里没工具的尴尬。
六、根因清单:火焰图里常见的那几根宽柱子
| 根因 | 分诊线索 | 火焰图长相 | 修法方向 |
|---|---|---|---|
| 正则回溯 | us 高、常单核打满;输入变长后才发作 | 一整根柱子卡在 regex 的 exec/test/match 帧 | 拆分模式、加锚点、限制输入长度;换线性时间引擎(如 RE2 系);给匹配加超时 |
| JSON 深拷贝/大对象序列化 | us 高,随请求体变大而恶化 | 宽柱在 JSON.parse/stringify 或深拷贝工具函数 | 热路径去掉反复序列化;改增量/流式;大对象结构共享 |
| GC 风暴 | us 高但业务帧不算宽;内存同时在涨 | 大片 CollectGarbage / GC task 帧(Java/Node 皆然) | 真凶在内存分配速率,不在 CPU——转《OOM 与内存泄漏的定位》的思路查分配源 |
| 日志风暴 | us 与 sy 双高,磁盘写入量同步暴涨 | 宽柱在日志序列化 + write 系统调用 | 降级别、异步 appender、限流;日志该出生产事故时也该有配额 |
| 锁竞争 | sy 高、多核各自忙但吞吐上不去 | 内核态自旋帧宽,业务帧碎片化 | 缩小临界区;降低锁粒度;无锁结构或队列化 |
本站这台 1.8G 小机让这套路径多了一条经验:1.8G 机器上 GC 风暴与日志风暴的出场率远高于计算密集——内存越紧,”CPU 高”的第一嫌疑人越是内存问题的伪装。
七、预防:留一张正常的底片
火焰图最有价值的用法不是出事后再采,而是没出事时先采一张当基线:故障时再采一张,两张对比,多出来的宽柱就是增量。成本几乎为零——perf 或 asprof 低频采 30 秒不惊动服务。配套两条纪律:压测时顺手留图(等价于给未来存底片);load 告警阈值之外,给”us/sy/wa 占比”也设一条告警——占比突变比总量突变更早说话。
路线收拢:top 分诊(us/sy/si/wa)→ mpstat 分单核/全核 → 线程级锁定(top -H / pidstat -t)→ perf 或 asprof 出图 → 对照根因清单读图。顺序走完,”CPU 100%”会从一个报警变成一个函数名。
版本核实(以官方文档为准):FlameGraph 工具链见 Brendan Gregg 的 brendangregg/FlameGraph 仓库;async-profiler 已迁至独立组织 async-profiler/async-profiler,新版启动器为 asprof(取代旧 profiler.sh)。mpstat/pidstat 属 sysstat 包。均以各官方仓库与文档为准。