低延迟交易机器:Linux 内核调优实战指南
部署一台低延迟交易机器,不仅仅是写快的代码。操作系统通过电源管理、中断处理、调度器负载均衡和内存管理引入各种抖动。本文将多年生产部署经验浓缩为一份系统化的调优指南——每个参数有解释,每个服务有理由,每项设置有验证。
核心目标
低延迟调优有两个目标:
- 最大化单核性能 —— 锁定 CPU 频率到最高,消除节能状态切换
- 最小化抖动 —— 防止中断、定时器、缺页异常和后台内核活动带来的意外延迟尖峰
这两个目标塑造了我们所有的调优决策。下面逐步展开。
你可以用 hiccups 工具测量抖动变化。隔离出 core 3 后,输出显示了巨大的差异:
$ hiccups | column -t -R 1,2,3,4,5,6
cpu threshold_ns hiccups pct99_ns pct999_ns max_ns
0 168 17110 83697 6590444 17010845
1 168 9929 169555 5787333 9517076
2 168 20728 73359 6008866 16008460
3 168 28336 1354 4870 17869
core 3 的最大抖动降到 18 微秒,其他核仍在数十毫秒量级。
CPU 电源管理
禁用 C-States
现代 CPU 实现了多级 C-states(空闲电源状态)。越深的 C-state 越省电,但唤醒延迟也越大——从 C6 恢复到 C0 可能需要数十微秒。在每一微秒都至关重要的交易系统中,这是不可接受的。
GRUB 参数:
intel_idle.max_cstate=0 processor.max_cstate=0 idle=poll
intel_idle.max_cstate=0—— 禁用 Intel idle 驱动,阻止 CPU 进入 C0 以外的任何 C-stateprocessor.max_cstate=0—— 禁用通用 ACPI idle 驱动作为后备idle=poll—— 将空闲循环替换为忙等轮询;CPU 永远不会真正休眠
权衡: 功耗大幅增加。一个在 C6 下只消耗约 5W 的核心,在 C0 下会消耗 80W 以上。对于延迟就是收入的交易机器来说,这是可以接受的。
实战提示: 在 Solarflare 机器上,还可以用 onload_tool disable_cstates 在运行时禁用 C-states,无需重启。
禁用 PCIe ASPM
PCIe ASPM(主动状态电源管理)会在 PCIe 链路空闲时将其置于低功耗状态(L0s、L1)。从 L1 唤醒链路会增加数微秒延迟——正是这种抖动会损害交易性能。
pcie_aspm=off
注意:一些旧文档使用 pcie_aspm.policy=performance,它保持链路在 L0 但允许一定的电源协商。pcie_aspm=off 更激进,是交易场景的首选。
禁用 HPET
高精度事件定时器(HPET)是传统的系统定时器,访问延迟比 TSC(时间戳计数器)高。在现代 Intel CPU 上,TSC 以基频运行且不变频,读取更快、精度更高。
hpet=disabled
内核会回退到基于 TSC 的定时器,访问速度显著更快(20-30ns vs HPET 的 1000+ns)。
BIOS 设置
在 BIOS/UEFI 层面:
- 将能源配置设为 最大性能
- 确保 Turbo Boost 已启用 —— 允许核心频率超过基频
- 禁用超线程(SMT)—— 详见下文
启动后验证:
cat /sys/devices/system/cpu/intel_pstate/no_turbo # 0 = turbo 已启用
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 应显示 "performance"
生产环境不要运行 turbostat —— 它会引入调度抖动。仅在搭建验证期间使用,验证完毕后禁用。
CPU 隔离
这是交易系统调优中影响最大的类别。核心思路很简单:为交易应用预留专用核心,阻止内核在这些核心上调度任何其他任务。
CPU 频率调节器
直接通过 sysfs 强制所有核心到最高频率:
find /sys/devices/system/cpu -name scaling_governor \
-exec sh -c 'echo performance > {}' ';'
或通过 tuned(后面详细说明):
tuned-adm profile latency-performance
从调度器中隔离核心
isolcpus=1-17
isolcpus 将指定 CPU 从内核的负载均衡器中移除。除非通过 taskset 或 sched_setaffinity() 显式绑定,否则不会有用户态进程被调度到这些核心上。这是 CPU 隔离的基础。
抑制定时器中断
nohz_full=1-17 nohz=on
nohz_full 抑制指定核心上的周期性调度时钟中断。没有这个参数,每个隔离核心每 1-4ms(取决于 CONFIG_HZ)就会收到一次定时器中断,造成不必要的唤醒和缓存污染。
前提条件: nohz_full 要求 isolcpus 设置在相同的核心上——内核不会在可能需要调度活动的核心上抑制定时。
nohz=on 全局启用无滴答内核模式。
注意: 只有当核心上恰好只有一个可运行线程时,tick 才会被抑制。查看 /proc/sched_debug 验证隔离核心上的 tick 抑制状态。
卸载 RCU 回调
rcu_nocbs=1-17
RCU(读-拷贝-更新)是内核中广泛使用的同步机制。当宽限期结束,RCU 回调会在发起它的 CPU 上执行。在隔离核心上,这些回调会引入抖动。
rcu_nocbs 将 RCU 回调处理卸载到管理核心(通常是 CPU 0),保持隔离核心干净。
禁用超线程
noht
超线程在两个逻辑核心之间共享 L1/L2 缓存和执行单元。对于需要确定性缓存访问的交易线程来说,这是个隐患——兄弟线程随时可能驱逐你的缓存行。
禁用 HT 使每个物理核心的 L1/L2 缓存有效翻倍,消除资源争用。代价是逻辑核心数减半,但对延迟关键线程来说,这永远是正确的权衡。
也可以在运行时禁用:
echo off > /sys/devices/system/cpu/smt/control
绑定内核线程和工作队列
即使设置了 isolcpus,一些内核线程和工作队列仍可能落在隔离核心上。把它们移走:
# 将内核线程移到 CPU 0
pgrep -P 2 | xargs -I{} taskset -pc 0 {}
# 将工作队列移到 CPU 0
find /sys/devices/virtual/workqueue -name cpumask -exec sh -c 'echo 1 > {}' ';'
或使用 tuna 获得更清晰的接口:
tuna --cpus=1-17 --isolate
内存管理
透明大页:禁用
transparent_hugepage=never
THP 通过页面压缩、TLB 击落和 khugepaged 内核线程导致数微秒的延迟尖峰。关于 THP、KSM、NUMA 平衡和 TLB 击落对低延迟系统为何有害的详细解释,参见虚拟内存与延迟。
也可以在运行时禁用 THP,无需重启:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
显式大页
在 2024 年的部署中,我们配置了:
default_hugepagesz=2M hugepagesz=1G hugepages=8 hugepagesz=2M hugepages=2048
这分配了:
- 8 × 1GB 大页 = 8GB,用于大型结构(LOB 快照、特征累加器)
- 2048 × 2MB 大页 = 4GB,用于环形缓冲区、较小的数据结构
运行时也可以调整:
sysctl -w vm.nr_hugepages=20
为什么用显式大页? 一个 4KB 页需要一个 TLB 条目。一个 2MB 页用一个 TLB 条目覆盖 512 倍的内存。对于遍历环形缓冲区或订单簿的交易应用,这意味着 TLB 未命中大幅减少——在现代 x86 上,每次 TLB 未命中代价约 100-200 个时钟周期(2.5GHz 下约 40-80ns)。
禁用 NUMA 自动平衡
echo 0 > /proc/sys/kernel/numa_balancing
NUMA 自动平衡会迁移页面,导致 TLB 击落和缺页异常。应该通过 numa_alloc_onnode() 或 mbind() 显式绑定内存。
禁用内核同页合并(KSM)
echo 0 > /sys/kernel/mm/ksm/run
KSM 引入页表锁竞争和 TLB 击落,交易机器上没有它的位置。
锁定应用内存
从应用层面,调用 mlockall() 防止缺页异常:
#include <sys/mman.h>
mlockall(MCL_CURRENT | MCL_FUTURE);
这锁定所有当前和未来的页面在 RAM 中,防止内核将它们换出。与大页结合,消除了两大抖动源:缺页异常和换页 I/O。
还要完全禁用交换分区:
swapoff -a
降低 VM 统计更新频率:
sysctl vm.stat_interval=120
禁用 CPU 漏洞缓解(可选)
mitigations=off
Spectre、Meltdown 等漏洞通过 retpoline、KPTI(内核页表隔离)和 IBRS 等技术缓解。这些缓解措施给每个系统调用和间接分支增加开销——通常 5-15% 的性能损失。
对于在隔离网络上、防火墙后面的交易机器,这是一个可接受的风险。请仔细评估你的威胁模型。
Intel 缓存分配技术(CAT)
如果 CPU 支持 Intel CAT,可以对末级缓存(LLC)分区,把大多数 cache way 分配给延迟敏感应用。这能防止其他进程污染交易线程依赖的缓存。
使用 pqos(来自 intel-cmt-cat 包)配置:
pqos -e "llc:1=0xff;llc:2=0xf" # 类 1 分 8 路,类 2 分 4 路
pqos -a "pid:1=$(pgrep trading_app);pid:2=$(pgrep monitoring)"
避免 TLB 击落
启动时映射好所有内存,之后不释放。禁用 THP、KSM 和自动 NUMA 平衡。避免热路径上的可写文件映射。完整的击落触发源和消除策略参见虚拟内存与延迟。
监控:
egrep 'TLB|CPU' /proc/interrupts
perf stat -e 'tlb:tlb_flush' -a -A --timeout 10000
中断处理
禁用 IRQ 均衡器并绑定中断
systemctl stop irqbalance
systemctl disable irqbalance
irqbalance 将硬件中断分配到所有 CPU 以实现”公平”。在交易机器上,这意味着你的隔离核心会被网卡、磁盘和定时器中断打扰。
方案 1 —— 完全禁用 irqbalance 并手动绑定(我们的生产环境做法):
for ((i=1; i<=500; i++)); do
echo "1" > /proc/irq/$i/smp_affinity
done
这向每个 IRQ 的亲和掩码写入 1(仅 CPU 0),确保所有硬件中断都不会落在隔离核心上。
方案 2 —— 使用 irqbalance 一次性模式(它会尊重 isolcpus):
irqbalance --foreground --oneshot
或手动 ban 指定核心(core 3 = 位掩码 0x8):
IRQBALANCE_BANNED_CPUS=8 irqbalance --foreground --oneshot
更精细的控制可以将特定网卡中断绑定到专用核心:
# 将网卡队列 0 的中断绑定到 CPU 0
echo 1 > /proc/irq/<irq_number>/smp_affinity
查看 /proc/interrupts 找到各设备对应的 IRQ 编号。
irqaffinity GRUB 参数
irqaffinity=0
这在启动时将所有中断的默认 CPU 亲和性设为 CPU 0,早于任何用户态配置。这是一个安全网,确保在早期启动阶段也不会有中断落在隔离核心上。
网络栈调优
套接字缓冲区大小
股票机器接收行情数据:
sysctl -w net.core.rmem_max=1073741824 # 1GB 最大接收缓冲区
sysctl -w net.core.rmem_default=4194304 # 4MB 默认
期货机器消息率更高:
sysctl -w net.core.rmem_default=16777216 # 16MB
sysctl -w net.core.rmem_max=16777216 # 16MB
sysctl -w net.core.wmem_default=16777216 # 16MB
sysctl -w net.core.wmem_max=16777216 # 16MB
为什么这么大? 如果应用短暂暂停(例如 GC 事件或处理突发数据),内核缓冲区需要在不丢包的情况下吸收行情数据。以典型的 CTP 行情速率(持续约 200MB/天,峰值约 50MB/小时),16MB 缓冲区提供数分钟的余量。
持久化到 /etc/rc.local:
# /etc/rc.local
sysctl -w net.core.rmem_max=1073741824
sysctl -w net.core.rmem_default=4194304
chmod +x /etc/rc.d/rc.local
ethtool 中断合并
ethtool -C eth<N> rx-usecs 0 adaptive-rx off
中断合并延迟 RX 中断以批量通知。rx-usecs 设置延迟微秒数——设为 0 表示立即交付每个数据包。
adaptive-rx off 阻止内核动态调整合并参数,否则会引入不可预测的延迟。
验证:
ethtool -c eth<N>
注意: 在使用 Solarflare Onload(内核旁路)的网卡上,这不那么重要,因为数据包不经过内核栈。但对于走内核路径的网卡(如 Intel X710),这个设置至关重要。
内核旁路替代方案
要获得最低的网络延迟,考虑完全绕过 Linux 内核网络栈:
- DPDK —— 开源、厂商中立的内核旁路框架
- OpenOnload / EFVI(Solarflare)—— 对基于 socket 的应用透明加速
- Mellanox VMA —— 针对 Mellanox 网卡的加速消息库
- Exablaze / SolarCapture —— 超低延迟抓卡
如果必须走内核栈,上面的 ethtool 和缓冲区调优就至关重要。
服务禁用
每个后台服务都是潜在的抖动源。以下是我们禁用的服务及原因:
| 服务 | 用途 | 禁用原因 |
|---|---|---|
abrt-ccpp | 核心转储处理 | 核心转储导致磁盘 I/O 和 CPU 峰值 |
abrt-oops | 内核 oops 报告 | 不必要的磁盘活动 |
abrt-vmcore | 内核崩溃转储报告 | 同上 |
abrtd | ABRT 守护进程 | 编排上述服务;禁用整个子系统 |
firewalld | 防火墙规则 | 交易机器位于专用防火墙后;iptables 足够 |
ipmi / ipmievd | IPMI 硬件监控 | 可能触发 SMI(系统管理中断)——不可屏蔽,延迟不可预测 |
kdump | 崩溃转储工具 | 预留大量内存;崩溃处理用于调试而非生产 |
postfix | 邮件 MTA | 完全不必要;后台磁盘 I/O |
rhel-domainname | NIS 域名 | 遗留组件,不必要 |
cpuspeed | CPU 频率调节 | 我们要求最大频率 |
systemctl disable abrt-ccpp.service abrt-oops.service abrt-vmcore.service \
abrtd.service firewalld.service ipmi.service ipmievd.service \
kdump.service postfix.service rhel-domainname.service
额外加固
# 禁用 SELinux
vi /etc/selinux/config
SELINUX=disabled
# 禁用 SSH DNS 查询(防止登录延迟)
vi /etc/ssh/sshd_config
UseDNS no
# 禁用 NMI 看门狗
nmi_watchdog=0 # GRUB 参数
# 禁用审计
audit=0 # GRUB 参数
# 禁用软锁死检测器
nosoftlockup # GRUB 参数
# 禁用 halt 计费
nohalt # GRUB 参数
NMI 看门狗(nmi_watchdog=0):看门狗使用性能计数器检测挂起的 CPU,会定期生成 NMI,可能抢占交易线程。禁用它。
审计(audit=0):Linux 审计框架记录安全事件。命中审计规则的每个系统调用都会增加开销。在没有合规审计要求的交易机器上,完全禁用。
nosoftlockup:软锁死检测器监控超过 20 秒未调度的 CPU。在运行忙等循环的隔离核心上,这会不断触发。禁用它。
nohalt:阻止内核在空闲循环中使用 halt 指令。与 idle=poll 结合,确保 CPU 永远不进入任何低功耗状态。
tuned-adm:快速配置
tuned-adm profile network-latency
network-latency 配置文件应用了一批合理的默认值:
- 设置 CPU 调度器为
performance - 禁用透明大页
- 增大 net.core.somaxconn 和 net.ipv4.tcp_max_syn_backlog
- 禁用 NUMA 自动平衡
- 设置 kernel.sched_min_granularity_ns 降低调度延迟
它不会做的: 不会设置 isolcpus、nohz_full 或 rcu_nocbs。这些需要用新的 GRUB 参数重启,所以上面的手动内核调优仍然必要。
验证当前配置:
tuned-adm active
Solarflare Onload(内核旁路)
要获得最低的网络延迟,Solarflare 网卡配合 OpenOnload 实现内核旁路——数据包通过 EFVI 接口直接从网卡投递到用户态,跳过整个 Linux 网络栈。
在线安装
# 安装依赖
yum install -y gcc gcc-c++ python-devel libpcap-devel automake \
libtool rpm-build kernel-devel
# 方法 1:RPM 构建
rpmbuild -ta openonload-*.tgz
rpm -ivh *.rpm
# 方法 2:直接安装
tar xzf openonload-<version>.tgz
cd openonload-<version>/scripts
./onload_install
重要: kernel-devel 版本必须与当前运行的内核完全匹配。用 uname -r 和 rpm -qa | grep kernel-devel 检查。
离线安装
在无法联网的交易机器上,预先下载压缩包使用方法 2。还需要从 CentOS ISO 搭建本地 yum 源来安装构建依赖。
安装后配置
# 加载 Onload 模块
onload_tool reload
# 验证
lsmod | grep sfc
lsmod | grep onload
# 设置固件为极低延迟模式
sfboot firmware-variant=ultra-low-latency
# 刷写固件
sfupdate --write
# 重启使固件更改生效
reboot
sfboot firmware-variant=ultra-low-latency 将网卡固件配置为优先延迟而非吞吐量。这会禁用一些卸载功能(如 TX 校验和卸载和 LRO),以换取最小的数据包处理延迟。
通过 Onload 禁用 C-States
onload_tool disable_cstates
这是 GRUB C-state 参数的运行时替代方案。在不能重启但需要立即禁用 C-states 时很有用。
Intel X710(i40e)驱动
对于非 Solarflare 网卡,可能需要从源码编译驱动:
tar -xf x710-i40e-2.12.6.tar.gz
cd i40e-2.12.6/src
make
make install
这确保你拥有带有延迟修复的最新驱动,而不是内核自带的旧版驱动。
调度策略
在被隔离的核上运行一个使用忙轮询的单一线程时,优先选择 SCHED_OTHER,而不是 SCHED_FIFO 或 SCHED_RR。实时优先级的线程如果永不让出 CPU,会饿死内核任务(比如 vmstat),可能锁死系统。
Linux 默认把实时任务限制在 CPU 时间的 95%(/proc/sys/kernel/sched_rt_runtime_us)。如果需要实时调度,请相应调整这个上限——但对于大多数在隔离核上运行交易负载的场景,SCHED_OTHER 加忙等轮询已经足够,也更安全。
完整 GRUB 配置
综合以上所有内容,以下是一台 18 核机器(核心 1-17 隔离)的生产 GRUB 配置:
GRUB_CMDLINE_LINUX="crashkernel=auto rhgb quiet \
isolcpus=1-17 \
nohz_full=1-17 \
nohz=on \
rcu_nocbs=1-17 \
idle=poll \
intel_idle.max_cstate=0 \
processor.max_cstate=0 \
mce=ignore_ce \
nmi_watchdog=0 \
transparent_hugepage=never \
pcie_aspm=off \
hpet=disabled \
nosoftlockup \
audit=0 \
irqaffinity=0 \
noht \
nohalt \
skew_tick=1 \
default_hugepagesz=2M \
hugepagesz=1G hugepages=8 \
hugepagesz=2M hugepages=2048"
编辑 /etc/default/grub 后:
grub2-mkconfig -o /boot/grub2/grub.cfg
# UEFI 系统:
# grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
reboot
注意: skew_tick=1 偏移每个 CPU 的定时器节拍,避免所有 CPU 同时唤醒,减少内核数据结构的锁竞争。
验证
应用所有调优后,逐一验证:
CPU 和电源
# 检查 CPU 调度器
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 期望:performance
# 检查 Turbo Boost
cat /sys/devices/system/cpu/intel_pstate/no_turbo
# 期望:0
# 检查 HT/SMT 状态
cat /sys/devices/system/cpu/smt/active
# 期望:0
# 检查 C-states(应仅显示 C0)
cat /sys/module/intel_idle/parameters/max_cstate
# 期望:0
隔离
# 检查隔离的 CPU
cat /sys/devices/system/cpu/isolated
# 期望:1-17
# 检查 nohz_full
cat /sys/devices/system/cpu/nohz_full
# 期望:1-17
# 检查 RCU nocbs
cat /sys/devices/system/cpu/rcu_nocbs
# 期望:1-17
内存
# 检查大页
cat /proc/meminfo | grep Huge
# 期望:HugePages_Total 与配置匹配
# 检查 THP
cat /sys/kernel/mm/transparent_hugepage/enabled
# 期望:always never [never]
# 检查 NUMA 平衡
cat /proc/sys/kernel/numa_balancing
# 期望:0
抖动测量
最终的验证是测量实际系统抖动:
# 测量每 CPU 上下文切换(10 秒)
perf stat -e 'sched:sched_switch' -a -A --timeout 10000
# 测量每 CPU 定时器中断(30 秒)
perf stat -e 'irq_vectors:local_timer_entry' -a -A --timeout 30000
# 监控 TLB 未命中
perf stat -e 'dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses' -a --timeout 10000
# 监控 TLB 击落
perf stat -e 'tlb:tlb_flush' -a -A --timeout 10000
持续抖动监控,我们使用 sysjitter 工具:
cd sysjitter-ace/
./longtest.sh
它在每个核心上运行忙等循环并测量与预期时间的最大偏差——任何尖峰都表示内核干扰。在正确调优的机器上,最大抖动应低于 5μs。
快速检查清单
以下是所有步骤的顺序总结:
- BIOS/UEFI: 最大性能、Turbo Boost 开启、HT 关闭
- GRUB: 应用所有内核参数(见上方完整配置)
- tuned-adm:
profile network-latency - 服务: 禁用 abrt、firewalld、ipmi、kdump、postfix、irqbalance
- SELinux: 禁用
- SSH:
UseDNS no - 交换分区:
swapoff -a - IRQ: 全部绑定到 CPU 0,禁用 irqbalance
- 网络缓冲区: 根据工作负载设置
rmem_max/wmem_max - ethtool: 内核路径网卡设置
rx-usecs 0 adaptive-rx off - Solarflare: 安装 Onload,
sfboot firmware-variant=ultra-low-latency - 大页: 通过 GRUB 或
sysctl分配 - 验证: 运行所有验证命令和 sysjitter