第三章 CPU性能分析:从指标到根因
CPU性能分析,说白了就是搞清楚一个问题:CPU到底在忙什么?
我刚开始做性能优化那会儿,总觉得CPU利用率高就是坏事。后来被现实狠狠教育了一顿——利用率高不一定是问题,利用率低反而可能藏着大坑。今天咱们就把CPU这块掰开揉碎了讲清楚。
3.1 CPU利用率:别被数字骗了
CPU利用率,大家最熟悉的指标。但你真的看懂了吗?
我见过太多人盯着top命令里的%CPU看,然后得出结论:「CPU满了,加机器吧」。嗯,有时候确实是这样,但更多时候,问题没那么简单。
3.1.1 用户态 vs 内核态 vs 等待I/O
CPU利用率其实分三块:
- 用户态(us):跑应用程序代码的时间
- 内核态(sy):跑系统调用、内核代码的时间
- 等待I/O(wa):CPU闲着,等磁盘或网络数据
我有个习惯:先看wa,再看sy,最后看us。为什么?
举个例子。有一次线上服务响应变慢,top一看CPU利用率才30%。新人同事说「CPU没满,不是性能问题」。我让他看看wa——好家伙,wa占了25%。说白了,CPU大部分时间在等磁盘I/O,应用当然慢。
核心判断逻辑:
- us高 → 应用层瓶颈,优化代码或加CPU
- sy高 → 系统调用频繁,检查锁、中断、上下文切换
- wa高 → I/O瓶颈,换SSD、加缓存、优化读写模式
3.1.2 工具怎么用
我最常用的三板斧:
# 第一板斧:实时看
top - 1
# 第二板斧:按CPU核看
mpstat -P ALL 1
# 第三板斧:历史趋势
sar -u 1 10
你想想看,top看的是全局,mpstat能发现单核打满的情况,sar帮你分析历史趋势。三个配合着用,基本不会漏掉问题。
我的小技巧: 用top的时候,按1键展开每个CPU核的利用率。我曾经遇到过16核的机器,15个核空闲,1个核跑满100%。不看单核数据,你根本发现不了。
3.2 上下文切换:看不见的开销
上下文切换,说白了就是CPU从一个任务切换到另一个任务。每次切换都要保存寄存器、刷新TLB、加载新进程的状态——这些都是开销。
我记得有一次排查性能问题,CPU利用率不高,但服务就是吞吐量上不去。用vmstat一看,cs(context switch)列的数字高得离谱,每秒几十万次。这就是典型的「CPU在忙着切换,没空干活」。
3.2.1 怎么判断上下文切换是否过高
| 场景 | 正常范围 | 警戒线 |
|---|---|---|
| Web服务器 | 几千~几万/秒 | > 5万/秒 |
| 数据库 | 几百~几千/秒 | > 1万/秒 |
| 计算密集型 | 几百/秒 | > 2000/秒 |
当然,这个范围不是的。我建议你先建立基线——在系统正常运行时记录一次数据,以后对比着看。
3.2.2 排查工具
# 看上下文切换次数
vmstat 1
# 看自愿/非自愿切换
pidstat -w 1
# 追踪具体进程
perf stat -e context-switches -p PID
这里有个关键点:自愿切换 vs 非自愿切换。
- 自愿切换:进程主动让出CPU(比如等I/O、等锁)
- 非自愿切换:时间片到了,被系统强制切换
我曾经遇到一个案例,pidstat显示某个进程的非自愿切换特别高。一看,原来是这个进程的CPU亲和性没设置好,被内核在不同核之间来回调度。设置taskset绑定后,性能提升了30%。
注意: 上下文切换不是越低越好。如果切换次数太少,说明系统可能空闲,或者有进程在「霸占」CPU。关键是要看切换的原因和切换的频率是否合理。
3.3 运行队列:CPU的「待办清单」
运行队列(run queue)表示有多少进程在等待CPU。这个指标比CPU利用率更能反映系统的真实负载。
为什么?你想想看,CPU利用率100%的时候,可能只有一个进程在跑,其他进程都在等I/O。这时候运行队列很短,系统其实不忙。反过来,CPU利用率只有50%,但运行队列里有10个进程在等——这才是真正的瓶颈。
3.3.1 怎么看运行队列
# 看运行队列长度
vmstat 1
# 看每个CPU的运行队列
sar -q 1 10
vmstat输出的r列就是运行队列长度。我个人的经验:r值持续大于CPU核数,说明CPU不够用了。
举个例子,4核的机器,r值长期在6~8之间。这意味着平均每个核有1.5~2个进程在排队。这时候加CPU核数或者优化代码,效果立竿见影。
判断公式:
r / CPU核数 > 1 → CPU瓶颈
r / CPU核数 > 2 → 严重瓶颈,需要立即处理
3.4 中断处理:别让硬件「吵」到CPU
中断,就是硬件设备「打断」CPU当前的工作,让它来处理自己的事情。网络包来了、磁盘读写完成了、鼠标动了一下——都会触发中断。
中断处理分两种:
- 硬中断:硬件直接触发,处理时间极短
- 软中断:硬中断的下半部分,可以延迟处理
我记得有一次,一台机器CPU利用率不高,但网络延迟特别大。用cat /proc/interrupts一看,所有网络中断都打在一个CPU核上。那个核的软中断占用率高达80%,其他核却闲着。
3.4.1 中断绑定
解决方案很简单:把中断分散到多个CPU核上。
# 查看中断分布
cat /proc/interrupts
# 设置中断亲和性(以eth0为例)
echo 3 > /proc/irq/$(grep eth0 /proc/interrupts | awk '{print $1}' | sed 's/://')/smp_affinity
这里echo 3表示把中断绑定到CPU 0和CPU 1(二进制11)。你可以根据实际情况调整。
我的建议: 高并发的网络服务,一定要做中断绑定。我曾经帮一个游戏后端团队优化,就这一步操作,网络吞吐量提升了40%。
3.5 CPU绑定与隔离:给关键任务「专车」
CPU绑定(affinity)和CPU隔离(isolation),说白了就是让某些CPU核只干特定的活。
3.5.1 CPU绑定
用taskset把进程绑定到指定CPU核上:
# 启动时绑定
taskset -c 0,1 ./myapp
# 运行时绑定
taskset -cp 0,1 PID
为什么要绑定?
- 减少缓存失效:进程一直在同一个核上跑,L1/L2缓存命中率高
- 避免上下文切换:不会被调度到其他核上
- 隔离干扰:不让其他进程来抢CPU
我做过一个测试:一个计算密集型的图像处理程序,绑定CPU后性能提升了15%。原因就是缓存命中率从60%升到了85%。
3.5.2 CPU隔离
CPU隔离更彻底——让某些CPU核完全脱离内核的调度系统,只运行指定的用户态程序。
# 内核启动参数,隔离CPU 2-3
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
隔离后的CPU核:
- 不会处理中断
- 不会被普通进程抢占
- 不会触发时钟中断(nohz_full)
这招在DPDK、实时计算、高频交易等场景特别有用。我参与过一个金融交易系统的优化,把核心交易线程放在隔离的CPU上,延迟抖动从毫秒级降到了微秒级。
注意: CPU隔离是把双刃剑。隔离的核越多,普通进程可用的核就越少。我建议先隔离1~2个核试试,观察效果再决定是否增加。
3.6 实战:一次完整的CPU性能分析
最后,我分享一个真实的排查过程。某天晚上,线上服务报警:响应时间从10ms飙升到500ms。
第一步:看全局
top - 1
CPU利用率只有40%,但wa占了30%。嗯,I/O有问题。
第二步:看上下文切换
vmstat 1
cs列每秒8万次,r列6(机器是4核)。CPU确实在忙,但忙的是切换。
第三步:看具体进程
pidstat -w 1
发现Java进程的非自愿切换特别高。进一步排查,这个进程没有设置CPU亲和性。
第四步:看中断
cat /proc/interrupts
网卡中断全部集中在CPU 0上。
解决方案:
- 用
taskset把Java进程绑定到CPU 1-3 - 把网卡中断分散到CPU 0和CPU 1
- 优化磁盘I/O(这是另一个故事了)
最终效果:响应时间降回15ms,吞吐量恢复。
总结一下我的分析流程:
- 先看CPU利用率(us/sy/wa)
- 再看上下文切换(自愿/非自愿)
- 然后看运行队列(r值)
- 最后看中断分布
- 根据问题选择绑定或隔离
记住,CPU性能分析不是看一个指标就下结论。要像侦探一样,把各个线索串起来。你想想看,如果我只看了CPU利用率40%就下结论「系统不忙」,那后面的问题就全漏掉了。
下一章,咱们聊聊内存。内存这块坑更多,我到时候分享几个「血泪史」。