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 架构到中断处理,再到亲和性设置,最终都指向同一个目标:减少开销,提高效率。

CPU 性能分析知识体系 CPU 性能优化 CPU 架构基础 上下文切换 中断处理 CPU 绑定与亲和性 超线程 缓存层级 NUMA 指令流水线 进程/线程切换 硬中断(上半部) 软中断(下半部) taskset / sched_setaffinity 核心目标 减少开销,提高缓存命中率

实战:用 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%。

总结