4. 操作系统调优:内核参数调优、中断亲和性与CPU隔离
操作系统调优,说白了就是给交易系统铺一条“高速公路”。
我见过太多团队,代码写得漂亮,硬件也舍得砸钱,结果系统跑起来就是卡顿。查到最后,问题往往出在操作系统层面——内核参数没调、中断乱跑、CPU被抢。嗯,今天我们就来把这几个坑填上。
4.1 内核参数调优:让网络栈“飞”起来
内核参数调优,是操作系统调优里最立竿见影的部分。我个人习惯,拿到一台新机器,第一件事就是改这些参数。
4.1.1 net.core.rmem_default 与 net.core.rmem_max
这两个参数控制的是UDP接收缓冲区的大小。为什么重要?因为高频交易里,UDP是主流——组播行情、订单回报,全走UDP。
默认值是多少?我记得在CentOS 7上,rmem_default是212992字节,也就是208KB。对于普通应用够了,但对于高频交易,这太小了。
我在项目中遇到过,行情数据量稍微一爆发,UDP缓冲区就满了,内核直接丢包。那感觉,就像你在高速上开车,前面突然出现一个坑,你只能眼睁睁看着轮胎碾过去。
推荐配置:
# 设置接收缓冲区默认值和最大值(单位:字节)
net.core.rmem_default = 16777216 # 16MB
net.core.rmem_max = 33554432 # 32MB
# 发送缓冲区同理
net.core.wmem_default = 16777216
net.core.wmem_max = 33554432
避坑指南:我曾经把rmem_max设成128MB,结果内存占用飙升,反而影响了性能。16MB到32MB是个安全区间,够用又不浪费。
4.1.2 kernel.sched_migration_cost_ns
这个参数控制的是进程迁移的“成本估算”。内核用它来判断,把一个进程从CPU A迁移到CPU B,是否划算。
默认值是500000纳秒,也就是0.5毫秒。对于高频交易来说,这个值太大了。你想想看,一个微秒级的交易决策,内核却花了0.5毫秒去考虑要不要迁移进程——这不是添乱吗?
我个人习惯,直接把这个值设成0:
kernel.sched_migration_cost_ns = 0
这样内核就不会因为“迁移成本”而犹豫,该迁移就迁移。当然,这也有代价——进程可能会更频繁地迁移,但高频交易场景下,我们更关心的是确定性,而不是省那点迁移开销。
注意:设成0后,如果系统负载很高,可能会增加上下文切换。建议配合CPU隔离使用,效果更好。
4.2 中断亲和性(IRQ affinity):把中断“钉”在指定CPU上
中断亲和性,说白了就是告诉内核:这个中断,只能由某个CPU处理。
为什么要这么做?因为默认情况下,中断可以在任意CPU上处理。这会导致一个问题——你的交易线程在CPU 0上跑得好好的,突然网卡中断来了,CPU 0被迫去处理中断,交易线程就被打断了。
我在项目中遇到过,某次回测时发现延迟抖动特别大,查了半天,发现是网卡中断和交易线程抢CPU。后来把中断绑定到单独的CPU上,抖动立刻消失了。
4.2.1 查看当前中断亲和性
# 查看网卡中断号
cat /proc/interrupts | grep eth0
# 查看某个中断的亲和性(假设中断号是 78)
cat /proc/irq/78/smp_affinity
smp_affinity是一个十六进制掩码。比如00000001表示只能由CPU 0处理,00000002表示只能由CPU 1处理,00000003表示CPU 0和CPU 1都可以处理。
4.2.2 设置中断亲和性
# 将中断78绑定到CPU 2(掩码为00000004)
echo 00000004 > /proc/irq/78/smp_affinity
最佳实践:
- 把网卡中断绑定到CPU 0或CPU 1
- 把交易线程绑定到CPU 2、3、4等
- 确保中断CPU和交易CPU不重叠
4.3 CPU隔离(isolcpus、cpuset):给交易线程“包场”
CPU隔离,是操作系统调优的终极手段。它的目标很简单:让某些CPU只跑你的交易线程,其他任何东西都不准上去。
4.3.1 isolcpus 内核参数
在GRUB启动参数里加上isolcpus=2,3,4,5,内核就不会把普通进程调度到这些CPU上。但注意,中断和内核线程还是可能跑上去的。
我刚开始用isolcpus时,以为设了就万事大吉。结果发现,网卡中断还是时不时跑到隔离的CPU上。后来才明白,isolcpus只隔离用户态进程,不隔离中断。
解决方案:配合中断亲和性一起用。把中断绑定到非隔离的CPU上,隔离的CPU就彻底干净了。
4.3.2 cpuset:更精细的隔离
cpuset是cgroup的一个子系统,可以更精细地控制进程的CPU亲和性。相比isolcpus,cpuset更灵活——你可以在系统运行时动态调整。
# 创建cpuset
mkdir /sys/fs/cgroup/cpuset/trading
# 设置CPU范围
echo 2-5 > /sys/fs/cgroup/cpuset/trading/cpuset.cpus
# 设置内存节点(通常为0)
echo 0 > /sys/fs/cgroup/cpuset/trading/cpuset.mems
# 把交易进程加入cpuset
echo 12345 > /sys/fs/cgroup/cpuset/trading/cgroup.procs
这样,PID为12345的交易进程就只能跑在CPU 2-5上。
我的经验:
- isolcpus适合“一锤子买卖”——启动时就定好
- cpuset适合“动态调整”——比如行情线程和交易线程分开管理
- 两者可以结合使用:isolcpus做粗粒度隔离,cpuset做细粒度控制
4.4 知识体系总览
下面这张图,是我自己总结的操作系统调优核心逻辑。你看一眼,就能明白这三块内容是怎么配合的。
4.5 实战检查清单
每次部署新机器,我都会按这个清单检查一遍:
| 检查项 | 命令/方法 | 期望值 |
|---|---|---|
| UDP接收缓冲区 | sysctl net.core.rmem_default |
16777216 |
| 进程迁移成本 | sysctl kernel.sched_migration_cost_ns |
0 |
| 中断亲和性 | cat /proc/irq/*/smp_affinity |
中断CPU与交易CPU不重叠 |
| CPU隔离 | cat /proc/cmdline | grep isolcpus |
隔离了指定CPU |
| cpuset配置 | cat /sys/fs/cgroup/cpuset/trading/cpuset.cpus |
交易进程在隔离CPU上 |
最后提醒一句:调完参数一定要重启验证。我曾经在测试环境调好了,上线时忘了同步配置,结果生产环境还是默认值。嗯,那天的延迟数据,我都不想看第二眼。