第二章:核心架构总览:系统分层设计、数据流与订单流、延迟预算分析

做高频交易系统,说白了就是在跟物理定律赛跑。光速是固定的,交换机处理时间也是固定的,你能优化的只有架构设计。我入行那会儿,带我的老工程师说过一句话,我一直记着:「你的系统每多一层抽象,就多一层延迟,但少一层抽象,就多一分风险。」

嗯,这句话基本概括了本章要讲的核心。我们来看看一个典型的美股高频交易系统,到底是怎么分层、怎么流转、以及怎么算延迟这笔账的。

2.1 系统分层设计:拆开来看,其实就四层

我个人习惯把系统分成四层。别搞太复杂,四层刚刚好。你想想看,层数少了耦合太重,层数多了延迟受不了。

层级 名称 核心职责 延迟要求
L1 接入层 行情解码、订单编码、协议转换 < 1 微秒
L2 信号层 特征计算、信号生成、风控预检 < 5 微秒
L3 决策层 策略逻辑、订单管理、路由选择 < 10 微秒
L4 执行层 订单发送、回执处理、重试机制 < 2 微秒

这里有个坑,我必须要说。很多团队喜欢在接入层和信号层之间再加一层「预处理层」,用来做数据清洗。我曾经见过一个项目,预处理层里做了大量的浮点运算,结果延迟直接飙到 20 微秒以上。后来我们把这层砍掉了,把清洗逻辑直接内联到信号层里,延迟降到了 3 微秒。说白了,高频系统里,能省则省。

2.2 数据流与订单流:两条腿走路

数据流和订单流,是系统的两条腿。一条负责「看」,一条负责「做」。它们必须并行,不能串行。

数据流(行情 -> 信号 -> 决策)

行情数据从交易所的 multicast 通道进来,经过接入层解码,变成内存里的结构体。然后信号层开始干活——计算买卖盘口、价差、成交量加权价格等等。决策层拿到这些信号,判断要不要下单。

这里有个细节:数据流必须是「零拷贝」的。我刚开始做的时候不懂,用了 memcpy 来传递行情数据,结果延迟多了 200 纳秒。后来改成指针传递,直接共享内存区域,效果立竿见影。

订单流(决策 -> 执行 -> 交易所)

订单流是反方向的。决策层生成一个订单对象,传给执行层。执行层负责把它编码成 FIX 协议或者二进制协议,然后通过 TCP 或者 RDMA 发出去。

注意,订单流里有一个「回执处理」的环节。交易所会返回一个确认消息,告诉你订单成交了还是被拒了。这个回执必须跟行情数据合并处理,否则你的订单簿状态会乱掉。我曾经踩过这个坑,回执处理慢了 1 微秒,导致同一个价格上下了两笔单子,亏了不少钱。

核心原则: 数据流和订单流必须使用不同的线程/核心来处理。混在一起的话,行情波动大的时候订单会被堵住,反之亦然。

2.3 延迟预算分析:每一纳秒都要算清楚

做高频交易,你得学会「算账」。不是算赚了多少钱,而是算每一笔交易花了多少时间。我习惯把延迟拆成三部分:

  1. 网络延迟: 从你的服务器到交易所的物理距离。光速是 1 英尺/纳秒,所以每多 1 米,就多 3.3 纳秒。这个你改变不了,只能靠「托管」——把服务器放到交易所机房里。
  2. 软件延迟: 从网卡收到数据包,到你的策略做出决策,再到订单发出去。这部分是你能优化的。
  3. 硬件延迟: 网卡、交换机、CPU 缓存 miss 等等。这部分可以通过选型来优化。

我给大家一个实际的延迟预算表,这是我之前在纳斯达克做的一个项目里用的:

环节 预算(纳秒) 实际(纳秒) 说明
网卡接收 + 内核旁路 500 420 使用 Solarflare 网卡 + OpenOnload
行情解码 300 280 手工汇编优化,不用标准库
信号计算 2000 1850 用 SIMD 指令集加速
决策逻辑 1000 950 状态机模式,避免分支预测失败
订单编码 + 发送 500 480 预分配内存,避免动态分配
总计 4300 3980 控制在 4 微秒以内
避坑指南: 我曾经在信号计算环节用了 std::map 来存储订单簿,结果每次插入都要 O(log n) 的时间。后来换成自定义的跳表实现,延迟直接降了 40%。记住,高频系统里不要用 STL 的容器,除非你非常清楚它的性能开销。

2.4 核心架构图:一张图看懂全局

下面这张图是我自己画的,展示了数据流和订单流如何在这四层中流转。注意看,数据流是单向的,订单流是双向的(有回执)。

高频交易系统核心架构总览 L1 接入层 行情解码 (ITCH/OUCH) | 订单编码 (FIX/Binary) | 协议转换 L2 信号层 特征计算 (价差/深度/波动率) | 信号生成 | 风控预检 L3 决策层 策略逻辑 (做市/套利/动量) | 订单管理 | 路由选择 L4 执行层 订单发送 (TCP/RDMA) | 回执处理 | 重试机制 行情 交易所 回执 总延迟预算:< 5 微秒 | 数据流 + 订单流 完全并行
注意: 这张图里没有画「共享内存」的细节。实际上,层与层之间通过无锁队列(lock-free queue)来传递数据。我建议使用 boost::lockfree::spsc_queue 或者自己实现一个基于 CAS 的环形缓冲区。千万别用 mutex,那玩意儿在高频场景下就是灾难。

2.5 总结:架构设计的三个核心原则

说了这么多,其实就三个原则:

  • 分层清晰,但不要过度分层。 每一层只做一件事,但层数控制在 4 层以内。多了延迟受不了,少了耦合太严重。
  • 数据流和订单流必须分离。 用不同的核心、不同的内存区域、不同的网卡队列。我曾经见过一个团队把两者混在一起,结果行情爆发时订单发送被阻塞,直接错过了最佳成交时机。
  • 延迟预算要精确到纳秒。 每一段代码、每一次内存访问、每一次系统调用,都要知道它花了多少时间。用 rdtsc 指令来测量,别用 clock_gettime,那个太慢了。

嗯,这就是核心架构总览的全部内容。下一章我们会深入接入层,聊聊行情解码的那些坑。不过那是后话了,先把这四层架构吃透,后面的内容才好理解。