4. 操作系统延迟优化:CPU亲和性、中断绑定、实时内核、内存大页

做市引擎跑在操作系统上,操作系统本身却是个「拖后腿」的家伙。你想想看,你的交易线程正忙着处理行情,操作系统突然说「嘿,你该歇歇了,让别的线程跑一会儿」——这就是上下文切换。一次切换几百纳秒,在纳秒级竞争的做市领域,这简直是在烧钱。

我个人习惯,拿到一台新机器,第一件事不是装软件,而是先把操作系统「扒一层皮」。把那些不必要的服务全关掉,把CPU和内存的控制权牢牢抓在自己手里。今天聊的这四个方向,就是我这些年反复折腾过的经验总结。

4.1 CPU亲和性:把线程焊死在核心上

CPU亲和性,说白了就是告诉操作系统:「这个线程只能在这几个核心上跑,别给我乱挪。」

为什么要这么做?因为每次线程被调度到另一个核心,L1/L2缓存就全废了。你想想看,好不容易把热数据加载到缓存里,一换核心,全得重来。对于做市引擎这种对延迟极度敏感的应用,这简直是灾难。

我在项目中遇到过最夸张的一次,一个订单处理线程在8个核心间来回跳,延迟抖动从5微秒直接飙到50微秒。后来绑定了核心,抖动立刻降到个位数。

核心原则: 做市引擎的每个关键线程,都应该独占一个物理核心。不要超线程,不要共享。

Linux下设置亲和性,我一般用两种方式:

方式一:命令行工具 taskset

# 启动时绑定到CPU 0和1
taskset -c 0,1 ./market_maker

# 对已运行的进程绑定
taskset -pc 0,1 <PID>

方式二:代码里用 sched_setaffinity

#include <sched.h>
#include <pthread.h>

void pin_thread_to_core(pthread_t thread, int core_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core_id, &cpuset);
    
    int ret = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (ret != 0) {
        // 嗯,这里要注意,权限不够会失败
        perror("pthread_setaffinity_np");
    }
}
我的习惯: 物理核心从0开始编号,但要注意超线程。比如一个8核16线程的CPU,物理核心是0-7,逻辑核心是8-15。我一般只用物理核心,关掉超线程。

4.2 中断绑定:别让网络中断打扰交易线程

网卡收到一个数据包,会触发一个硬件中断。这个中断默认可能在任何一个核心上处理。你想想看,你的交易线程正在CPU 2上跑着,结果网卡中断来了,操作系统把它安排到CPU 2上——你的交易线程就被打断了。

我曾经在一个实盘环境里排查延迟毛刺,查了三天,最后发现是网卡中断和交易线程抢同一个核心。每次中断处理要花几微秒,但关键是打断了交易线程的流水线,恢复执行时缓存都凉了。

解决方案很简单:把中断绑定到专门的核心上,和交易线程彻底隔离。

查看当前中断分布:

cat /proc/interrupts | grep eth0

修改中断亲和性:

# 假设网卡中断号是 78,绑定到 CPU 3
echo 8 > /proc/irq/78/smp_affinity

# 注意:这里的值是位掩码
# CPU 0 = 0x01
# CPU 1 = 0x02
# CPU 2 = 0x04
# CPU 3 = 0x08
避坑指南: 我曾经犯过一个错,把中断绑到了CPU 0,结果系统时钟中断也在CPU 0上,两个抢资源。后来我专门留了两个核心给中断和系统用,交易线程独占剩下的核心。

4.3 实时内核:让交易线程插队

普通Linux内核是「公平调度」的,每个线程轮流用CPU。但做市引擎需要的是「特权调度」——行情来了,交易线程必须立刻执行,其他线程靠边站。

实时内核(PREEMPT_RT)就是干这个的。它允许你给线程设置实时优先级,让高优先级的线程可以抢占低优先级的线程。

安装实时内核:

# Ubuntu/Debian
apt-get install linux-image-rt-amd64

# CentOS/RHEL
yum install kernel-rt

设置实时优先级:

# 使用 chrt 命令
chrt -f 99 ./market_maker

# 或者在代码里设置
#include <sched.h>

struct sched_param param;
param.sched_priority = 99;
int ret = pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);
重要提醒: 实时优先级范围是0-99,99最高。但千万别把所有线程都设成99,否则系统关键进程(比如sshd)抢不过你的线程,机器就失联了。我一般交易线程设95,辅助线程设80,留点余量给系统。

实时内核的代价是什么?吞吐量会下降。因为实时调度为了低延迟,牺牲了一部分CPU利用率。但对于做市引擎,延迟是第一位的,吞吐量够用就行。

4.4 内存大页:减少TLB缺失

这个知识点,说实话,很多人容易忽略。但它的收益非常直接。

操作系统管理内存是以「页」为单位的。默认4KB一页。你的做市引擎可能占了几十GB内存,想想看,4KB一页,得有多少页?TLB(页表缓存)根本装不下。每次访问新页面,都要去查页表,这就是TLB缺失。

大页(Huge Pages)把页大小从4KB提升到2MB甚至1GB。页数少了,TLB命中率自然就上去了。

配置大页:

# 查看当前大页配置
cat /proc/meminfo | grep Huge

# 设置大页数量(假设每页2MB,需要1024页)
echo 1024 > /proc/sys/vm/nr_hugepages

# 或者用 sysctl
sysctl -w vm.nr_hugepages=1024

在代码里使用大页:

#include <sys/mman.h>

// 分配大页内存
void *addr = mmap(NULL, size, 
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                  -1, 0);

if (addr == MAP_FAILED) {
    perror("mmap huge pages failed");
}
我的经验: 做市引擎的订单簿、行情缓存这些频繁访问的数据结构,一定要放在大页上。我见过一个团队,只改了内存分配方式,延迟就降了15%。而且大页还有个好处——不会被swap出去,这对实时系统太重要了。

4.5 把这些技术组合起来

单独用其中一项,效果有限。真正的优化是把它们组合成一个整体方案。

下面是我在一个项目里用过的核心分配方案:

CPU核心 用途 说明
0 系统/中断 网卡中断、时钟中断、系统进程
1 行情接收 实时优先级80,绑定大页
2 订单处理 实时优先级95,绑定大页
3 风控/日志 普通优先级,不参与实时竞争

这个方案的核心思路是:隔离。把实时任务和系统任务彻底分开,互不干扰。

一句话总结: CPU亲和性解决「在哪跑」,中断绑定解决「别打断」,实时内核解决「优先跑」,内存大页解决「跑得快」。四管齐下,才能把操作系统的延迟压到极致。

嗯,这些技术说起来简单,但真正落地的时候,每个环节都可能踩坑。我建议你从CPU亲和性开始,这个改动最小,收益最明显。然后逐步加上中断绑定和实时内核。最后再优化内存大页——因为大页需要应用层配合改代码,工作量稍大一些。

做市引擎的延迟优化,操作系统这关必须过。过了这关,你的引擎才算真正「跑在硬件上」,而不是「跑在操作系统里」。