第三章 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上。

解决方案:

  1. 用taskset把Java进程绑定到CPU 1-3
  2. 把网卡中断分散到CPU 0和CPU 1
  3. 优化磁盘I/O(这是另一个故事了)

最终效果:响应时间降回15ms,吞吐量恢复。

总结一下我的分析流程:

  1. 先看CPU利用率(us/sy/wa)
  2. 再看上下文切换(自愿/非自愿)
  3. 然后看运行队列(r值)
  4. 最后看中断分布
  5. 根据问题选择绑定或隔离

记住,CPU性能分析不是看一个指标就下结论。要像侦探一样,把各个线索串起来。你想想看,如果我只看了CPU利用率40%就下结论「系统不忙」,那后面的问题就全漏掉了。

下一章,咱们聊聊内存。内存这块坑更多,我到时候分享几个「血泪史」。