引言
Jane Street 的软件工程师 Tudor Brindus 在一次技术分享中,系统性地剖析了低延迟系统中抖动(Jitter)的来源与治理方法。抖动指的是输入处理时间的偏差。在高频交易这类对延迟极其敏感的场景里,控制抖动直接关系到交易风险——如果数据处理延迟了哪怕 10 微秒,都可能导致一次错误交易。
Tudor 用一个简单的内存乒乓应用作为切入点,详细演示了如何识别并解决由 Linux 内核与微架构条件造成的常见抖动源。

一个玩具示例:在机器内的多核间分发消息
Tudor 用一个乒乓案例(Ping-Pong)讲解单机多进程之间的消息传递机制。别看示例简单,它实际上对应着真实场景——比如从交易所接收市场数据行情。这类数据量非常庞大,大约 1 GiB/s,同时对延迟极度敏感。他特别强调,抖动会让交易滞后,哪怕只是 1 毫秒或 10 微秒,也可能引发错误的交易决策,这对交易公司来说是不可接受的。
引入环形缓冲区
环形缓冲区(Ring Buffer)是 Tudor 介绍的第一个核心概念。它是一个具有固定大小单元的数组,数据到达末尾后会重新循环到起点。在单机的两个进程之间建立环形缓冲区,就可以实现高效的数据交换。

乒乓示例中有两个进程:pinger 和 ponger,以及两个环形缓冲区,用来在这两个进程之间传递数据。具体做法是在缓冲区中写入和读取整数,实现数据的往返传输:

- 发球者(Pinger)在发球者到接球者的环形缓冲区第一个单元中写入整数
1。
- 接球者(Ponger)读取该值
1,加 1 得到 2,然后将其写回接球者到发球者的环形缓冲区。
- 发球者读取回来的值,继续加
1 得到 3,然后写入环形缓冲区的下一个单元。
- 当到达环形缓冲区末尾时,循环重新开始。
Tudor 用 OCaml 代码展示了这个逻辑,并讨论了延迟的测量方法:记录发球者写入值和接球者完成处理的时间戳,计算延迟 T2 - T1。
let ping_pong consumer producer =
while true do
let read_buf = Ringbuf.Consumer.poll consumer in
if not (Iobuf.is_empty read_buf)
then (
let data = Iobuf.Consume.int64_le_trunc read_buf in
let new_data = data + 1 in
let write_buf = Ringbuf.Producer.write_buf producer in
Iobuf.Fill.int64_le write_buf new_data;
Ringbuf.Producer.finish_write producer;
Ringbuf.Consumer.finish_poll consumer
)
done
;;

Jane Street 非常重视 OCaml,但 OCaml 基本上是一个单线程的运行时环境。要实现并行性,就需要使用多个进程,并在它们之间传递共享内存。Tudor 介绍了如何在同一台机器上的两个进程之间传递内存,用环形缓冲区来完成数据交换。
基准测试设置
- 典型的英特尔服务器
- 第二代英特尔至强金牌 CPU @ 3.6GHz(Cascade Lake)
- 2666 MHz DDR4 内存
- Linux 3.10

基准测试的初步结果
在基准测试中,Tudor 使用了一台典型的 Intel 服务器(第二代 Cascade Lake,主频 3.6GHz)。测试数据显示,抖动最高可达 16 微秒。那问题就来了:抖动到底从何而来?
优化前的初始尝试:虚拟化环境与裸机硬件
Tudor 在优化时首先对比了虚拟化环境(Virtualized Environment)和裸机硬件(Bare Metal Hardware)的性能差异。结果显示,裸机硬件在延迟和抖动控制方面有明显优势。
Try 1: 虚拟化环境
在虚拟机中运行测试时,延迟结果表现为:
- 延迟:抖动范围高达 16 微秒
- 特征:存在大量噪声,尤其在延迟的长尾部分(例如 1 微秒以上)。可能原因是邻居噪声问题(Noisy Neighbor Problem),即同一物理主机上的其他虚拟机占用了 CPU 周期;另外还可能存在虚拟机自身的开销,比如上下文切换和虚拟化相关的中断处理。
问题分析:虚拟机引入了大量不可控的延迟,尤其是尾部延迟。对于高性能低延迟系统来说,这种不确定性会严重影响稳定性。

Try 2: 裸机硬件
把相同的测试程序从虚拟化环境切换到裸机硬件(直接在物理主机上运行)后,结果显著改善:
- 延迟:从 637 纳秒降至 223 纳秒
- 特征:大部分噪声被消除,延迟曲线更加平滑,尤其是尾部延迟显著改善
优化分析:
- 无虚拟化开销:裸机硬件直接运行程序,避免了虚拟机带来的中断处理和邻居噪声问题。
- 更高的性能确定性:裸机硬件的延迟更可预测,不确定性更少。

