第三章:操作系统调优——让Linux为高频交易“裸奔”

做高频交易,说白了就是跟时间赛跑。你写的代码再快,操作系统不给力,一切都白搭。我见过太多团队,策略逻辑写得天衣无缝,结果一上生产环境,延迟直接飙到几十微秒。查来查去,问题出在操作系统上——内核调度、中断处理、内存分配,每一个环节都在“偷”你的时间。

这一章,我们就来聊聊怎么把Linux调教成一台纯粹的交易机器。嗯,让它“裸奔”起来。

3.1 内核参数调优:把“不必要”的东西关掉

Linux内核默认是为通用场景设计的。它要照顾服务器、桌面、嵌入式,什么活儿都得干。但对我们做市商来说,很多功能都是累赘。我个人的习惯是,先关掉一切不必要的服务,再针对网络和内存做专项优化。

3.1.1 网络栈调优

高频交易最依赖的就是网络。网卡收包、协议栈处理,每一步都可能成为瓶颈。下面这几个参数,是我在项目中必调的:

参数 推荐值 说明
net.core.rmem_max 134217728 接收缓冲区最大值,128MB
net.core.wmem_max 134217728 发送缓冲区最大值,128MB
net.ipv4.tcp_rmem 4096 87380 134217728 TCP接收缓冲区(最小、默认、最大)
net.ipv4.tcp_wmem 4096 65536 134217728 TCP发送缓冲区
net.core.netdev_budget 600 一次轮询处理的数据包数量
net.ipv4.tcp_fastopen 3 启用TCP快速打开,减少握手延迟
我的经验: 别盲目调大缓冲区。缓冲区太大反而会增加延迟。我一般先设一个保守值,然后用工具(比如perf)观察丢包率和延迟分布,再逐步调整。

3.1.2 关闭不必要的内核特性

有些内核特性,对高频交易来说就是“毒药”。比如透明大页(Transparent Huge Pages),它会让内存分配变得不可预测。我曾经在一个项目里,因为没关这个,导致交易系统的延迟抖动从2微秒飙到了15微秒。排查了整整两天才找到原因。

# 关闭透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 关闭NUMA balancing
echo 0 > /proc/sys/kernel/numa_balancing

# 关闭CPU频率调节
echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
注意: 关闭CPU频率调节后,CPU会一直运行在最高频率,功耗会上升。但做高频交易,这点电费算什么?

3.2 CPU隔离与绑核:让交易线程“独占”CPU

你有没有想过,为什么你的交易线程明明优先级很高,延迟还是不稳定?原因很简单——操作系统会时不时地打断它,去处理别的任务。比如网卡中断、定时器、甚至是别的进程。

解决方案就是CPU隔离。把一部分CPU核心“隔离”出来,不让操作系统在上面跑任何其他任务。然后,把你的交易线程绑定到这些核心上。

3.2.1 使用isolcpus内核参数

在GRUB启动参数里加上isolcpus,就能把指定核心隔离出来。比如:

isolcpus=2,3,4,5 nohz_full=2,3,4,5 rcu_nocbs=2,3,4,5

这个配置的意思是:CPU 2-5号核心被隔离,同时关闭这些核心上的时钟中断(nohz_full),并且把RCU回调也挪走(rcu_nocbs)。

嗯,这里要注意:隔离后的核心,连系统时钟中断都不会有。你的交易线程可以一直跑,不会被任何东西打断。

3.2.2 使用taskset绑核

隔离完CPU,接下来就是把你的交易进程绑上去。我习惯用taskset命令:

# 把进程PID 1234绑定到CPU 2和3上
taskset -cp 2,3 1234

# 启动时直接绑定
taskset -c 2,3 ./trading_engine

如果你用的是C++,也可以在代码里用sched_setaffinity系统调用:

#include <sched.h>

void bind_to_cpu(int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);
    
    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) != 0) {
        // 处理错误
    }
}
核心原则: 一个核心只跑一个线程。别把多个交易线程绑到同一个核心上,那样反而会引入竞争。

