yuqi-zheng

低延迟交易机器:Linux 内核调优实战指南


部署一台低延迟交易机器,不仅仅是写快的代码。操作系统通过电源管理、中断处理、调度器负载均衡和内存管理引入各种抖动。本文将多年生产部署经验浓缩为一份系统化的调优指南——每个参数有解释,每个服务有理由,每项设置有验证。


核心目标

低延迟调优有两个目标:

  1. 最大化单核性能 —— 锁定 CPU 频率到最高,消除节能状态切换
  2. 最小化抖动 —— 防止中断、定时器、缺页异常和后台内核活动带来的意外延迟尖峰

这两个目标塑造了我们所有的调优决策。下面逐步展开。

你可以用 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-state
  • processor.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 从内核的负载均衡器中移除。除非通过 tasksetsched_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内核崩溃转储报告同上
abrtdABRT 守护进程编排上述服务;禁用整个子系统
firewalld防火墙规则交易机器位于专用防火墙后;iptables 足够
ipmi / ipmievdIPMI 硬件监控可能触发 SMI(系统管理中断)——不可屏蔽,延迟不可预测
kdump崩溃转储工具预留大量内存;崩溃处理用于调试而非生产
postfix邮件 MTA完全不必要;后台磁盘 I/O
rhel-domainnameNIS 域名遗留组件,不必要
cpuspeedCPU 频率调节我们要求最大频率
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 降低调度延迟

它不会做的: 不会设置 isolcpusnohz_fullrcu_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 -rrpm -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_FIFOSCHED_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。


快速检查清单

以下是所有步骤的顺序总结:

  1. BIOS/UEFI: 最大性能、Turbo Boost 开启、HT 关闭
  2. GRUB: 应用所有内核参数(见上方完整配置)
  3. tuned-adm: profile network-latency
  4. 服务: 禁用 abrt、firewalld、ipmi、kdump、postfix、irqbalance
  5. SELinux: 禁用
  6. SSH: UseDNS no
  7. 交换分区: swapoff -a
  8. IRQ: 全部绑定到 CPU 0,禁用 irqbalance
  9. 网络缓冲区: 根据工作负载设置 rmem_max / wmem_max
  10. ethtool: 内核路径网卡设置 rx-usecs 0 adaptive-rx off
  11. Solarflare: 安装 Onload,sfboot firmware-variant=ultra-low-latency
  12. 大页: 通过 GRUB 或 sysctl 分配
  13. 验证: 运行所有验证命令和 sysjitter

参考资料