4、操作系统与内核优化:Linux内核调优、CPU亲和性与隔离、内存大页与NUMA绑定

做高频交易的人都知道,硬件选型只是第一步。真正让系统跑出纳秒级延迟的,是操作系统层面的调优。说白了,Linux内核默认是为通用场景设计的——它要照顾数据库、Web服务器、桌面应用。但咱们做高频交易,要的是极致的确定性。

我个人习惯,拿到一台新机器后,第一件事就是关掉所有不必要的内核服务。然后才是今天要聊的三个核心话题:内核调优、CPU亲和性与隔离、内存大页与NUMA绑定。

核心逻辑图:操作系统优化的三个层次

高频交易操作系统优化体系 Linux内核调优 时钟源切换 中断亲和性 CPU亲和性与隔离 isolcpus taskset/sched_setaffinity 内存大页与NUMA HugePages numactl绑定 优化目标:降低延迟抖动(Jitter) • 内核调优:减少上下文切换,避免时钟中断干扰 • CPU隔离:让交易线程独占核心,不被其他进程抢占 • 内存大页:减少TLB Miss,提升内存访问速度 • NUMA绑定:避免跨节点内存访问,降低延迟

4.1 Linux内核调优——把通用系统变成交易专用

先说说内核调优。我见过不少团队,花了几十万买硬件,结果跑在默认内核参数上。这就像买了一辆法拉利,却用着家用车的机油。浪费。

时钟源切换——这是最容易忽略的点。Linux默认的时钟源可能是tsc或者hpet。在高频交易场景下,我强烈建议使用tsc(时间戳计数器)。为什么?因为tsc是CPU内部的寄存器,读取开销只有几十纳秒。而hpet需要经过总线访问,延迟高一个数量级。

检查当前时钟源:

cat /sys/devices/system/clocksource/clocksource0/current_clocksource

如果显示的是hpet,赶紧切:

echo tsc > /sys/devices/system/clocksource/clocksource0/current_clocksource

注意:有些老CPU的tsc不稳定,比如会随CPU频率变化。现代Intel/AMD CPU的tsc基本都是恒定不变的(constant_tsc + nonstop_tsc)。你可以用 cat /proc/cpuinfo | grep tsc 确认一下。

中断亲和性——网卡中断来了,如果落在交易线程所在的CPU上,那就会打断交易逻辑。我习惯的做法是:把网卡中断绑定到某个专用核心上,交易线程跑在另外的核心上。这样中断不会干扰交易。

设置中断亲和性:

# 查看网卡中断号
cat /proc/interrupts | grep eth0

# 将中断绑定到CPU2(bitmask为0x04)
echo 04 > /proc/irq/<中断号>/smp_affinity

嗯,这里要注意:smp_affinity用的是十六进制掩码。CPU0对应0x01,CPU1对应0x02,CPU2对应0x04,以此类推。

内核参数调整——我常用的几个sysctl参数:

# 减少上下文切换
kernel.sched_min_granularity_ns = 1000000
kernel.sched_wakeup_granularity_ns = 500000

# 关闭透明大页(后面会讲为什么)
vm.nr_hugepages = 0
transparent_hugepage/enabled = never

# 网络优化
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728

我曾经在一个项目中,因为没关透明大页,导致内存分配出现微秒级的抖动。查了两天才定位到问题。从那以后,我每台机器都会检查这个参数。

4.2 CPU亲和性与隔离——让交易线程独占核心

CPU亲和性,说白了就是告诉操作系统:这个线程只能跑在指定的CPU上。为什么要这么做?因为如果线程在CPU之间迁移,缓存就全废了。L1/L2缓存里的数据全部失效,重新加载又要几十纳秒。对于高频交易来说,这几十纳秒可能就是一笔交易的盈亏。

隔离核心——我推荐使用内核启动参数 isolcpus。这个参数告诉内核:这些CPU不要被普通进程使用。只有明确绑定了这些CPU的线程才能跑上去。

在GRUB配置里加上:

