3. 系统性能分析方法论
CPU 性能分析,说白了就是搞清楚你的程序到底在 CPU 上干了什么。我见过太多团队,上来就调 JVM、调数据库,结果发现根子在 CPU 调度上。这一章,咱们把 CPU 相关的几个核心概念掰开揉碎,看看它们怎么影响性能,以及我们能做什么。
CPU 架构基础:别把 CPU 当黑盒
很多人觉得 CPU 就是个执行指令的盒子,其实没那么简单。现代 CPU 内部结构相当复杂,我挑几个跟性能最相关的点说说。
- 超线程(Hyper-Threading):一个物理核模拟成两个逻辑核。好处是能提高吞吐,坏处是俩逻辑核争抢同一个执行单元。我在压测时遇到过,开了超线程反而变慢的情况。
- 缓存层级(L1/L2/L3):L1 最快但最小(通常 32KB),L3 最大但慢。数据在 L1 命中,延迟不到 1ns;要是跑到内存里,那就是 100ns 级别。差了两个数量级。
- NUMA(非统一内存访问):多路服务器里,每个 CPU 有自己的本地内存。访问本地内存快,访问远端内存慢。这个在数据库和大数据场景下特别明显。
核心观点:CPU 性能优化的本质,就是让数据尽量靠近计算单元。能放 L1 别放 L2,能放本地内存别放远端内存。
上下文切换:看不见的性能杀手
上下文切换,就是操作系统把 CPU 从一个任务切换到另一个任务。每次切换都要保存当前任务的寄存器、程序计数器、内存映射等信息,再加载新任务的。这个过程纯属开销,不干任何实际工作。
我曾在线上环境遇到过一个诡异问题:CPU 利用率只有 30%,但系统响应特别慢。查了半天,发现是上下文切换频率高达每秒 20 万次。说白了,CPU 的时间都花在“换人”上了,没真正干活。
上下文切换的类型
| 类型 | 触发场景 | 开销级别 |
|---|---|---|
| 进程切换 | 时间片耗尽、系统调用阻塞 | 高(涉及 TLB 刷新) |
| 线程切换 | 锁竞争、I/O 等待 | 中(共享地址空间) |
| 中断处理 | 硬件中断、软中断 | 低(但频率可能很高) |
避坑指南:我曾经遇到过一个案例,开发同学用了几百个线程去处理一个简单的计算任务。结果上下文切换开销占到了 CPU 时间的 40%。记住,线程数不是越多越好,通常 CPU 密集型的线程数 = 核心数 + 1 就够了。
中断处理:别让 CPU 疲于奔命
中断是硬件通知 CPU 有事要处理的方式。比如网卡收到数据包,就会触发一个中断。CPU 收到中断后,暂停当前工作,去执行中断处理程序。
中断处理分上半部和下半部:
- 上半部(硬中断):快速响应,只做最必要的事(比如保存数据)。要求尽快返回。
- 下半部(软中断):处理耗时逻辑(比如协议栈解析)。可以延迟执行。
高并发网络服务中,中断频率可能高得吓人。我记得有一次帮一个游戏团队调优,他们的网卡每秒触发 10 万次中断,CPU 光处理中断就占了 15%。解决方案是开启网卡的 中断合并(coalescing),把多个小中断合并成一个大中断批量处理。
实用技巧:用 /proc/interrupts 可以查看每个 CPU 处理的中断数量。如果某个 CPU 的中断数特别高,说明中断分布不均匀,可以考虑用 irqbalance 或手动绑定中断到指定 CPU。
CPU 绑定与亲和性:把任务钉死在核心上
CPU 亲和性,就是让某个进程或线程固定在某个(或某几个)CPU 核心上运行。好处很明显:
- 减少缓存失效(L1/L2 是每个核私有的)
- 避免上下文切换带来的 TLB 刷新
- 提高性能的可预测性
如何设置 CPU 亲和性
Linux 下用 taskset 命令:
# 将进程 PID 绑定到 CPU 0 和 CPU 1
taskset -cp 0,1 <PID>
# 启动时绑定到 CPU 2
taskset -c 2 ./myapp
代码里也可以用 sched_setaffinity 系统调用:
#include <sched.h>
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(2, &mask); // 绑定到 CPU 2
sched_setaffinity(0, sizeof(mask), &mask);
什么时候该用亲和性?
| 场景 | 建议 | 原因 |
|---|---|---|
| CPU 密集型计算 | 绑定到固定核心 | 充分利用 L1/L2 缓存 |
| 网络收发包(DPDK) | 绑定到独立核心 | 避免中断和轮询干扰 |
| 数据库实例 | 绑定到一组核心 | 减少 NUMA 跨节点访问 |
| 普通 Web 服务 | 不绑定或宽松绑定 | 让内核调度更灵活 |
注意:别把所有进程都绑死。内核的调度器其实挺聪明的,过度绑定反而可能导致某些核心空闲、某些核心过载。我一般只在性能关键路径上使用亲和性,其他进程让内核自由调度。
知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,从 CPU 架构到中断处理,再到亲和性设置,最终都指向同一个目标:减少开销,提高效率。
实战:用 perf 分析上下文切换
光说不练假把式。咱们用 perf 工具看看真实的上下文切换情况。
# 统计系统级上下文切换
perf stat -e context-switches,cpu-migrations,page-faults -a sleep 5
# 跟踪特定进程的调度事件
perf sched record -- sleep 10
perf sched latency
# 查看每个 CPU 的上下文切换次数
cat /proc/stat | grep ctxt
输出示例:
Performance counter stats for 'system wide':
12,345 context-switches # 每秒约 2469 次
1,234 cpu-migrations # 每秒约 247 次
567 page-faults # 每秒约 113 次
5.001234 seconds time elapsed
我的经验:如果 cpu-migrations(进程在不同 CPU 间迁移)数量很高,说明亲和性设置有问题。我曾经在一个 Redis 集群上看到每秒上千次迁移,绑核之后直接降到了个位数,吞吐提升了 20%。