3. CPU 性能分析:CPU 使用率、上下文切换、中断、负载均衡,如何用 top 和 perf 定位 CPU 瓶颈
CPU 性能分析,说白了就是搞清楚你的程序到底在“忙什么”。
我见过太多人,一看 CPU 飙到 100% 就慌了,直接加机器。其实很多时候,CPU 根本没在干正事——它在忙着切换、忙着等锁、忙着处理中断。今天我们就来聊聊怎么用 top 和 perf 这两把刀,把 CPU 的底裤扒干净。
3.1 CPU 使用率:别只看 100%
CPU 使用率高,不一定代表性能好。你想想看,一个死循环也能把 CPU 跑满,但它不产生任何有效输出。
我个人习惯,拿到 top 输出后,先看这几个指标:
- us (user):用户态 CPU 时间。程序在跑你的业务逻辑,这是好事。
- sy (system):内核态 CPU 时间。程序在调用系统调用,比如读写文件、网络通信。
- wa (iowait):等待 I/O 完成。CPU 在等磁盘或网络,说明瓶颈可能在存储。
- hi (hardirq) 和 si (softirq):硬中断和软中断。这个后面细说。
- st (steal):被虚拟机偷走的 CPU。云上常见,说明宿主机超卖严重。
核心判断逻辑:
- us 高 + sy 低:程序在密集计算,考虑算法优化或并行化。
- sy 高 + us 低:系统调用频繁,可能是锁竞争或上下文切换过多。
- wa 高:磁盘或网络 I/O 是瓶颈,先排查存储。
- st 高:云服务器,找云厂商投诉去。
我在项目中遇到过一件事:一个 Java 服务 CPU 使用率 95%,但 us 只有 20%,sy 却占了 70%。一看 strace,发现程序每秒在疯狂调用 gettimeofday() 几百万次。嗯,这就是典型的“伪忙”——CPU 很累,但活没干多少。
3.2 上下文切换:看不见的杀手
上下文切换,就是 CPU 从一个线程/进程切换到另一个。每次切换都要保存和恢复寄存器、内存映射、缓存等。切换太频繁,CPU 就在“搬家”,而不是“干活”。
用 top 看上下文切换,需要按 1 键看每个 CPU 核的详情,或者直接看 /proc/stat。但我更推荐用 vmstat 或 pidstat -w:
# 每秒查看上下文切换次数
vmstat 1
# 查看具体进程的上下文切换
pidstat -w 1
输出里的 cswch/s(自愿切换)和 nvcswch/s(非自愿切换)是关键:
- 自愿切换:线程主动让出 CPU,比如等待锁、等待 I/O。数量多说明锁竞争严重或 I/O 等待多。
- 非自愿切换:线程的时间片用完了,被操作系统强制换下。数量多说明线程数太多,CPU 不够分。
避坑指南:我曾经优化过一个 Nginx 网关,发现非自愿切换高达 10 万次/秒。查了半天,发现 worker 进程数配了 64 个,但机器只有 4 核。改成 4 个 worker 后,切换降到 2000 次/秒,吞吐量翻了一倍。
3.3 中断:CPU 的“紧急任务”
中断是硬件或软件发给 CPU 的信号,告诉它“有急事,先停一下”。中断处理分两部分:
- 硬中断 (hardirq):硬件设备发来的,比如网卡收到数据包、磁盘完成读写。处理时间要极短。
- 软中断 (softirq):内核发起的,比如网络协议栈处理、定时器。可以稍慢一点,但也不能拖。
用 top 看中断,按 1 键看每个 CPU 的 hi 和 si 字段。如果某个 CPU 的 si 特别高,说明软中断都堆在一个核上了。
更细的排查用 /proc/interrupts:
cat /proc/interrupts | grep -E "CPU|eth"
这个文件会显示每个中断号、每个 CPU 上触发了多少次。如果某个网卡中断只落在 CPU0 上,而 CPU0 的 si 已经 80% 了,那就是典型的中断不均衡。
注意:中断绑核(IRQ affinity)是个双刃剑。绑好了能提升缓存命中率,绑不好反而让一个核累死。我建议先看 /proc/irq/<中断号>/smp_affinity,确认当前绑定情况。
3.4 负载均衡:CPU 核之间的“分活”艺术
负载均衡,就是操作系统把进程/线程分配到不同的 CPU 核上。理想情况是每个核的负载差不多。但现实往往是:一个核忙死,其他核闲死。
用 top 按 1 键,看每个核的 %idle(空闲率)。如果某个核的 idle 是 0%,另一个是 90%,那就是负载不均衡。
为什么会这样?常见原因有:
- 线程绑定:程序用
sched_setaffinity把线程绑死在某个核上。 - 中断绑定:网卡中断只落在某个核上,导致该核大量处理软中断。
- NUMA 架构:内存访问有远近之分,操作系统倾向于把线程留在同一个 NUMA 节点上。
排查工具:
# 查看进程的 CPU 亲和性
taskset -p <PID>
# 查看 NUMA 内存分配
numastat -p <PID>
我的经验:有一次线上 Redis 实例,CPU0 跑满 100%,其他核只有 20%。查了 /proc/interrupts,发现网卡中断全在 CPU0 上。用 irqbalance 服务重新分配后,所有核的负载都均衡到 60% 左右,Redis 的 P99 延迟从 5ms 降到了 1ms。
3.5 用 perf 深挖:到底谁在吃 CPU?
top 只能告诉你“谁在吃 CPU”,但 perf 能告诉你“CPU 在干什么”。
基本用法:
# 采样 10 秒,记录 CPU 周期事件
perf record -a -g -- sleep 10
# 生成火焰图数据
perf script > out.perf
# 用 FlameGraph 工具生成 SVG 火焰图
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > cpu.svg
火焰图怎么看?
- 宽度:代表 CPU 时间占比。越宽的函数,越值得优化。
- 颜色:红色通常是用户态,蓝色是内核态。
- 顶部:当前正在执行的函数。底部是调用链。
我常用的几个 perf 事件:
| 事件 | 说明 | 适用场景 |
|---|---|---|
cycles |
CPU 周期 | 通用 CPU 热点分析 |
instructions |
执行的指令数 | 看 CPI(每指令周期数) |
cache-misses |
缓存未命中 | 内存访问瓶颈 |
context-switches |
上下文切换 | 切换频繁的场景 |
小技巧:用 perf stat -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,cache-references,cache-misses ./your_program 可以一次性看到关键指标。我每次上线新版本前,都会跑一遍这个命令,对比基线数据。
3.6 实战排查流程
我把排查流程总结成一张图,方便你照着做:
这张图是我自己总结的。每次排查 CPU 问题,我就按这个流程走一遍,基本不会漏掉关键点。
3.7 避坑总结
最后,分享几个我踩过的坑:
- 别只看 CPU 使用率:80% 的 CPU 使用率 + 高 iowait,比 100% 使用率更可怕。因为 CPU 在等 I/O,程序实际是停滞的。
- 上下文切换不是越低越好:完全无切换意味着只有一个线程在跑,对于 I/O 密集型程序反而是坏事。关键是要看“自愿切换”和“非自愿切换”的比例。
- perf 采样时间要够:我习惯至少采样 30 秒,否则容易漏掉偶发性的热点。如果程序有周期性波动,可以采样 3 分钟。
- 火焰图要结合代码看:有一次我看到一个函数占了 40% 的 CPU,以为是热点。结果发现是日志打印函数——因为线上日志级别配成了 DEBUG。关掉后 CPU 直接降了 30%。
曾经我犯过的错:有一次线上告警 CPU 100%,我直接 perf record 抓了 10 秒,发现热点在 __do_softirq。我以为是网卡驱动问题,折腾了半天。后来才发现,是另一个团队在机器上跑了一个定时任务,每秒触发大量定时器软中断。所以,排查前先确认“最近谁动过这台机器”。
好了,CPU 性能分析的核心就这些。记住:工具只是辅助,关键是理解 CPU 到底在忙什么。用 top 看表象,用 perf 挖本质,用火焰图做决策。