2、CPU亲和性概念:什么是CPU亲和性、软亲和性与硬亲和性、为什么交易系统需要绑定CPU
大家好,我是老赵。今天咱们聊聊CPU亲和性。这词儿听起来有点学术,但说白了,就是让某个进程或线程,固定在某个CPU核心上跑,别到处乱窜。
我刚开始做交易系统那会儿,也踩过这个坑。当时系统延迟忽高忽低,查了半天,发现是进程在CPU之间来回切换,缓存全失效了。嗯,从那以后,CPU亲和性就成了我优化清单里的头号选手。
什么是CPU亲和性
CPU亲和性,英文叫CPU Affinity。它定义了一个规则:哪个进程可以跑在哪个CPU核心上。
你想想看,操作系统默认的调度策略是“公平调度”。它觉得所有CPU核心都一样,哪个空闲就把任务扔过去。但现实不是这样的。每个CPU核心都有自己的L1、L2缓存。进程在核心A上跑了一会儿,缓存里全是它的数据。如果突然被切到核心B,缓存就全废了,得重新加载。这就是所谓的“缓存污染”和“缓存未命中”。
CPU亲和性就是用来解决这个问题的。它告诉操作系统:“别瞎调度,让这个进程就待在这个核心上。”
核心观点:CPU亲和性 = 进程与CPU核心的绑定关系。绑定了,缓存就热了,延迟就低了。
软亲和性与硬亲和性
这里有个容易混淆的概念:软亲和性和硬亲和性。我当年也搞混过,后来在调优一个高频策略时才算彻底弄明白。
软亲和性
软亲和性,说白了就是一个“建议”。你告诉操作系统:“我希望能跑在核心0上。”但操作系统不一定听你的。如果核心0太忙,它还是会把你的进程挪到核心1上去。
Linux里,软亲和性通过sched_setaffinity()系统调用来设置。它设置的是一个CPU掩码(CPU mask),表示“允许这个进程跑在这些核心上”。
// 示例:将当前进程绑定到CPU 0和CPU 1
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
int main() {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask); // 允许CPU 0
CPU_SET(1, &mask); // 允许CPU 1
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
perror("sched_setaffinity");
return 1;
}
printf("进程已绑定到CPU 0和CPU 1\n");
return 0;
}
注意,这里用的是“允许”,不是“强制”。操作系统在调度时,会尽量满足你的要求,但如果实在没办法,它还是会打破这个约束。
硬亲和性
硬亲和性就霸道多了。它通过cgroup或内核参数,强制进程只能跑在指定的核心上。操作系统没有商量余地。
在Linux里,硬亲和性通常通过cpuset cgroup子系统来实现。你可以创建一个cgroup,指定它只能使用哪些CPU核心,然后把进程放进去。
# 创建一个cpuset cgroup
mkdir /sys/fs/cgroup/cpuset/my_trading_group
# 设置允许的CPU核心(只允许CPU 0和CPU 1)
echo "0-1" > /sys/fs/cgroup/cpuset/my_trading_group/cpuset.cpus
# 设置内存节点(通常跟CPU对应)
echo "0" > /sys/fs/cgroup/cpuset/my_trading_group/cpuset.mems
# 把进程PID放进去
echo 12345 > /sys/fs/cgroup/cpuset/my_trading_group/cgroup.procs
这样做的好处是:绝对隔离。其他进程就算想用这些核心,也用不了。坏处是:如果绑定的核心不够用,进程会直接卡死,不会自动迁移到其他核心。
| 特性 | 软亲和性 | 硬亲和性 |
|---|---|---|
| 实现方式 | sched_setaffinity() | cgroup cpuset |
| 强制程度 | 建议性 | 强制性 |
| 是否可被覆盖 | 是(系统负载高时) | 否 |
| 适用场景 | 一般性能优化 | 硬实时、低延迟交易 |
我的经验:在交易系统里,我一般先用软亲和性做初步绑定。如果延迟抖动还是大,再上硬亲和性。别一上来就硬绑,万一核心数算错了,系统可能直接挂掉。
为什么交易系统需要绑定CPU
这个问题,说白了就三个字:确定性。
交易系统对延迟的要求,跟普通Web服务完全不是一个量级。Web服务几百毫秒的延迟,用户可能感觉不到。但交易系统里,微秒级的抖动都可能导致策略失效。
CPU亲和性带来的好处,我总结了几点:
- 缓存命中率提升:进程固定在同一个核心上,L1/L2缓存始终是热的。数据访问速度能快几十倍。
- 减少上下文切换:进程不跨核心迁移,就避免了TLB刷新和缓存失效。上下文切换的开销,一次可能就几微秒,但累积起来很可怕。
- 避免NUMA影响:在多路服务器上,CPU访问本地内存和远端内存的速度差很多。绑定CPU后,可以确保进程始终访问本地内存。
- 隔离干扰:把交易进程绑在专用核心上,其他系统进程(如日志、监控)就不会来抢资源。
避坑指南:我曾经犯过一个错误。在32核的机器上,我把交易进程绑到了核心0,但没注意核心0还要处理时钟中断和网卡中断。结果交易延迟反而更差了。后来我学乖了:一定要把交易进程绑在隔离的中断亲和性核心上,或者干脆用isolcpus内核参数把核心隔离出来。
下面这张图,是我自己画的CPU亲和性在交易系统里的作用流程。你看一眼就明白了。
从图里能看出来,CPU亲和性不是直接降低延迟,而是通过消除不确定性来间接降低延迟。交易系统最怕的就是“忽快忽慢”。绑定了CPU,延迟曲线就变得平滑了。
一个小技巧:在绑定CPU之前,先用lscpu命令看看你的CPU拓扑。搞清楚哪些核心是物理核心,哪些是超线程。我一般把交易进程绑在物理核心上,超线程留给辅助任务。
好了,这一章就聊到这儿。CPU亲和性这个概念,说白了就是让进程“安家落户”,别到处流浪。下一章咱们会深入讲讲,怎么在Linux里实际配置和验证CPU亲和性。
公众号:蓝海资料掘金营,微信deep3321