3、CPU性能分析:CPU利用率、上下文切换、运行队列、CPU绑定
说到CPU性能分析,很多人第一反应就是看利用率。嗯,这没错,但远远不够。我见过太多团队,CPU利用率才60%,系统却已经慢得像蜗牛。为什么?因为真正的瓶颈往往藏在利用率背后。
今天我们就来聊聊CPU性能分析的四个核心维度。我个人习惯把这四个维度比作汽车的仪表盘——利用率是速度表,上下文切换是换挡频率,运行队列是排队长度,CPU绑定则是车道分配。每个指标单独看都有意义,但组合起来才能看清全貌。
3.1 CPU利用率:不只是百分比
CPU利用率,说白了就是CPU忙活的时间占比。但「忙活」也分三六九等。
在Linux里,/proc/stat文件记录了CPU时间的详细分布。我建议你用top或mpstat来查看,重点关注这几个维度:
- user:用户态程序运行时间。高的话说明应用在真干活。
- system:内核态时间。高的话要警惕——是不是系统调用太频繁?
- iowait:等待I/O完成的时间。这个高了,CPU其实在「空转等数据」。
- steal:虚拟化环境下被宿主机偷走的时间。云服务器上这个值高,说明邻居在抢资源。
关键判断:如果user高,说明CPU在认真工作;如果system高,可能是锁竞争或系统调用过多;如果iowait高,别盯着CPU了,去查磁盘或网络。
我曾经接手过一个Java应用,CPU利用率长期在85%以上,但业务吞吐量就是上不去。一查,发现system占了40%。再往下挖,原来是日志框架每写一条日志就做一次系统调用。改成批量写入后,CPU利用率降到50%,吞吐量翻了一倍。
3.2 上下文切换:看不见的开销
上下文切换,就是CPU从一个任务切换到另一个任务。每次切换都要保存和恢复寄存器、内存映射、栈指针……这些操作虽然快,但积少成多。
用vmstat 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
2 0 0 123456 1234 56789 0 0 10 20 500 3000 30 10 60 0 0
注意cs列,就是每秒上下文切换次数。3000次算正常吗?看场景。如果是数据库服务器,这个值可能上万。但如果是Web应用,超过5000就要警惕了。
我的经验:上下文切换高,通常有两个原因:一是线程太多,二是锁竞争激烈。我曾经优化过一个消息队列中间件,把线程数从64降到16,上下文切换从每秒2万次降到3000次,延迟直接减半。
为什么会这样?你想想看,每个线程都在抢锁,抢不到就挂起,挂起就触发切换。线程越多,切换越频繁,形成恶性循环。
3.3 运行队列:CPU到底有多忙?
运行队列(run queue)是等待CPU执行的线程数。这个指标比利用率更能反映CPU是否过载。
用uptime或top可以看负载平均值:
$ uptime
14:32:10 up 10 days, 2:30, 2 users, load average: 4.50, 3.20, 2.80
三个数字分别代表1分钟、5分钟、15分钟的平均负载。关键判断标准:
- 负载 < CPU核心数:资源充裕,运行良好。
- 负载 ≈ CPU核心数:刚好饱和,需要关注。
- 负载 > CPU核心数:过载了,任务在排队。
注意:负载不等于利用率。一个4核CPU,负载4.0,利用率可能只有50%。因为有些线程在等I/O,不算「运行中」但算「可运行」。所以负载高+利用率低,通常意味着I/O瓶颈。
我记得有一次排查线上问题,CPU利用率才30%,但接口响应时间飙升。一看负载,8核机器负载到了12。再查iowait,发现磁盘在疯狂写日志。把日志级别调低后,负载降到2,问题解决。
3.4 CPU绑定:把线程钉在核心上
CPU绑定(CPU affinity),就是把进程或线程固定到某个或某几个CPU核心上。为什么要这么做?
- 减少缓存失效:线程一直在同一个核心上跑,L1/L2缓存命中率更高。
- 避免上下文切换开销:核心间迁移也是切换,绑定后能减少。
- 隔离关键任务:把核心留给重要线程,不让其他进程抢。
在Linux里,用taskset命令设置CPU绑定:
# 把进程PID 1234绑定到CPU 0和1
taskset -cp 0,1 1234
# 启动时绑定
taskset -c 0-3 ./myapp
代码里也能设置,以C语言为例:
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>
void set_cpu_affinity(int cpu_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
pthread_t thread = pthread_self();
if (pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset) != 0) {
perror("pthread_setaffinity_np");
}
}
避坑指南:我曾经把数据库的IO线程和业务线程绑在同一个核心上,结果IO线程抢不到CPU,写入延迟飙升。后来把IO线程绑到独立核心,业务线程绑到另外几个核心,性能才稳定下来。记住:绑定要合理规划,别把不相干的任务挤在一起。
另外,超线程(Hyper-Threading)核心要小心。两个逻辑核心共享物理资源,如果绑到同一个物理核心的两个逻辑核上,其实没起到隔离作用。我建议用lscpu -e查看核心拓扑,再决定绑定策略。
3.5 实战:综合分析案例
最后分享一个我实际遇到的案例。一个游戏服务器,玩家一多就卡顿。当时的数据是这样的:
| 指标 | 数值 | 分析 |
|---|---|---|
| CPU利用率 | 75% | 看起来还行 |
| 上下文切换 | 15000次/秒 | 偏高 |
| 运行队列 | 6.5(4核) | 明显过载 |
| iowait | 5% | 不算高 |
你看,利用率才75%,但运行队列已经6.5了。说明什么?说明CPU在频繁切换,真正干活的时间被切碎了。我建议他们把玩家线程从32个降到8个,并绑定到不同核心。结果上下文切换降到4000次,运行队列降到1.2,卡顿消失。
嗯,这就是CPU性能分析的精髓——别只看一个指标,要四个维度一起看。利用率高不一定有问题,利用率低也不一定没问题。上下文切换和运行队列,才是真正反映CPU「累不累」的关键。