4、操作系统与内核优化:Linux内核调优、CPU亲和性与隔离、内存大页与NUMA绑定
做高频交易的人都知道,硬件选型只是第一步。真正让系统跑出纳秒级延迟的,是操作系统层面的调优。说白了,Linux内核默认是为通用场景设计的——它要照顾数据库、Web服务器、桌面应用。但咱们做高频交易,要的是极致的确定性。
我个人习惯,拿到一台新机器后,第一件事就是关掉所有不必要的内核服务。然后才是今天要聊的三个核心话题:内核调优、CPU亲和性与隔离、内存大页与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