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 0 缓存命中率提升 减少上下文切换 避免NUMA影响 延迟确定性提升

从图里能看出来,CPU亲和性不是直接降低延迟,而是通过消除不确定性来间接降低延迟。交易系统最怕的就是“忽快忽慢”。绑定了CPU,延迟曲线就变得平滑了。

一个小技巧:在绑定CPU之前,先用lscpu命令看看你的CPU拓扑。搞清楚哪些核心是物理核心,哪些是超线程。我一般把交易进程绑在物理核心上,超线程留给辅助任务。

好了,这一章就聊到这儿。CPU亲和性这个概念,说白了就是让进程“安家落户”,别到处流浪。下一章咱们会深入讲讲,怎么在Linux里实际配置和验证CPU亲和性。


公众号:蓝海资料掘金营,微信deep3321