操作系统调优:内核旁路、中断亲和性、CPU隔离与频率锁定
各位同学,今天我们来聊聊操作系统层面的延迟优化。说实话,很多做高频交易的朋友,一开始都把精力花在硬件上——换网卡、换交换机、缩短光纤。这些当然重要,但我个人经验是,操作系统这块要是没调好,硬件再牛也白搭。
为什么?因为操作系统本身就是一个巨大的延迟来源。你想想看,一个网络数据包从网卡到应用程序,中间要经过多少次中断、多少次上下文切换、多少次内核态到用户态的拷贝?我见过一个项目,换了最好的网卡,延迟反而没降多少,最后发现是中断亲和性没配好,CPU在多个核心之间来回跳。嗯,这种坑我踩过不止一次。
一、内核旁路:绕过那个慢吞吞的协议栈
先说说内核旁路。传统网络收发包,数据要经过内核协议栈——中断处理、内存拷贝、上下文切换,每一步都是延迟。我做过测试,光一个数据包从网卡到用户态应用,内核路径上就要消耗 5-10 微秒。在高频交易里,这简直是天文数字。
内核旁路的思路很简单:让应用程序直接跟网卡对话,绕过内核。常用的方案有 DPDK 和 Solarflare 的 OpenOnload。
核心原理:用户态驱动 + 轮询模式。网卡收到数据后,直接 DMA 到用户态内存,应用程序轮询检查,没有中断,没有拷贝。
我个人习惯用 DPDK。配置起来其实不复杂,核心就几步:
# 1. 加载 uio 驱动
modprobe uio_pci_generic
# 2. 绑定网卡到 DPDK
dpdk-devbind.py --bind=uio_pci_generic 0000:02:00.0
# 3. 分配大页内存
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 4. 启动应用时指定核心和内存
./my_app -l 0-3 -n 4 --socket-mem 1024
我的经验:大页内存一定要配够。我遇到过应用跑着跑着突然报内存不足,就是因为大页分配少了。建议预留物理内存的 30%-50% 给大页。
用 DPDK 之后,延迟能从微秒级降到纳秒级。但代价是什么?应用程序得自己处理协议栈。说白了,你得自己写 TCP 或者 UDP 的解析逻辑。嗯,这工作量不小。
避坑指南:我曾经在一个项目里直接用 DPDK 的 KNI(内核网络接口)来跟传统应用通信,结果延迟反而比原来还高。后来才发现 KNI 本质上又绕回了内核路径。所以,要么全用 DPDK,要么别用。
二、中断亲和性:别让 CPU 到处乱跑
好,假设你还没用内核旁路,还是走传统中断路径。那中断亲和性就是你必须优化的第一件事。
什么是中断亲和性?说白了,就是指定哪个 CPU 核心来处理哪个设备的中断。默认情况下,中断可以在任意核心上处理。这会导致什么问题?CPU 缓存频繁失效,性能抖动。
我建议的做法是:
- 先看看当前中断分配情况:
cat /proc/interrupts - 找到网卡对应的中断号(一般是 eth0 或者 ensX)
- 把中断绑定到固定的 CPU 核心上
# 查看中断
cat /proc/interrupts | grep eth0
# 假设中断号是 78,绑定到 CPU 核心 2
echo 2 > /proc/irq/78/smp_affinity
注意:smp_affinity 的值是位掩码。核心 0 对应 1,核心 1 对应 2,核心 2 对应 4,以此类推。想绑定到核心 2,就写 4(2的2次方)。
为什么要这么做?你想想看,如果中断在核心 0 和核心 1 之间来回跳,每次切换都要刷新 L1/L2 缓存。我实测过,绑定前后延迟抖动可以从 10 微秒降到 1 微秒以内。
我的习惯:把中断绑定到跟业务进程不同的核心上。比如业务跑在核心 0-3,中断就绑到核心 4-5。这样业务核心不会被中断打断,缓存更稳定。
三、CPU 隔离:给关键任务划个专属区域
CPU 隔离,英文叫 CPU Isolation。它的目的是让某些核心只跑你的业务进程,不被其他乱七八糟的进程打扰。
Linux 默认的调度器会把进程分配到所有核心上。你想想看,你的高频交易进程正在核心 0 上跑,突然系统调度了一个 cron 任务或者日志进程到同一个核心,那延迟瞬间就上去了。
隔离的方法有两种:
| 方法 | 配置方式 | 效果 |
|---|---|---|
| isolcpus 内核参数 | 在 grub 中设置 isolcpus=2,3 |
核心 2-3 不参与普通进程调度 |
| cpuset 控制组 | 使用 cgroup 的 cpuset 子系统 | 更灵活,可动态调整 |
# 方法一:grub 配置
# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"
# 更新 grub
grub2-mkconfig -o /boot/grub2/grub.cfg
# 方法二:cpuset 动态配置
mkdir /sys/fs/cgroup/cpuset/hts
echo 2-3 > /sys/fs/cgroup/cpuset/hts/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/hts/cpuset.mems
echo $PID > /sys/fs/cgroup/cpuset/hts/cgroup.procs
我曾经踩过的坑:只配了 isolcpus,没配 nohz_full。结果隔离的核心还在处理时钟中断,每毫秒一次,延迟抖动依然存在。所以 isolcpus 和 nohz_full 要一起用。
nohz_full 是啥?它让隔离的核心不再接收周期性时钟中断。rcu_nocbs 则是把 RCU 回调也挪走。这三个参数配合使用,才能做到真正的「干净」核心。
四、频率锁定:别让 CPU 偷懒
最后一个话题,CPU 频率锁定。现代 CPU 都有动态调频功能——负载低的时候降频省电,负载高的时候升频提性能。听起来很智能对吧?但在高频交易里,这就是灾难。
为什么?因为调频需要时间。从低频升到高频,少说也要几十微秒。你的交易信号来了,CPU 还在那慢悠悠地升频,等它反应过来,行情已经过去了。
解决方案:把 CPU 频率锁定在最高档,关闭所有节能特性。
# 查看当前调频策略
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 设置为 performance 模式
echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 批量设置所有核心
for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance > $cpu
done
额外建议:在 BIOS 里关闭 C-States 和 P-States。这些是硬件层面的节能状态,操作系统管不了。我见过有人在软件层配了 performance,但 BIOS 里还开着节能,结果延迟该高还是高。
还有个细节:关闭超线程(Hyper-Threading)。超线程虽然能提高吞吐,但会引入不可预测的延迟。两个逻辑核心共享物理核心的缓存和执行单元,一个核心的负载会影响到另一个。在高频交易里,确定性比吞吐量重要得多。
我的做法:在 BIOS 里直接关掉超线程。如果 BIOS 不支持,可以在 grub 里加 nosmt 参数。效果一样。
总结一下
这四个优化点,说白了就是一件事:消除不确定性。内核旁路消除了软件路径的不确定性,中断亲和性消除了中断分配的不确定性,CPU 隔离消除了调度干扰的不确定性,频率锁定消除了硬件调频的不确定性。
我个人的经验是,这四个优化做完,延迟能降 50%-80%。但要注意,它们不是孤立的,要配合使用。比如你做了 CPU 隔离,但没做中断亲和性,那中断还是可能打到隔离的核心上,等于白做。
嗯,操作系统调优这块内容不少,但核心思路就这些。下一章我们会聊聊网络协议栈的深度优化,包括 TCP 参数调优和用户态协议栈的实现。各位先把今天的内容消化一下,有问题随时交流。