3.3 内存大页:减少TLB Miss

内存访问延迟,是高频交易的另一大杀手。默认情况下,Linux使用4KB大小的内存页。这意味着,如果你要访问2MB的数据,需要查512次页表。每次查页表,都可能触发TLB Miss,一次TLB Miss就是几十纳秒的延迟。

解决方案?用大页。2MB甚至1GB的大页,能大幅减少TLB Miss。

3.3.1 配置HugePages

首先,预留大页内存:

# 预留1024个2MB的大页
echo 1024 > /proc/sys/vm/nr_hugepages

# 查看是否成功
grep HugePages_Total /proc/meminfo

然后,在代码里使用大页:

#include <sys/mman.h>

void* alloc_hugepage(size_t size) {
    void* addr = mmap(NULL, size, 
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                      -1, 0);
    if (addr == MAP_FAILED) {
        // 处理错误
    }
    return addr;
}

3.3.2 透明大页 vs 显式大页

前面我提到要关掉透明大页。为什么?因为透明大页是内核自动管理的,它会在运行时合并或拆分页,这个过程不可预测。而显式大页(就是我们手动预留的)是固定的,不会变。

我个人的建议是:关掉透明大页,用显式大页。虽然配置起来麻烦一点,但换来的是稳定的延迟。

避坑指南: 我曾经在一个项目里,用了透明大页,结果发现内存碎片化严重,导致大页分配失败。从那以后,我再也不敢在生产环境用透明大页了。

3.4 中断亲和性:让网卡中断“定点”处理

网卡收到数据包后,会触发一个中断。默认情况下,这个中断可能被分配到任何一个CPU核心上处理。如果恰好分配到了你的交易线程所在的核心,那你的线程就会被中断,延迟瞬间飙升。

解决方案是设置中断亲和性,把网卡中断固定到某个或某几个核心上,跟交易线程的核心分开。

3.4.1 查看中断号

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

输出大概长这样:

  CPU0    CPU1    CPU2    CPU3
 128:   12345       0       0       0   IR-PCI-MSI  eth0-TxRx-0
 129:       0   12345       0       0   IR-PCI-MSI  eth0-TxRx-1
 130:       0       0   12345       0   IR-PCI-MSI  eth0-TxRx-2
 131:       0       0       0   12345   IR-PCI-MSI  eth0-TxRx-3

3.4.2 设置亲和性

假设你的交易线程在CPU 2和3上,那把网卡中断固定到CPU 0和1上:

# 把中断128(eth0-TxRx-0)绑定到CPU 0
echo 1 > /proc/irq/128/smp_affinity

# 把中断129绑定到CPU 1
echo 2 > /proc/irq/129/smp_affinity

smp_affinity的值是一个位图。1表示CPU0,2表示CPU1,4表示CPU2,8表示CPU3,以此类推。

最佳实践: 把网卡中断和交易线程放在不同的NUMA节点上。如果可能,让网卡中断在某个核心上处理,然后通过共享内存把数据传给交易线程,避免跨NUMA访问。

3.5 本章知识体系

说了这么多,我们来画个图,把这一章的核心逻辑串起来:

操作系统调优知识体系 内核参数调优 CPU隔离与绑核 内存大页 中断亲和性 网络栈调优 关闭透明大页 关闭NUMA balancing isolcpus内核参数 taskset绑核 sched_setaffinity 配置HugePages mmap + MAP_HUGETLB 显式大页 vs 透明大页 查看中断号 设置smp_affinity NUMA感知分配 最终目标:稳定、可预测的低延迟

你看,这四个方面其实是环环相扣的。内核参数调优是基础,CPU隔离和绑核保证线程不被干扰,内存大页减少访存延迟,中断亲和性避免网卡中断打断交易线程。每一步都做到位了,你的系统才能真正做到“稳定、可预测的低延迟”。

好了,这一章的内容就到这里。操作系统调优是个细活,需要反复测试和调整。别指望一次就能调到位。我当年也是踩了无数坑,才总结出这些经验。希望你能少走一些弯路。


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