第二章:核心架构总览:系统分层设计、数据流与订单流、延迟预算分析
做高频交易系统,说白了就是在跟物理定律赛跑。光速是固定的,交换机处理时间也是固定的,你能优化的只有架构设计。我入行那会儿,带我的老工程师说过一句话,我一直记着:「你的系统每多一层抽象,就多一层延迟,但少一层抽象,就多一分风险。」
嗯,这句话基本概括了本章要讲的核心。我们来看看一个典型的美股高频交易系统,到底是怎么分层、怎么流转、以及怎么算延迟这笔账的。
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 米,就多 3.3 纳秒。这个你改变不了,只能靠「托管」——把服务器放到交易所机房里。
- 软件延迟: 从网卡收到数据包,到你的策略做出决策,再到订单发出去。这部分是你能优化的。
- 硬件延迟: 网卡、交换机、CPU 缓存 miss 等等。这部分可以通过选型来优化。
我给大家一个实际的延迟预算表,这是我之前在纳斯达克做的一个项目里用的:
| 环节 | 预算(纳秒) | 实际(纳秒) | 说明 |
|---|---|---|---|
| 网卡接收 + 内核旁路 | 500 | 420 | 使用 Solarflare 网卡 + OpenOnload |
| 行情解码 | 300 | 280 | 手工汇编优化,不用标准库 |
| 信号计算 | 2000 | 1850 | 用 SIMD 指令集加速 |
| 决策逻辑 | 1000 | 950 | 状态机模式,避免分支预测失败 |
| 订单编码 + 发送 | 500 | 480 | 预分配内存,避免动态分配 |
| 总计 | 4300 | 3980 | 控制在 4 微秒以内 |
2.4 核心架构图:一张图看懂全局
下面这张图是我自己画的,展示了数据流和订单流如何在这四层中流转。注意看,数据流是单向的,订单流是双向的(有回执)。
boost::lockfree::spsc_queue 或者自己实现一个基于 CAS 的环形缓冲区。千万别用 mutex,那玩意儿在高频场景下就是灾难。
2.5 总结:架构设计的三个核心原则
说了这么多,其实就三个原则:
- 分层清晰,但不要过度分层。 每一层只做一件事,但层数控制在 4 层以内。多了延迟受不了,少了耦合太严重。
- 数据流和订单流必须分离。 用不同的核心、不同的内存区域、不同的网卡队列。我曾经见过一个团队把两者混在一起,结果行情爆发时订单发送被阻塞,直接错过了最佳成交时机。
- 延迟预算要精确到纳秒。 每一段代码、每一次内存访问、每一次系统调用,都要知道它花了多少时间。用
rdtsc指令来测量,别用clock_gettime,那个太慢了。
嗯,这就是核心架构总览的全部内容。下一章我们会深入接入层,聊聊行情解码的那些坑。不过那是后话了,先把这四层架构吃透,后面的内容才好理解。