3、CPU瓶颈定位:CPU使用率分析、上下文切换、中断处理、负载均衡

说到CPU瓶颈,我见过太多人一上来就盯着CPU使用率看。其实这玩意儿就像体温计——能告诉你发烧了,但说不清是病毒感染还是伤口发炎。今天咱们就把CPU这层皮扒开,看看里面到底在忙什么。

3.1 CPU使用率:别被数字骗了

CPU使用率100%就一定有问题吗?不一定。我遇到过一台服务器,CPU跑满100%,但业务延迟反而正常。后来发现是某个批处理任务在“偷跑”,把空闲时间全占了。所以,关键要看“谁在用”和“怎么用”。

Linux下我习惯用top和mpstat来拆解CPU时间片:

# 查看CPU时间分布
$ mpstat -P ALL 1 5
Linux 5.4.0 (node01)  03/15/2024  _x86_64_ (8 CPU)

10:32:01  CPU   %usr  %nice  %sys  %iowait  %irq  %soft  %steal  %guest  %idle
10:32:02  all   65.2   0.0   12.3    8.5    0.0    2.1     0.0     0.0   11.9
10:32:02    0   72.1   0.0   10.5    6.2    0.0    1.8     0.0     0.0    9.4
10:32:02    1   58.3   0.0   14.1   10.8    0.0    2.4     0.0     0.0   14.4

这里有几个关键指标:

  • %usr:用户态CPU时间。如果这个高,说明你的应用代码在疯狂计算。
  • %sys:内核态CPU时间。这个高了要小心——可能是系统调用太频繁。
  • %iowait:CPU等I/O的时间。说白了就是CPU在“摸鱼”,等磁盘或网络。
  • %steal:虚拟化环境被偷走的时间。云服务器上这个高,说明邻居在抢资源。

我的经验法则:如果%usr高,优化代码;如果%sys高,减少系统调用;如果%iowait高,换SSD或加内存缓存。

3.2 上下文切换:看不见的性能杀手

上下文切换,说白了就是CPU从一个任务切到另一个任务。每次切换都要保存寄存器、刷新TLB、加载新进程状态。你想想看,如果每秒切换几万次,CPU光干这个就累死了。

我曾在压测一个高并发服务时,发现CPU使用率只有30%,但吞吐量上不去。用vmstat一看,上下文切换(cs列)高达每秒8万次:

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 8  0      0 2048000  1024 8192000    0    0     0     0 1200 82000 25 45 30  0  0

看到没?cs(context switch)高达82000,sy(系统CPU)占了45%。这明显是上下文切换把CPU吃掉了。

为什么会这样?我排查后发现是线程池开得太大,每个请求都创建新线程。后来改用协程,上下文切换直接降到2000以下。

避坑指南:我曾经在Nginx上遇到类似问题,后来发现是开启了太多worker进程,每个worker又绑定独立CPU。其实对于I/O密集型服务,worker数等于CPU核数就够了,多了反而因为上下文切换拖慢性能。

3.3 中断处理:别让硬件打断你的节奏

中断是硬件通知CPU“有事找你”的方式。但中断太多,CPU就得频繁停下手中的活去处理。我见过一个极端案例——某台机器网卡中断每秒超过10万次,CPU的%irq和%soft加起来占了60%。

查看中断分布用/proc/interrupts:

$ cat /proc/interrupts | grep eth0
           CPU0       CPU1       CPU2       CPU3
 24:  12345678   23456789   34567890   45678901   IR-PCI-MSI  eth0-TxRx-0
 25:  87654321   98765432   09876543   10987654   IR-PCI-MSI  eth0-TxRx-1

这里每个CPU处理的中断数一目了然。如果某个CPU的中断数特别高,说明中断没有均衡分布。

解决办法有两个:

  • 中断亲和性(IRQ affinity):把中断绑定到特定CPU,避免所有中断挤在一个核上。
  • 多队列网卡(RSS):现代网卡支持多队列,每个队列可以绑定不同CPU,自动均衡。

注意:别把中断和业务进程绑在同一个CPU上。否则业务进程被中断打断,延迟会飙升。我一般把中断绑在CPU 0-1,业务进程跑在CPU 2-7。

3.4 负载均衡:让每个CPU都忙起来

负载不均衡是常见问题。比如8核CPU,一个核跑满100%,其他核闲着。这就像8个人干活,一个人累死,七个人聊天。

检查负载均衡用mpstat -P ALL或者pidstat:

$ pidstat -p ALL -t 1
Linux 5.4.0 (node01)  03/15/2024  _x86_64_ (8 CPU)

10:35:01   UID      TGID       TID    %usr %system  %guest   %wait    %CPU   CPU  Command
10:35:02     0      1234         -   85.2    2.1     0.0    0.0   87.3     0   myapp
10:35:02     0      1235         -    2.3    0.5     0.0    0.0    2.8     1   myapp
10:35:02     0      1236         -    1.8    0.3     0.0    0.0    2.1     2   myapp

看到没?CPU列显示所有线程都挤在CPU 0上。这就是典型的软中断或锁竞争导致的问题。

我常用的调优手段:

  1. 设置CPU亲和性:用taskset或numactl把线程绑定到不同CPU。
  2. 调整内核调度策略:比如把/proc/sys/kernel/sched_migration_cost_ns调小,让调度器更积极地迁移任务。
  3. 使用cgroups隔离:给不同业务分配CPU配额,防止互相干扰。

我的一个教训:有一次数据库CPU不均衡,我折腾了半天亲和性,最后发现是NUMA架构下内存访问延迟导致的。说白了,线程在CPU 0上跑,但数据在CPU 1的内存里,访问一次要绕远路。后来用numactl --membind把内存和CPU绑定,问题就解决了。

知识体系总览

下面这张图总结了CPU瓶颈定位的核心逻辑。你可以把它当作排查时的“地图”:

CPU瓶颈定位知识体系 CPU瓶颈定位 CPU使用率分析 %usr / %sys / %iowait mpstat / top / sar 上下文切换 vmstat cs列 / pidstat 线程数过多 / 锁竞争 中断处理 /proc/interrupts IRQ亲和性 / RSS多队列 负载均衡 taskset / numactl NUMA感知 / cgroups 排查顺序:先看使用率 → 再看上下文切换 → 检查中断分布 → 最后做负载均衡

嗯,这张图基本把CPU瓶颈的排查路径串起来了。实际工作中,我一般按这个顺序来:先看使用率是不是异常,再查上下文切换有没有过高,接着检查中断是否均衡,最后才做负载均衡调优。每一步都有对应的工具和指标,别跳步。

小技巧:如果你在排查时觉得无从下手,先跑一个perf top看看热点函数在哪。很多时候,热点函数会直接告诉你瓶颈在用户态还是内核态,省去很多猜测时间。

想要更多资料,联系可可