2. 全链路延迟拆解:从行情到达到订单成交的每一纳秒

做高频系统这么多年,我最大的体会是:延迟不是优化出来的,是量出来的

你想想看,如果你连每一纳秒花在哪都不知道,谈何优化?

这一章,我们就来干一件事——把一条完整的交易链路,从行情数据到达网卡,到订单最终被交易所确认,拆成一个个可测量的环节

2.1 一条交易指令的“一生”

先画个全景图。我个人习惯把全链路分成四大段:

  1. 行情接收段:从网卡收到UDP组播,到解析出订单簿快照
  2. 策略计算段:从新行情触发策略逻辑,到生成下单指令
  3. 订单发送段:从下单指令序列化,到TCP/UDP包离开网卡
  4. 交易所处理段:从交易所收到订单,到成交回报返回

每一段里,都有几个“吃时间的大户”。

核心观点:全链路延迟不是加法,是乘法。你优化了A段10纳秒,B段可能因此多等20纳秒。所以必须整体看。

2.2 行情接收段:从线缆到内存

行情数据从交易所出来,第一站是网卡。这里我拆成5个步骤:

步骤 典型延迟 说明
物理层传输 ~5ns/m 光纤中光速约2/3c,1米线缆约5纳秒
网卡接收 100-300ns 取决于网卡型号和中断模式
内核协议栈 500-2000ns 标准Linux TCP/IP栈,UDP稍快
用户态拷贝 200-500ns 从内核缓冲区拷贝到应用内存
行情解析 100-500ns 二进制协议解码,重建订单簿

嗯,这里要注意。最坑的是内核协议栈。我在项目中遇到过,同样的硬件,用标准socket收行情,延迟抖动能达到±1微秒。后来换成DPDK,直接降到200纳秒以内,抖动也控制在50纳秒。

避坑指南:我曾经以为网卡硬件时间戳是万能的。后来发现,如果PTP时钟同步没做好,时间戳本身就有几百纳秒的误差。所以,先校准时钟,再谈测量。

2.3 策略计算段:CPU在忙什么

行情解析完,策略引擎开始干活。这段延迟主要取决于:

  • 数据依赖:策略是否要等多个行情源对齐?
  • 计算复杂度:简单的价差计算 vs 机器学习模型
  • 内存访问:L1 cache命中 vs 主存访问,差几十倍

我见过最夸张的情况:一个策略在行情更新后,花了3微秒去查数据库。3微秒啊!足够光信号跑600米了。后来改成内存哈希表,直接降到200纳秒。

关键数字:L1 cache命中约1ns,L3约10ns,主存约100ns。所以,热数据必须留在L1

2.4 订单发送段:从指令到线缆

策略决定下单后,指令要经过:

  1. 序列化:把订单结构体转成二进制协议(50-100ns)
  2. 系统调用:send()或write()(100-300ns)
  3. 内核协议栈:TCP分段、窗口管理(500-2000ns)
  4. 网卡发送:DMA传输、MAC封装(100-200ns)

这里有个坑:系统调用。每次send()都要切到内核态,光上下文切换就几十纳秒。我建议用批处理:攒够N个订单再一次性发送。或者用内核旁路技术,比如Solarflare的onload,直接把用户态和网卡打通。

警告:千万别在发送路径上做任何内存分配!new/malloc都可能触发page fault,一次缺页中断就是几微秒的抖动。我见过一个团队,就因为下单时打印了一行日志,延迟从5微秒飙到50微秒。

2.5 交易所处理段:黑盒里的时间

订单离开你的网卡后,进入交易所的黑盒。这段我们控制不了,但必须测量:

  • 网络传输:取决于物理距离,每公里约5微秒
  • 交易所撮合:一般10-50微秒
  • 成交回报:同样路径返回

怎么测?用回环测试。发一个订单,等成交回报回来,记录总时间。然后减去你的发送和接收延迟,剩下的就是交易所处理时间。

我记得有一次,我们测出来交易所处理时间突然从20微秒变成200微秒。排查了半天,发现是交易所升级了系统,我们的订单协议版本不匹配,被丢到了慢速队列。所以,一定要监控这个黑盒延迟

2.6 全链路延迟分解图

下面这张图,是我自己项目里用的延迟分解模板。每个环节都标了典型值和优化目标。

全链路延迟分解(典型值) 行情接收 500-3000ns 策略计算 200-5000ns 订单发送 300-2000ns 交易所 10-50μs 物理层 5ns/m 网卡接收 100-300ns 内核协议栈 500-2000ns 用户态拷贝 200-500ns 行情解析 100-500ns 数据依赖等待 计算逻辑 内存访问 L1/L2/L3/主存 序列化 50-100ns 系统调用 100-300ns 内核协议栈 500-2000ns 网卡发送 100-200ns 网络传输 撮合引擎 成交回报 优化目标:全链路 < 10μs(不含交易所处理) ⚠ 延迟抖动比平均延迟更致命 一次99.9%的抖动(如GC、缺页中断)可能导致订单错过最佳价位 目标:P99.9抖动 < 平均延迟的2倍

2.7 测量是第一生产力

说了这么多,核心就一句话:不测量,不优化

我个人习惯在每个环节都埋一个时间戳。用rdtsc指令,精度在1纳秒左右。注意要处理CPU频率变化和乱序执行。

// 一个简单的rdtsc封装
static inline uint64_t rdtsc() {
    uint32_t lo, hi;
    __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)hi << 32) | lo;
}

// 使用示例
uint64_t t1 = rdtsc();
// ... 某个环节 ...
uint64_t t2 = rdtsc();
double ns = (t2 - t1) / cpu_freq_ghz;  // 假设cpu_freq_ghz已校准

小技巧:rdtsc在乱序执行CPU上可能不准确。我建议用rdtscp,它会等前面指令执行完再读。代价是多几个纳秒,但值得。

好了,这一章我们把全链路拆成了四个段,每个段都标了典型延迟和优化方向。下一章,我们会深入第一个段——网卡和内核协议栈,看看怎么把500纳秒的接收延迟压到100纳秒以内。


公众号:蓝海资料掘金营,微信deep3321