第三章:操作系统内核调优

说到低延迟交易系统,很多人第一反应是硬件要快、网卡要贵。嗯,这没错。但我要说,操作系统这层要是没调好,再贵的硬件也是白搭。我见过不少团队,花了几十万买FPGA加速卡,结果Linux内核的默认配置就把延迟吃掉了大半。

这一章,咱们就聊聊Linux内核调优的几个关键点。说白了,就是让操作系统别来添乱。

3.1 irqbalance:中断的平衡艺术

先说说irqbalance。这是个啥?它负责把硬件中断请求(IRQ)分配到不同的CPU核心上。默认情况下,Linux会开启irqbalance服务,让中断均匀分布。

听起来挺合理对吧?但在低延迟场景下,这恰恰是个问题。

注意:irqbalance会让中断在多个核心间跳来跳去。每次跳转都有缓存丢失的开销。对于微秒级延迟敏感的交易系统,这种不确定性是致命的。

我个人习惯的做法是:关掉irqbalance。然后手动把网卡中断绑定到指定的CPU核心上。

# 停止irqbalance服务
systemctl stop irqbalance
systemctl disable irqbalance

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

# 手动绑定中断到CPU 2(假设中断号是78)
echo 2 > /proc/irq/78/smp_affinity

这里有个坑,我曾经踩过:smp_affinity用的是位掩码。CPU 0对应bit 0,CPU 1对应bit 1,以此类推。所以CPU 2的掩码是4(二进制100),而不是2。嗯,这个细节容易搞错。

3.2 isolcpus:把CPU圈起来

接下来是isolcpus。这个内核参数的作用,是把指定的CPU核心从Linux的进程调度器中隔离出来。

你想想看,默认情况下,Linux调度器会在所有核心上跑来跑去。你的交易线程正在CPU 3上跑着,突然系统要处理一个定时器中断,啪,把你的线程踢到CPU 4上去了。这一踢,缓存全没了,延迟直接飙升。

isolcpus就是解决这个问题的。它在内核启动时就把某些核心标记为「隔离区」,普通进程和中断不会跑上去。

核心思路:把交易线程钉在隔离的核心上,其他系统任务别来打扰。

配置方法很简单,在GRUB启动参数里加一行:

isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3

这表示CPU 2和3被隔离。我一般还会加上nohz_fullrcu_nocbs,后面会细说。

记得更新GRUB配置:

grub2-mkconfig -o /boot/grub2/grub.cfg

重启后,用taskset把交易进程绑定到隔离核心上:

taskset -c 2,3 ./trading_engine

3.3 nohz_full:关掉时钟中断

好,核心隔离了,但还有个问题:时钟中断。

Linux内核默认每秒会产生1000次时钟中断(取决于HZ配置)。每次中断都会打断CPU上正在运行的线程。对于隔离的核心来说,这简直是灾难。

nohz_full参数的作用,就是让指定的核心完全关闭时钟中断。只有在有任务切换或外部事件时,才会产生中断。

经验之谈:我建议至少留一个核心不隔离,专门处理系统任务和中断。比如4核CPU,可以隔离CPU 2和3,让CPU 0和1处理系统任务。

完整的GRUB配置示例:

GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 intel_pstate=disable processor.max_cstate=1"

这里我还加了intel_pstate=disableprocessor.max_cstate=1,目的是禁用CPU的深度睡眠状态。为什么?因为CPU从睡眠状态唤醒需要时间,这个延迟不可控。

3.4 CPU亲和性:把线程钉死

隔离了核心,还得把线程绑上去。这就是CPU亲和性(affinity)做的事。

除了前面提到的taskset,在代码里也可以设置:

#include <sched.h>

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

pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);

我习惯在交易引擎启动时,把每个关键线程都显式绑定。比如行情接收线程绑到CPU 2,订单管理线程绑到CPU 3。这样每个线程都有自己的专属核心,互不干扰。

注意:绑定前要确认核心确实被隔离了。否则Linux调度器可能会把其他进程也调度上去,造成竞争。

3.5 内存大页:减少TLB缺失

最后聊聊HugePages。这是个老生常谈的话题,但确实重要。

默认情况下,Linux内存页大小是4KB。对于交易系统这种需要大量内存访问的场景,TLB(页表缓存)很容易被撑爆。每次TLB缺失,CPU都要去查页表,这个开销在微秒级别——对于交易系统来说,已经很大了。

HugePages把页大小提升到2MB甚至1GB。同样的内存空间,需要的页表项大大减少,TLB命中率自然就上去了。

配置方法:

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

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

然后在程序里使用大页:

// 使用mmap分配大页内存
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

或者通过libhugetlbfs库透明地使用:

export LD_PRELOAD=libhugetlbfs.so
export HUGETLB_MORECORE=yes
./trading_engine
一个小技巧:我习惯在系统启动时就预留好大页,而不是运行时动态分配。因为动态分配可能会失败,而且分配过程本身也有延迟。

3.6 本章知识体系

说了这么多,我画张图帮你理清思路:

Linux内核调优核心框架 低延迟内核调优 irqbalance 关闭自动中断均衡 isolcpus 隔离CPU核心 nohz_full 关闭时钟中断 HugePages 减少TLB缺失 手动绑定中断到指定核心 配合taskset绑定交易线程 配合rcu_nocbs使用 mmap + MAP_HUGETLB 分配 目标:减少中断干扰 + 消除调度抖动 + 降低内存延迟

这张图把四个调优方向串起来了。核心思路就一个:让交易线程独占资源,减少一切不必要的干扰

嗯,这一章的内容就这些。调优是个细致活,每个参数都要根据你的硬件和业务场景来调整。我建议你先在测试环境里验证,确认没问题了再上生产。

一句话总结:关掉irqbalance,隔离核心,关掉时钟中断,用上大页。这四步做完,你的Linux内核就基本「干净」了。


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