isolcpus=2,3,4,5

这样CPU2到CPU5就被隔离出来了。系统进程、中断处理都不会跑到这些核心上。

绑定线程——代码里用 sched_setaffinity 来绑定:

#include <sched.h>

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);  // 绑定到CPU2

pthread_t thread = pthread_self();
int ret = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
if (ret != 0) {
    // 处理错误
}

命令行也可以用 taskset

taskset -c 2 ./trading_engine

你想想看,如果交易线程在CPU2上跑得好好的,突然被调度到CPU3上,L1缓存里的行情数据全没了。下次行情来了,得重新从L2/L3甚至内存里加载。这个延迟抖动,足以让策略错过最佳成交时机。

我的经验:隔离核心的数量取决于你的策略复杂度。一般来说,一个交易线程配一个核心就够了。如果还有行情解码、风控等模块,可以再分配2-3个核心。别贪多,核心多了反而增加核间通信的开销。

4.3 内存大页与NUMA绑定——减少TLB Miss,避免跨节点访问

内存访问延迟,是高频交易系统里最容易被忽视的瓶颈。很多人只关注网络延迟,却忘了内存访问也可能成为瓶颈。

HugePages(大页)——Linux默认的内存页大小是4KB。对于高频交易系统来说,这太小了。为什么?因为TLB(页表缓存)的条目数是有限的。如果内存页太小,TLB很快就被占满,每次访问内存都要查页表,这就是TLB Miss。

我举个例子:假设你的交易引擎占用了2GB内存。用4KB页,需要512K个页表条目。TLB一般只有几十到几百个条目。所以TLB Miss率会非常高。但如果用2MB的大页,只需要1024个条目。TLB Miss率大大降低。

配置大页:

# 分配1024个2MB的大页
echo 1024 > /proc/sys/vm/nr_hugepages

# 查看是否成功
cat /proc/meminfo | grep HugePages

代码里使用大页:

// 使用mmap分配大页内存
void *addr = mmap(NULL, 2 * 1024 * 1024,
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                  -1, 0);
if (addr == MAP_FAILED) {
    perror("mmap hugepage failed");
}

NUMA绑定——现代服务器都是NUMA架构。什么意思?就是CPU访问本地内存快,访问远程内存慢。如果交易线程在CPU0上跑,却分配了CPU1上的内存,那每次内存访问都要跨过QPI/UPI总线,延迟多出几十纳秒。

检查NUMA拓扑:

numactl --hardware

绑定内存到本地节点:

# 在代码中绑定
struct bitmask *nodemask = numa_allocate_nodemask();
numa_bitmask_setbit(nodemask, 0);  // 绑定到node0
numa_bind(nodemask);

// 或者用命令行
numactl --cpunodebind=0 --membind=0 ./trading_engine

我曾经遇到过一个案例:某团队的交易系统延迟忽高忽低,查了网络、查了CPU,都没问题。最后发现是NUMA没绑定。交易线程在node0上跑,但内存分配到了node1。每次访问内存都要跨节点,延迟多了50-80纳秒。绑定之后,延迟抖动立刻消失了。

总结一下优化前后的对比:

优化项 优化前 优化后 延迟改善
时钟源 hpet tsc ~100ns
CPU亲和性 无绑定 isolcpus + taskset 消除缓存抖动
内存页 4KB默认页 2MB大页 减少TLB Miss
NUMA绑定 未绑定 numactl --membind ~50ns

这些优化,每一个单独拿出来可能只省了几十纳秒。但叠加在一起,效果就很可观了。而且最重要的是——它们消除了延迟抖动,让系统的行为变得可预测。对于高频交易来说,确定性比绝对速度更重要。

嗯,今天就聊到这里。操作系统优化是个细活,需要反复测试、验证。别指望一次改完就万事大吉。每台机器的硬件配置不同,内核版本不同,最优参数也会有差异。我的习惯是:每改一个参数,就跑一遍延迟测试,看看有没有改善。数据不会骗人。


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