2. 全链路延迟拆解:从行情到达到订单成交的每一纳秒
做高频系统这么多年,我最大的体会是:延迟不是优化出来的,是量出来的。
你想想看,如果你连每一纳秒花在哪都不知道,谈何优化?
这一章,我们就来干一件事——把一条完整的交易链路,从行情数据到达网卡,到订单最终被交易所确认,拆成一个个可测量的环节。
2.1 一条交易指令的“一生”
先画个全景图。我个人习惯把全链路分成四大段:
- 行情接收段:从网卡收到UDP组播,到解析出订单簿快照
- 策略计算段:从新行情触发策略逻辑,到生成下单指令
- 订单发送段:从下单指令序列化,到TCP/UDP包离开网卡
- 交易所处理段:从交易所收到订单,到成交回报返回
每一段里,都有几个“吃时间的大户”。
核心观点:全链路延迟不是加法,是乘法。你优化了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 订单发送段:从指令到线缆
策略决定下单后,指令要经过:
- 序列化:把订单结构体转成二进制协议(50-100ns)
- 系统调用:send()或write()(100-300ns)
- 内核协议栈:TCP分段、窗口管理(500-2000ns)
- 网卡发送:DMA传输、MAC封装(100-200ns)
这里有个坑:系统调用。每次send()都要切到内核态,光上下文切换就几十纳秒。我建议用批处理:攒够N个订单再一次性发送。或者用内核旁路技术,比如Solarflare的onload,直接把用户态和网卡打通。
警告:千万别在发送路径上做任何内存分配!new/malloc都可能触发page fault,一次缺页中断就是几微秒的抖动。我见过一个团队,就因为下单时打印了一行日志,延迟从5微秒飙到50微秒。
2.5 交易所处理段:黑盒里的时间
订单离开你的网卡后,进入交易所的黑盒。这段我们控制不了,但必须测量:
- 网络传输:取决于物理距离,每公里约5微秒
- 交易所撮合:一般10-50微秒
- 成交回报:同样路径返回
怎么测?用回环测试。发一个订单,等成交回报回来,记录总时间。然后减去你的发送和接收延迟,剩下的就是交易所处理时间。
我记得有一次,我们测出来交易所处理时间突然从20微秒变成200微秒。排查了半天,发现是交易所升级了系统,我们的订单协议版本不匹配,被丢到了慢速队列。所以,一定要监控这个黑盒延迟。
2.6 全链路延迟分解图
下面这张图,是我自己项目里用的延迟分解模板。每个环节都标了典型值和优化目标。
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