CPU 性能分析:从使用率到亲和性

CPU 性能调优,说白了就是搞清楚你的程序到底在“等”什么。是等 CPU 算完?还是在等锁?还是在等内存?

我个人习惯,拿到一个性能问题,先看 CPU 使用率。但这玩意儿很容易骗人。你想想看,CPU 跑满 100% 不一定是好事,跑不满也不一定是坏事。关键得拆开来看。

CPU 使用率:别被数字骗了

CPU 使用率分两种:整体使用率和进程使用率。整体使用率告诉你系统忙不忙,进程使用率告诉你具体谁在忙。

我遇到过一个问题:线上服务 CPU 使用率只有 30%,但请求延迟高得离谱。查了半天,发现是大量线程在争一把锁,CPU 都花在上下文切换上了。你看,CPU 没闲着,但干的都是无用功。

关键指标拆解

  • user (us):用户态 CPU 时间。程序逻辑在跑,这是好事。
  • system (sy):内核态 CPU 时间。系统调用、中断处理、锁竞争都在这里。
  • iowait (wa):等待 I/O 完成。磁盘慢、网络慢,CPU 在干等。
  • steal (st):被虚拟机偷走的 CPU。云上常见,邻居太吵了。
  • idle (id):空闲。CPU 没事干,但你的程序可能被阻塞了。

用 top 看第一眼,我一般先看 sy 和 wa。sy 超过 20%,说明内核开销太大。wa 超过 10%,磁盘大概率是瓶颈。

# 查看 CPU 使用率细节
top - 10:15:30 up 3 days,  2:15,  4 users,  load average: 2.50, 2.30, 2.10
%Cpu(s): 45.0 us, 15.0 sy,  0.0 ni, 35.0 id,  5.0 wa,  0.0 hi,  0.0 si,  0.0 st

这个例子,sy 15% 已经偏高了。我会接着看 vmstat 1 确认上下文切换频率。

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

上下文切换,就是 CPU 从一个线程切到另一个线程。每次切换都要保存寄存器、刷新 TLB、重新加载指令。一次切换大概几微秒,但每秒几万次呢?

我曾经优化过一个网关程序,CPU 使用率只有 40%,但吞吐量上不去。用 perf 一看,每秒上下文切换 8 万次。说白了,线程数开太多了,CPU 都在忙着“换人”,没空干活。

我的经验:上下文切换超过每秒 1 万次,就要警惕了。超过 5 万次,基本可以断定是性能瓶颈。

怎么查?vmstat 1 看 cs 列,或者 pidstat -w 1 看每个进程的切换次数。

# 查看上下文切换
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
 5  0      0 1024000  20480 512000    0    0    10    20 2000 15000 45 15 35  5  0

cs 15000,每秒 1.5 万次切换。配合 r 列(运行队列)5,说明有 5 个线程在抢 CPU。这时候就该考虑减少线程数,或者用协程替代了。

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

中断分两种:硬中断(hardirq)和软中断(softirq)。硬中断是硬件发的,比如网卡收到包。软中断是内核自己触发的,比如网络协议栈处理。

中断太多,CPU 会被频繁打断。我见过一个极端案例:某台机器每秒硬中断 10 万次,CPU 的 si(软中断)占了 60%。查下来是网卡队列没做 RSS(Receive Side Scaling),所有中断都打在一个核上。

注意:中断处理默认在 CPU 0 上执行。如果 CPU 0 的 si 特别高,其他核却很闲,说明中断分布不均匀。

用 cat /proc/interrupts 可以看每个 CPU 的中断分布。

# 查看中断分布
cat /proc/interrupts | grep eth0
           CPU0       CPU1       CPU2       CPU3
 24:   50000000          0          0          0  IR-PCI-MSI  eth0

你看,所有中断都在 CPU0 上。这时候需要开启网卡的多队列,或者用 irqbalance 自动均衡。

CPU 绑定与亲和性:把线程钉死在核上

CPU 亲和性,就是让某个线程只在指定的 CPU 上运行。好处很明显:减少缓存失效,避免上下文切换带来的 TLB 刷新。

我习惯在两种场景下用亲和性:

  • 延迟敏感型:比如高频交易、游戏服务器,需要稳定的响应时间。
  • CPU 密集型:比如视频编码、科学计算,缓存命中率直接影响性能。

设置亲和性有两种方式:

  • taskset:命令行工具,启动时绑定。
  • sched_setaffinity:代码里调用,运行时绑定。
# 启动时绑定到 CPU 0 和 CPU 1
taskset -c 0,1 ./myapp

# 运行时查看亲和性
taskset -p 12345
pid 12345's current affinity mask: 3

mask 3 是二进制 11,表示 CPU 0 和 CPU 1。如果 mask 是 1,就只绑在 CPU 0 上。

避坑指南:我曾经把两个 CPU 密集型的线程绑在同一个核上,结果性能反而下降了。亲和性不是“绑得越紧越好”,要保证每个核上的线程数不超过 1(对于超线程,不超过 2)。

实战:用一张图串起来

下面这张图,是我做性能分析时的思考路径。从 CPU 使用率入手,一步步排查到亲和性设置。

CPU 性能分析决策流程 CPU 使用率异常 us 高?sy 高?wa 高? us 高 → 程序在算 检查算法复杂度 sy 高 → 内核开销大 查上下文切换 wa 高 → 等 I/O 查磁盘/网络 检查中断分布 设置 CPU 亲和性

这张图的逻辑很简单:先看 CPU 使用率拆解,找到大头。如果是 sy 高,查上下文切换;如果是 wa 高,查 I/O。最后别忘了检查中断分布,必要时用亲和性把线程钉在指定核上。

总结一下

CPU 性能分析,我一般按这个顺序来:

  1. 看使用率:us、sy、wa 哪个高?
  2. 看上下文切换:cs 超过 1 万就要动手了。
  3. 看中断分布:是不是都打在 CPU 0 上?
  4. 设亲和性:把关键线程绑在独立核上。

嗯,这套流程我用了好几年,基本没失手过。你下次遇到 CPU 问题,不妨也按这个顺序走一遍。