第4章:操作系统内核调优:CPU隔离、中断绑定与内存大页,榨干Linux最后一滴性能

说实话,很多做量化交易的朋友,花了大价钱买服务器,堆了最好的网卡和CPU,结果延迟还是下不去。问题出在哪?十有八九是操作系统没调好。

Linux内核默认是为通用场景设计的——它要照顾数据库、Web服务、桌面应用,甚至你后台跑的定时任务。但咱们做高频交易,要的是极致的确定性。说白了,就是让CPU只干一件事,别被乱七八糟的中断和调度打断。

这一章,我就带你把这几个关键点调明白。CPU隔离、中断绑定、内存大页,这三板斧下去,延迟能降一个数量级。

核心目标:让交易进程独占CPU核心,避免上下文切换和缓存污染,同时减少TLB miss。

4.1 CPU隔离:把交易进程关进“单间”

Linux默认的进程调度器,会把你的交易进程在各个核心之间来回迁移。每次迁移,CPU缓存就废了——L1、L2里的数据全得重新加载。这在微秒级别的交易里,简直是灾难。

我个人的习惯是,用 isolcpus 内核参数把几个核心隔离出来。这些核心不会被普通进程使用,只有你指定的进程才能跑在上面。

4.1.1 配置isolcpus

在GRUB启动参数里加上:

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

解释一下:

  • isolcpus:把核心2-5从调度器中移除,普通进程不会跑上去
  • nohz_full:关闭这些核心的周期性时钟中断,减少不必要的打断
  • rcu_nocbs:把RCU回调从这些核心上卸载,避免后台任务干扰

我的经验:隔离的核心数量不要太多。我一般留2-4个核心给交易进程,剩下的给系统和其他服务。你想想看,如果所有核心都隔离了,系统连网卡中断都没地方处理了。

4.1.2 使用cpuset做精细控制

如果不想重启机器,可以用cpuset动态分配:

# 创建cpuset
mkdir /sys/fs/cgroup/cpuset/trading
echo 2-5 > /sys/fs/cgroup/cpuset/trading/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/trading/cpuset.mems

# 把交易进程放进去
echo $TRADING_PID > /sys/fs/cgroup/cpuset/trading/cgroup.procs

嗯,这里要注意:cpuset的配置在重启后会失效。所以生产环境我建议直接改GRUB参数,一劳永逸。

4.2 中断绑定:别让网卡中断打扰交易核心

网卡收到行情数据时,会产生中断。默认情况下,中断可能落在任何一个核心上。如果恰好落在你的交易核心上,那延迟就炸了——交易进程被中断处理程序抢占了CPU。

我曾经遇到过一个问题:某次回测时延迟突然飙升到200微秒,查了半天发现是网卡中断和交易进程抢同一个核心。从那以后,我养成了绑定中断的习惯。

4.2.1 查看当前中断分布

cat /proc/interrupts | grep eth0

你会看到类似这样的输出:

  CPU0 CPU1 CPU2 CPU3
 98:  1234   56   78   90  IR-PCI-MSI  eth0-TxRx-0
 99:   567   89  123   45  IR-PCI-MSI  eth0-TxRx-1

4.2.2 绑定中断到指定核心

把网卡中断绑定到非交易核心上:

# 将eth0的中断绑定到CPU0和CPU1
echo 3 > /proc/irq/98/smp_affinity
echo 3 > /proc/irq/99/smp_affinity

这里的 3 是位掩码,二进制是 0011,表示CPU0和CPU1。如果绑定到CPU2,就是 4(二进制 0100)。

避坑指南:我曾经犯过一个错——把中断全部绑定到一个核心上。结果那个核心被中断打爆了,反而导致中断处理延迟增加。正确的做法是:把中断分散到2-3个非交易核心上,每个核心处理一部分队列。

4.3 内存大页:减少TLB Miss,提升内存访问速度

交易系统里,内存访问是最大的瓶颈之一。尤其是行情数据量大的时候,频繁的TLB miss会让内存访问延迟翻倍。

Linux默认的内存页大小是4KB。对于动辄几十GB的共享内存来说,页表项多得吓人。TLB(快表)根本装不下,每次访问内存都要查好几级页表。

说白了,大页就是把页大小从4KB变成2MB甚至1GB。页表项少了,TLB命中率自然就上去了。

4.3.1 配置HugePages

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

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

在代码里使用大页:

#include <sys/mman.h>

void* addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                  -1, 0);

4.3.2 透明大页(THP)的坑

Linux还支持透明大页(Transparent Huge Pages),它会自动把连续的内存页合并成大页。听起来很美,但实际用起来问题很多。

我个人的建议是:关掉THP,手动管理大页。为什么?

  • THP的合并和拆分过程会触发内存 compaction,导致延迟抖动
  • THP对共享内存的支持不好,容易造成内存浪费
  • 手动管理大页,你能精确控制哪些内存用大页,哪些不用
# 关闭透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

我的经验:对于行情共享内存,我一般用2MB的大页。对于订单簿这种频繁更新的数据结构,用4KB小页反而更好——因为大页的TLB miss惩罚虽然小,但大页的分配和释放开销更大。你想想看,频繁分配释放2MB的大页,那延迟反而上去了。

4.4 知识体系总览

下面这张图,是我自己总结的延迟优化知识体系。你可以对照着看,这一章讲的是最底层的内核调优部分。

跨市场交易系统延迟优化知识体系 硬件层 CPU选型 · 网卡(Solarflare/Mellanox) · 内存频率 · PCIe通道 内核调优 ← 本章重点 CPU隔离(isolcpus) · 中断绑定(smp_affinity) · 内存大页(HugePages) 关闭THP · nohz_full · rcu_nocbs · cpuset 用户态优化 DPDK · 共享内存 · 无锁队列 · 内存池 · NUMA亲和性 应用层 低延迟协议 · 行情解析 · 订单路由 · 风控引擎 延迟监控与持续优化

4.5 实战检查清单

调优完成后,我建议你按这个清单检查一遍:

检查项 命令 期望结果
CPU隔离生效 taskset -cp $$ 交易进程只跑在隔离核心上
中断绑定正确 cat /proc/interrupts 网卡中断不在交易核心上
大页已启用 grep HugePages /proc/meminfo HugePages_Total > 0
THP已关闭 cat /sys/kernel/mm/transparent_hugepage/enabled 输出 always [never]
nohz_full生效 cat /sys/devices/system/cpu/isolated 显示隔离的核心列表

重要提醒:调优完成后,一定要做压力测试。我曾经在测试环境调得完美,上线后却发现隔离的核心被某个内核线程偷偷占用了。用 perf top 盯一段时间,确认没有异常中断和调度。

好了,这一章的内容就到这里。内核调优是延迟优化的基石,但也是最容易被忽视的环节。你想想看,如果操作系统都在跟你抢资源,再好的算法也跑不出低延迟。

把这些配置加到你的部署脚本里,让每一台交易服务器都跑在最佳状态。

专注资料整理