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 使用率拆解,找到大头。如果是 sy 高,查上下文切换;如果是 wa 高,查 I/O。最后别忘了检查中断分布,必要时用亲和性把线程钉在指定核上。
总结一下
CPU 性能分析,我一般按这个顺序来:
- 看使用率:us、sy、wa 哪个高?
- 看上下文切换:cs 超过 1 万就要动手了。
- 看中断分布:是不是都打在 CPU 0 上?
- 设亲和性:把关键线程绑在独立核上。
嗯,这套流程我用了好几年,基本没失手过。你下次遇到 CPU 问题,不妨也按这个顺序走一遍。