总结:虚拟化与裸机的对比
Tudor 强调:如果性能和延迟确定性非常重要,应优先选择裸机硬件而非虚拟机环境。裸机硬件可能增加基础设施成本,但在高性能低延迟系统中,它能带来更稳定可靠的运行环境。不过,即使在裸机环境下,仍然存在一些延迟峰值(例如 1 微秒),还需要继续深入优化。
深入识别并消除抖动源
虚拟化环境的影响
如前所述,在虚拟机中运行程序会遇到邻居噪声问题,共享主机的其他用户会抢占资源,虚拟机自身也带来额外开销。切换到裸机后,延迟从 637 纳秒降到 223 纳秒,噪声大幅减少。但裸机下仍存在 1 微秒级别的延迟峰值,需要进一步排查。
网络中断导致的抖动
面对裸机上仍然存在的抖动,Tudor 使用了 MagicTrace 工具。MagicTrace 可以生成包含调用栈深度和时间轴的图表,帮助定位抖动来源。通过分析延迟峰值,他发现网络活动会通过中断机制干扰 CPU 的正常运行:当网络设备收到数据包时,会通过中断通知 CPU,这些中断会打断正在运行的应用程序。



解决方法:
- 使用内核参数
isolcpus 隔离特定的 CPU 核心,让这些核心不处理网络中断。
- 将需要运行的进程绑定到隔离的 CPU 核心上(例如通过
taskset 命令)。
优化效果:系统延迟从 223 纳秒降到 194 纳秒,抖动也显著减少。

定时器中断的影响
进一步分析显示,本地定时器中断也是抖动来源。默认情况下,内核每秒会触发 1000 次定时器中断,用于调度和统计。通过设置 nohz_full 参数,可以关闭这些定时器中断。

解决方法:
- 使用内核参数
nohz_full 关闭定时器中断,让内核在特定 CPU 核心上尽量减少中断频率。
优化效果:定时器中断频率从每秒一千次降到每秒一次,延迟进一步降低 24 纳秒,来到了 170 纳秒左右。
处理器频率调整的影响
Turbo Boost 这类处理器频率调整技术会引入频率转换的延迟,造成抖动。禁用 Turbo Boost 可以提高延迟的一致性,尽管平均性能可能略有下降。实验中,禁用后系统延迟稳定在 164 纳秒。

微架构级别的优化
即便完成了上述所有优化,仍有一些抖动存在。Tudor 指出,这可能与推测执行(Speculative Execution)有关。在高频循环中,CPU 会猜测下一个操作,一旦猜错就会产生错误推测(Bad Speculation),带来额外性能开销。


在循环中加入 pause 指令,可以提示处理器减少错误推测。对于 OCaml 代码,需要在编译器中引入对应的内联函数(Intrinsic)。这一步优化让延迟进一步降到了 163 纳秒。
关于推测执行的代码示例
let ping_pong consumer producer =
while true do
let read_buf = Ringbuf.Consumer.poll consumer in
if not (Iobuf.is_empty read_buf)
then (
let data = Iobuf.Consume.int64_le_trunc read_buf in
let new_data = data + 1 in
let write_buf = Ringbuf.Producer.write_buf producer in
Iobuf.Fill.int64_le write_buf new_data;
Ringbuf.Producer.finish_write producer;
Ringbuf.Consumer.finish_poll consumer
)
done
;;
硬件选择的重要性
最后,Tudor 还提到了硬件本身对性能的影响。更高频率的 CPU 和更快的内存可以显著提升性能。但过度超频可能导致系统不稳定,甚至出现内存错误,因此需要在性能和稳定性之间找到平衡点。
关键总结
通过这一系列优化,Tudor 将系统延迟从最初的 637 纳秒降至 163 纳秒,抖动也大幅减少。

主要经验教训包括:
- 避免在虚拟化环境中运行对延迟敏感的应用。
- 深入理解系统架构,针对性地进行优化。
- 硬件选择至关重要,但需要平衡性能和稳定性。
- 持续监测系统性能,及时发现并解决问题。
总结
Tudor 的这次分享深入揭示了低延迟系统中减少抖动的多种方法。从软件层面的参数调整到微架构级别的优化,每一步都需要对系统有深刻的理解。对于从事高频交易、实时系统或任何对延迟敏感领域的工程师来说,这些经验都具有很高的参考价值。如果你也在做低延迟系统调优,欢迎来云栈社区分享你的实践经验。