操作系统调优:Linux内核参数调优与实时内核
做市商报价路由,说白了就是跟时间赛跑。我见过太多团队,代码写得贼漂亮,算法也牛,结果一上生产环境,延迟就是降不下来。最后查来查去,问题出在操作系统层面。嗯,今天我们就聊聊这块。
我个人习惯,拿到一台新机器,第一件事不是装软件,而是先调内核参数。为什么?因为默认的Linux内核是为通用场景设计的,它要照顾桌面用户、服务器用户、数据库用户……唯独没专门为低延迟交易优化过。你想想看,一个做市商系统,每秒可能要处理几万笔报价,内核随便一个调度抖动,可能就是几微秒的延迟——在交易领域,这已经够亏一大笔钱了。
核心思路:让CPU专心做交易计算,别被其他杂事打扰。让内存访问更快,别让缺页中断拖后腿。让网络包走最短路径,别在协议栈里绕来绕去。
CPU隔离:把交易线程关进「单间」
CPU隔离,我习惯叫它「绑核」。说白了,就是告诉内核:这几个CPU核心你别乱动,留给我专用的交易线程。
具体怎么做?在Linux启动参数里加 isolcpus。比如我有16个核,想留4个给交易用:
# 在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 中添加
isolcpus=12-15 nohz_full=12-15 rcu_nocbs=12-15
这里我解释一下:isolcpus 把核心12到15从内核的调度器中移除,普通进程不会跑上去。nohz_full 关闭这些核心的周期性时钟中断——这个很重要,时钟中断会打断你的交易线程。rcu_nocbs 把RCU回调也挪走,进一步减少干扰。
我的经验:我曾经在一个项目中,只做了isolcpus,没加nohz_full。结果延迟还是不稳定,抓包一看,每4毫秒就有一个微小的抖动。加上nohz_full后,抖动直接消失了。所以这三个参数,我建议一起上。
然后,用 taskset 或 cset 把你的交易进程绑上去:
# 把进程PID绑定到CPU 12
taskset -p 0x1000 $PID
# 或者用cset,更灵活
cset shield --cpu 12-15 --kthread=on
cset shield --shield --pid $PID
中断亲和性:别让网卡中断乱跑
网卡收到一个网络包,会触发一个硬中断。默认情况下,这个中断可能跑到任何一个CPU上。如果你的交易线程在CPU 0上跑,结果中断跑到了CPU 1,那还好。但如果中断跑到了CPU 0上,你的交易线程就被打断了——这就是延迟抖动。
解决方案:把网卡中断绑定到非交易核心上。
# 查看网卡中断号
cat /proc/interrupts | grep eth0
# 假设中断号是 130-137,把它们绑定到CPU 0-3
echo 0f > /proc/irq/130/smp_affinity
echo 0f > /proc/irq/131/smp_affinity
# ... 以此类推
这里 0f 是十六进制掩码,表示CPU 0-3。如果你的交易核心是12-15,那中断就绑到0-11上。这样,交易核心永远不会被中断打扰。
注意:有些网卡支持多队列(RSS),每个队列有自己的中断。这种情况下,你可以把不同队列的中断绑到不同CPU上,实现负载均衡。但记住,交易核心要避开。
内存大页:减少TLB misses
内存分页,默认是4KB一页。你的交易程序如果用了大量内存,TLB(页表缓存)很快就会撑爆。每次TLB miss,都要去查页表,这又是几十纳秒的延迟。
大页(HugePages)把页大小变成2MB甚至1GB。TLB能覆盖的内存范围瞬间大了几百倍。说白了,就是让CPU少跑腿。
# 分配1024个2MB大页
echo 1024 > /proc/sys/vm/nr_hugepages
# 查看是否成功
cat /proc/meminfo | grep HugePages
在程序里,用 mmap 加上 MAP_HUGETLB 标志来分配大页内存。或者用 libhugetlbfs 库,更方便。
我的习惯:我一般会分配比实际需要多10%的大页。为什么?因为有些内存分配器会有碎片,多留点余量。曾经有一次我算得刚刚好,结果程序启动时分配失败,查了半天才发现是大页用完了。
TCP/IP协议栈优化:让网络包飞得更快
做市商系统,网络是命脉。TCP协议栈默认的参数,是为可靠性和公平性设计的,不是为低延迟设计的。我们需要调整几个关键参数。
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| net.core.rmem_max | 212992 | 134217728 | 接收缓冲区最大值,调大减少丢包 |
| net.core.wmem_max | 212992 | 134217728 | 发送缓冲区最大值 |
| net.ipv4.tcp_rmem | 4096 87380 6291456 | 4096 87380 134217728 | TCP接收缓冲区,最后一个值调大 |
| net.ipv4.tcp_wmem | 4096 16384 4194304 | 4096 16384 134217728 | TCP发送缓冲区 |
| net.core.busy_poll | 0 | 50 | 忙等模式,减少中断延迟 |
| net.core.busy_read | 0 | 50 | 读操作忙等,配合busy_poll使用 |
还有一个我特别喜欢的参数:net.ipv4.tcp_fastopen。它允许在SYN包中携带数据,减少一次RTT。对于做市商这种高频短连接场景,效果很明显。
# 开启TCP快速打开
echo 3 > /proc/sys/net/ipv4/tcp_fastopen
另外,SO_BUSY_POLL 这个socket选项也值得提一下。它让应用程序在收包时主动轮询网卡,而不是等中断。代价是CPU占用会高一些,但延迟能降不少。
实时内核(RT-Preempt):什么时候该上?
RT-Preempt内核,说白了就是把Linux变成一个硬实时系统。它通过修改内核的锁机制、调度器,让任务响应时间变得可预测。
但我要说句实话:不是所有做市商系统都需要RT内核。我个人判断标准有三条:
- 延迟要求是否在10微秒以下?如果是,考虑RT内核。
- 你的系统是否有多线程竞争?如果大量使用锁、条件变量,RT内核能减少优先级反转带来的抖动。
- 你愿意牺牲多少吞吐量?RT内核因为增加了抢占点,吞吐量通常会下降5%-10%。
我的建议:先用标准内核+CPU隔离+中断亲和性,把延迟优化到极致。如果还达不到要求,再上RT内核。我曾经在一个项目中,标准内核优化后延迟已经到5微秒了,客户非要上RT内核,结果只降了0.5微秒,吞吐量反而掉了8%。得不偿失。
安装RT内核很简单,以Ubuntu为例:
apt-get install linux-image-rt-amd64
然后重启,选RT内核启动。之后用 chrt 把你的交易线程设为实时优先级:
# 设为FIFO调度策略,优先级99
chrt -f -p 99 $PID
这里要注意,实时优先级范围是0-99,99最高。别把所有线程都设成99,否则低优先级的实时线程可能永远得不到CPU。
避坑指南:我曾经把看门狗线程也设成了实时优先级,结果它把CPU占满了,交易线程反而被饿死。所以,只对关键的交易线程设实时优先级,其他线程保持默认就好。
本章小结
操作系统调优,没有银弹。CPU隔离、中断亲和性、内存大页、TCP/IP优化、RT内核,每个工具都有它的适用场景。我的经验是:先做测量,再做优化。用 perf、ftrace 这些工具找出真正的瓶颈,然后对症下药。
记住一句话:不要优化你还没测量过的东西。 很多团队一上来就开大页、绑核、上RT内核,结果延迟没降多少,系统反而变得不稳定。嗯,稳扎稳打,才是做市商系统的王道。
最后分享一个实用技巧:每次修改内核参数后,用 sysctl -p 重载,然后用 cyclictest 测试延迟抖动。我习惯在优化前后各跑一次,对比数据。如果优化后抖动反而变大了,那说明参数调错了,赶紧回滚。