CPU 问题定位:从 top 到火焰图
CPU 跑满了,系统卡成狗,这是咱们排查性能问题最常碰到的场景。我个人习惯先问三个问题:哪个进程在吃 CPU?内核态还是用户态在忙?热点函数到底是什么?
这一节,我就带你把这三件事干明白。工具不复杂——top、htop、perf,再加一张火焰图,这套组合拳打下来,基本上没有搞不定的 CPU 问题。
先看表象:top 与 htop
我经常看到有人打开 top 只看一眼 %CPU 列就完事了,这其实不够。你想想看,很多时候高 CPU 是内核态的驱动或者软中断在作祟,单看进程态根本发现不了。
1,看每个 CPU 核心的负载是否均衡。如果某个核心跑满 100%,其他核心空闲,基本可以判断是单线程瓶颈或者中断亲缘性问题。
具体用法不啰嗦,直接看命令:
top - 10:23:45 up 3 days, 2:15, 2 users, load average: 8.45, 7.32, 6.18
Tasks: 312 total, 1 running, 311 sleeping, 0 stopped, 0 zombie
%Cpu(s): 35.2 us, 12.8 sy, 0.0 ni, 48.6 id, 2.1 wa, 0.0 hi, 1.3 si, 0.0 st
KiB Mem : 32767616 total, 4231424 free, 12103296 used, 16432896 buff/cache
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5678 nobody 20 0 2621748 349684 16544 S 87.3 1.1 123:45.67 beam.smp
1234 root 20 0 103652 4520 3188 S 12.5 0.0 2:34.56 ksoftirqd/0
这里要注意两个关键点:us(用户态)和 sy(内核态)的比例。如果 sy 超过 30%,那大概率是系统调用太频繁或者内核驱动有性能问题。我遇到过好几次,业务方说是应用层代码慢,结果一看 sy 跑到 40% 多,最后定位到是网卡多队列没开。
htop 比 top 好看一点,操作也更顺手。我个人偏爱用它来排序和树形查看进程关系。htop 里按 F6 可以选排序字段,很多新手不知道这个功能。
- 看
load average是否超过 CPU 核数——超过说明有排队 - 看每个 CPU 核心的使用率是否均匀——不均可能绑定问题
- 看
wa(等待 I/O)是否偏高——CPU 在等磁盘,别冤枉应用 - 看
hi和si(硬/软中断)是否异常——驱动或网络可能有问题
锁定热点:perf 采样
top 只能告诉你哪个进程在忙,但不知道忙在哪。这时候就轮到 perf 上场了。perf 的原理很简单——每隔固定时间采一次 CPU 的 PC(程序计数器),统计每个函数被采样到的次数,次数越多说明它吃 CPU 越多。
基础命令就两个,一个采样,一个看报告:
# 采样 30 秒,采样频率 99 Hz(每秒 99 次)
perf record -F 99 -p 5678 -g -- sleep 30
# 生成报告并显示调用拓扑
perf report -g graph
这里 -g 表示采集调用栈,没有它你就只能看到叶子函数,看不出是谁调了谁。-F 99 是我常用的频率,别设太高(比如 1000+),否则 perf 本身会引入不小的开销。
/sys/kernel/debug 和 /proc/kcore,perf 会报权限错误。我一般会在宿主机上直接采样容器进程的 PID。
perf report 的输出大概长这样:
Samples: 2K of event 'cpu-clock', Event count (approx.): 1980000
Children Self Command Shared Object Symbol
+ 67.32% 0.00% beam.smp beam.smp [.] process_main
+ 65.10% 0.12% beam.smp beam.smp [.] sched_scan
+ 54.87% 0.03% beam.smp beam.smp [.] sched_sieve_run
+ 31.20% 15.40% beam.smp [kernel.kallsyms] [k] _raw_spin_lock
注意 Self 和 Children 的区别。Self 是当前函数直接消耗的 CPU,Children 包含了它调用的子孙函数。如果一个函数 Self 很低但 Children 很高,说明它是调度者,真正热的地方在它下面。
深度分析:perf 的进阶玩法
光看一眼报告还不够。有些场景下,你需要区分用户态和内核态的调用,或者按模块聚合看看平均 CPI(每指令周期数)。
我个人常用的几个 perf 子命令:
| 子命令 | 用途 | 示例 |
|---|---|---|
perf stat |
统计全局性能计数器(周期、指令数、缓存命中率等) | perf stat -p PID sleep 5 |
perf top |
实时显示正在消耗 CPU 的函数(类似 top 但按函数粒度) | perf top -p PID |
perf record -e |
指定事件采样,不只是 CPU 时钟 | perf record -e cache-misses -g -a sleep 10 |
perf script |
输出原始采样点文本,供外部工具进一步分析 | perf script > out.perf |
举个例子。假设你的程序跑得很慢但 CPU 不高,这时候采 CPU 时钟就没用。换成 perf stat 看 cache-misses 和 branch-misses,往往能直接发现问题——我遇到过很多次,代码写了个巨大的哈希表,cache 命中率掉到 40% 以下,全在等内存读取。
perf stat -e task-clock,cycles,instructions,cache-references,cache-misses,branch-misses 一组全看,然后算 CPI = cycles / instructions。通常 CPI > 2 说明内存访问有瓶颈,< 0.5 说明指令级并行很好。
可视化瓶颈:火焰图生成
perf report 的文本报告虽然能用,但面对深层调用链时,读起来真的很费眼。火焰图的好处是把调用栈的纵向深度和横向宽度变成一张图,一眼就能看出哪个分支最热。
怎么生成?其实就三步:采样→解析→画图。
# 第一步:采样,一定要带 -g(调用栈)
perf record -F 99 -p 5678 -g -- sleep 60
# 第二步:用 perf script 转成文本,然后用 FlameGraph 工具栈折叠
perf script | ./stackcollapse-perf.pl > collapsed.folded
# 第三步:生成 svg 火焰图
./flamegraph.pl collapsed.folded > cpu_flame.svg
解释一下:stackcollapse-perf.pl 把每个采样点的完整调用栈压成一行,flamegraph.pl 再根据这些行画图。火焰图里每个矩形代表一个函数,宽度和采样次数成正比,颜色是随机的没啥特殊含义。
- 找最宽的「起火的尾巴」——就是最宽的矩形,它是真正的热点函数
- 看调用链的深度——如果某个分支特别深,说明调用层次有问题,可能有冗余
- 看是否有大量
[unknown]或[kernel.kallsyms]区域——说明内核态占用高,应用层可能背锅
我记得有一次排查一个 Java 服务,业务方说是自己的代码慢,我一看火焰图,__GI___libc_malloc 占了将近 40% 的宽度,这说明频繁的堆分配和释放是真正的瓶颈。后来建议他们用对象池,问题直接解决。
perf script 输出的栈符号解析不了,原因是 perf 需要 --call-graph dwarf 参数来获取 DWARF 格式的栈帧。如果默认的 fp(帧指针)找不到调用栈,可以试试这个:
perf record -F 99 --call-graph dwarf -p PID -- sleep 30
知识结构图
整体排障流程,我整理成了一张图,方便你对照:
综合案例分析
讲个真实场景吧。有次线上服务 CPU 持续 90%+,运维先上了 top:
top - 14:22:01 up 12 days, 3:10, 1 user, load average: 72.45, 65.12, 58.33
%Cpu(s): 30.5 us, 55.2 sy, 0.0 ni, 10.1 id, 3.2 wa, 0.0 hi, 1.0 si, 0.0 st
注意看:内核态(sy)55.2%,用户态才 30.5%。这说明问题不在应用层代码。用 perf 采样一看:
perf record -F 99 -a -g -- sleep 10
perf report -g graph
# 结果:_raw_spin_lock 占 41%,__do_softirq 占 22%
内核自旋锁和软中断占了大部分 CPU。再跟踪调用链发现是网卡驱动的 napi_poll 里频繁竞争锁。最后定位到是该机器的 RPS (Receive Packet Steering) 配置不当,导致多个 CPU 核竞争同一把锁。
修复方案很简单:调整 /proc/irq/XX/smp_affinity,让每个队列绑定到不同 CPU 核。调整后 top 里的 sy 降到了 12%,问题解决。
- top 阶段区分 us/sy,可以快速缩小问题范围
- perf report 的 Self 列才是真正的 CPU 消耗点
- 火焰图是说服团队和业务方的最直观方式——一张图胜过千言万语
- 每次改完配置或代码,务必重新采一次对比,别靠感觉说「好像好了」