第三章:操作系统调优——让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快速打开,减少握手延迟 |
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
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,以此类推。
3.5 本章知识体系
说了这么多,我们来画个图,把这一章的核心逻辑串起来:
你看,这四个方面其实是环环相扣的。内核参数调优是基础,CPU隔离和绑核保证线程不被干扰,内存大页减少访存延迟,中断亲和性避免网卡中断打断交易线程。每一步都做到位了,你的系统才能真正做到“稳定、可预测的低延迟”。
好了,这一章的内容就到这里。操作系统调优是个细活,需要反复测试和调整。别指望一次就能调到位。我当年也是踩了无数坑,才总结出这些经验。希望你能少走一些弯路。
公众号:蓝海资料掘金营,微信deep3321