3、操作系统内核调优:让Linux为高频交易“量身定制”

做高频交易,硬件选好了,网卡插上了,你以为就完事了?

远远不够。操作系统如果不调,就像给法拉利装了拖拉机的变速箱。我见过太多团队,硬件投入几百万,结果延迟就是降不下来,一查,全是内核参数没动。

说白了,Linux默认是为通用场景设计的。它要照顾文件服务器、Web服务器、数据库……唯独没考虑过“微秒级延迟”这种极端需求。所以,我们必须亲手给它“动手术”。

核心目标:把操作系统的“不确定性”降到最低。让CPU只干交易这一件事,让网络数据包以最短路径到达用户态程序。

3.1 网络栈调优:给数据包铺一条“高速公路”

网络延迟是高频交易的头号敌人。数据包从网卡到你的应用程序,中间要经过内核协议栈。每一步都可能成为瓶颈。

3.1.1 套接字缓冲区与TCP参数

我个人习惯,先把TCP缓冲区调大。为什么?因为默认值太小,在高吞吐场景下容易丢包或触发拥塞控制,导致延迟抖动。

# 调整TCP读写缓冲区(单位:字节)
net.core.rmem_max = 134217728    # 128MB
net.core.wmem_max = 134217728    # 128MB
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728

这里有个坑:tcp_rmem的三个值分别代表最小值、默认值和最大值。我建议把最大值设大,但默认值不要太大,否则内存浪费严重。

3.1.2 关闭Nagle算法与延迟确认

Nagle算法是为了减少小包数量,但它会故意延迟发送。这对高频交易是致命的。你想想看,一个订单指令就几十个字节,等它攒够了再发,黄花菜都凉了。

# 在应用程序中关闭Nagle
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));

# 内核层面关闭延迟确认
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_no_metrics_save = 1

避坑指南:我曾经在一个项目中,只调了Nagle没关延迟确认,结果延迟还是高。后来发现是内核的tcp_ack_delay在作怪。建议直接关闭:net.ipv4.tcp_delack_min = 0

3.1.3 网卡多队列与RPS/RFS

现代网卡支持多队列,每个队列可以绑定到不同CPU核心。这样数据包就能被并行处理,避免单核瓶颈。

# 查看网卡队列数
ethtool -l eth0

# 设置队列数(假设支持)
ethtool -L eth0 combined 8

# 设置RPS(Receive Packet Steering)
echo "f0" > /sys/class/net/eth0/queues/rx-0/rps_cpus

嗯,这里要注意:RPS的掩码是十六进制,f0表示CPU 4-7。我习惯把网卡中断和RPS绑定到同一组核心上,避免跨NUMA访问。

3.2 CPU隔离与亲和性:让核心“专一”

Linux的进程调度器会公平地分配CPU时间。但公平意味着切换,切换意味着上下文开销。高频交易程序需要的是“独占”。

3.2.1 隔离CPU核心

通过内核启动参数isolcpus,可以把指定核心从调度器中“摘出来”。普通进程不会跑到这些核心上,只有你手动绑定的进程才能用。

# 在GRUB配置中添加
GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7"

# 更新GRUB
grub2-mkconfig -o /boot/grub2/grub.cfg

这里我多说一句:nohz_fullrcu_nocbs是配套的。它们能减少时钟中断和RCU回调对隔离核心的干扰。我刚开始做调优时没加这两个参数,结果隔离效果大打折扣。

3.2.2 设置CPU亲和性

隔离完核心,还得把交易进程绑上去。用tasksetsched_setaffinity都行。

# 将进程绑定到CPU 4
taskset -c 4 ./trading_engine

# 或者用C代码
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(4, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);

注意:不要把系统关键进程(如sshd、systemd)绑到隔离核心上。否则系统出问题你连登录都登不上去。我一般留2个核心给操作系统,剩下的全隔离。

3.3 中断绑定:让网卡“直通”CPU

网卡收到数据包后,会触发硬件中断。默认情况下,中断可能落在任意核心上。这会导致缓存污染和延迟抖动。

3.3.1 查看并绑定中断

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

# 绑定中断到指定CPU(假设中断号78)
echo "10" > /proc/irq/78/smp_affinity

smp_affinity的值是十六进制掩码。10表示CPU 4(二进制00010000)。我习惯把网卡中断和交易进程绑到同一个核心上,这样数据包从中断处理到应用层,全程不换核。

3.3.2 使用irqbalance?不,关掉它

irqbalance服务会自动均衡中断。但在高频交易场景下,它带来的不确定性远大于好处。我建议直接关掉:

systemctl stop irqbalance
systemctl disable irqbalance

3.4 大页内存:减少TLB Miss

内存分页默认是4KB。交易程序如果占用大量内存,页表会非常庞大,导致TLB(页表缓存)频繁失效。大页内存(HugePages)可以把页表缩小到2MB甚至1GB,显著降低TLB Miss。

3.4.1 配置大页内存

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

# 或者通过sysctl永久配置
vm.nr_hugepages = 1024

然后,在应用程序中使用mmap分配大页内存:

#include <sys/mman.h>
void *addr = mmap(NULL, 2 * 1024 * 1024, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

避坑指南:我曾经遇到一个问题:大页预留了,但程序就是分配不到。后来发现是vm.nr_overcommit_hugepages没设置。建议把这个值也设大一点,比如vm.nr_overcommit_hugepages = 256

3.4.2 透明大页?关掉它

Linux的透明大页(THP)会自动合并小页。听起来很美,但合并过程会触发内存 compaction,导致延迟抖动。在高频交易中,这是不可接受的。

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

知识体系总览

下面这张图,是我自己总结的调优框架。你可以把它当作一个检查清单:

Linux内核调优知识体系 网络栈调优 • TCP缓冲区调大 • 关闭Nagle & 延迟确认 • 网卡多队列 + RPS/RFS • 关闭irqbalance 目标:减少网络延迟抖动 CPU隔离与亲和性 • isolcpus隔离核心 • nohz_full减少时钟中断 • rcu_nocbs减少RCU回调 • taskset绑定进程 目标:独占CPU,避免上下文切换 中断绑定 • smp_affinity绑定中断 • 中断与进程同核 • 避免跨NUMA中断 • 关闭irqbalance 目标:数据包直达,不换核 大页内存 • 预留HugePages • mmap使用MAP_HUGETLB • 关闭透明大页(THP) • 设置overcommit 目标:减少TLB Miss 协同 调优

这四个方面不是孤立的。网络栈调优减少外部延迟,CPU隔离和中断绑定减少内部干扰,大页内存减少内存访问延迟。它们共同作用,才能把延迟压到极致。

最后说一句:调优不是一蹴而就的。我每次调完参数,都会用perfftrace做一遍profile,看看瓶颈到底在哪。有时候,一个参数没调对,效果就差一个数量级。嗯,多试几次,找到最适合你硬件和业务的那组参数。


专注资料整理