CPU性能分析:从架构到实战
CPU性能分析,说白了就是搞清楚你的程序到底在CPU上干了什么。我见过太多人一上来就盯着CPU使用率看,其实那只是冰山一角。今天咱们聊聊CPU架构基础、上下文切换、运行队列,还有CPU绑定与亲和性。这些概念串起来,你就能看懂CPU到底在忙什么。
核心观点:CPU性能瓶颈往往不是算力不够,而是调度和资源争抢的问题。
1. CPU架构基础:别把CPU当黑盒
现代CPU早就不是单核单线程那么简单了。我刚开始做性能优化时,以为CPU就是一块芯片,后来被一次线上问题狠狠教育了一顿。
先看几个关键概念:
- 物理核 vs 逻辑核:物理核是真实的硬件单元,逻辑核是通过超线程技术虚拟出来的。一个物理核通常可以跑2个逻辑核。
- L1/L2/L3缓存:L1最快但最小(通常32KB),L3最大但慢一些。缓存命中率直接影响性能。
- NUMA架构:非统一内存访问。每个CPU有自己的本地内存,访问远程内存要慢很多。
嗯,这里要注意:很多人在做性能分析时,完全忽略了NUMA的影响。我曾经遇到一个数据库服务,跨NUMA节点访问内存,延迟直接翻倍。
我的习惯:拿到一台新机器,先用 lscpu 和 numactl --hardware 看看CPU拓扑。别急着调优,先搞清楚硬件长什么样。
2. 上下文切换:看不见的隐形杀手
上下文切换,说白了就是CPU从一个任务切换到另一个任务时,要保存和恢复状态。每次切换都有开销,大概几微秒。听起来不多,但架不住频繁啊。
我记得有一次帮客户排查一个Java应用,CPU使用率只有30%,但吞吐量就是上不去。一看/proc/stat里的ctxt字段,每秒上下文切换次数高达20万次。这就是典型的「CPU在忙着切换,没空干活」。
怎么查?用vmstat 1看cs列:
# 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 1024000 1234 567890 0 0 10 20 500 8000 15 10 70 5 0
看到cs列8000了吗?这还算正常。如果超过2万,就要警惕了。
我曾经踩过的坑:有一次线上服务频繁超时,我查了半天发现是日志框架在疯狂刷屏,导致每秒上下文切换飙到10万+。关掉debug日志后,问题立刻消失。所以,别小看日志的威力。
上下文切换主要分两种:
- 自愿切换:线程主动让出CPU,比如等待I/O或锁。
- 非自愿切换:时间片用完了,被系统强制切换。
用pidstat -w 1可以看每个进程的切换情况:
# pidstat -w 1
Linux 5.4.0-26-generic (hostname) 01/15/25 _x86_64_ (8 CPU)
10:32:01 UID PID cswch/s nvcswch/s Command
10:32:02 1000 1234 1200 300 java
10:32:02 1000 5678 800 50 nginx
cswch/s是自愿切换,nvcswch/s是非自愿切换。如果非自愿切换占比高,说明线程太多,CPU在抢时间片。
3. 运行队列:CPU到底有多忙
运行队列(run queue)是等待CPU执行的线程队列。这个指标比CPU使用率更能反映系统压力。
为什么?因为CPU使用率100%不一定有问题,但运行队列长了,说明任务在排队,响应时间肯定会变差。
用uptime或top看负载:
$ uptime
10:35:01 up 30 days, 2:15, 1 user, load average: 4.50, 3.80, 2.10
三个数字分别代表1分钟、5分钟、15分钟的平均负载。对于8核CPU,负载4.5意味着平均有4.5个任务在等待或运行。如果负载持续高于CPU核数,说明CPU不够用了。
判断标准:负载/核数 > 0.7 就要关注,> 1.0 说明已经过载。我个人习惯是,超过0.8就开始排查。
用sar -q 1可以看更详细的运行队列信息:
# sar -q 1
Linux 5.4.0-26-generic (hostname) 01/15/25 _x86_64_ (8 CPU)
10:40:01 runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15
10:40:02 4 256 4.50 3.80 2.10
runq-sz是当前运行队列长度,plist-sz是系统总线程数。如果runq-sz长期大于CPU核数,你该加CPU了,或者优化代码减少线程争抢。
4. CPU绑定与亲和性:把任务钉死在CPU上
CPU亲和性,就是让某个进程或线程只在指定的CPU上运行。为什么要这么做?
- 减少缓存失效:线程在同一个CPU上跑,L1/L2缓存是热的。
- 避免NUMA惩罚:跨NUMA节点访问内存,延迟高。
- 隔离关键任务:把数据库进程绑定到特定CPU,不让其他任务干扰。
用taskset可以设置亲和性:
# 把PID为1234的进程绑定到CPU 0和1
taskset -cp 0,1 1234
# 启动时直接绑定
taskset -c 0-3 ./myapp
也可以用numactl:
# 绑定到CPU 0-3,并分配本地内存
numactl --cpunodebind=0 --membind=0 ./myapp
我的建议:对于延迟敏感的服务(比如Redis、Nginx),一定要做CPU绑定。我曾经把一个Redis实例从随机调度改成绑定到2个物理核,吞吐量提升了15%。
但注意,别把所有核心都绑死。留几个核心给操作系统和中断处理,否则系统会卡死。
避坑指南:我曾经把所有的CPU都绑给了应用进程,结果网络中断没地方跑,导致网卡丢包。后来留了2个核心给系统,问题解决。记住,系统也需要CPU。
5. 实战:一张图看懂CPU性能分析
下面这张图是我自己总结的CPU性能分析框架,你照着这个思路排查,基本不会漏掉关键点。
排查时,我一般按这个顺序来:
- 先看CPU架构,搞清楚物理核、逻辑核、NUMA节点。
- 再看上下文切换,是不是太高了。
- 然后看运行队列,负载是不是超标了。
- 最后考虑要不要做CPU绑定。
你想想看,如果连CPU有几个核、缓存怎么分布都不清楚,后面的分析都是瞎猜。我见过有人花了一周调优,结果发现是超线程没开对。
总结一句话:CPU性能分析不是看使用率,而是看调度效率。上下文切换、运行队列、亲和性,这三个指标比CPU使用率更能反映问题。
公众号:蓝海资料掘金营,微信deep3321