撮合引擎每快 1 微秒,一年就多赚 200 万。 我们的老引擎,单笔撮合 47 微秒。 30 天后,3.1 微秒。
撮合引擎是交易系统的心脏。每一笔买单和卖单,都要在这里匹配、成交。
它的延迟,直接决定你能抢到多好的价格。
我们的老引擎是 C++ 写的,跑了三年,单笔撮合 47 微秒。三年前这个数字还行,但市场在卷,对手在变快。特别是这波"人工智能“带来的行情及“美以伊战争“带来的不确定性,47 微秒,已经从"够用"变成了"不够用"。
必须提升性能,要么被淘汰,要么追上去。计划 30 天完成改造。
第一周没写一行业务代码。全在搞架构和数据结构。
核心决策一:无锁设计。
传统撮合引擎用互斥锁保护订单簿。锁的问题不是慢,是抖——行情平稳时没问题,行情暴增时锁竞争加剧,延迟从 50μs 直接飙到 500μs。
最致命的是,抖动不可预测。你不知道哪一笔会慢。
方案:用 lock-free 的 MPMC 队列做订单入口,订单簿本身用无锁的数据结构。Rust 的 crossbeam 库提供了工业级的无锁队列实现,省了大量造轮子的时间。我们基于 crossbeam 做了大量的模拟和验证测试。
核心决策二:内存池。
撮合引擎最怕运行时内存分配。每次 malloc 都是一次系统调用,动辄几十微秒。
方案:启动时预分配所有内存,运行时只做指针交换,永远不在 hot path 上分配。Rust 的所有权模型在这里天然合适——你可以精确控制每块内存的生命周期,不用担心悬挂指针。
第二周开始写撮合逻辑。
撮合引擎的核心其实不复杂:维护一个买单簿和一个卖单簿,价格优先、时间优先。但在微秒级要求下,每个细节都是坑:
坑一:缓存行伪共享
两个线程分别更新订单簿的 bid 和 ask,看起来互不干扰。但如果这两个字段恰好在同一个 CPU 缓存行(64 字节)里,每次写操作都会导致对方的缓存行失效。
一个看似无关的写操作,凭空多了几十纳秒。乘以每秒几十万笔,就是毫秒级的浪费。
解法:用 #[repr(align(64))] 强制缓存行对齐:
#[repr(align(64))]
struct CacheAligned<T>(T);
struct OrderBook {
bids: CacheAligned<BTreeMap<PriceLevel, OrderQueue>>,
asks: CacheAligned<BTreeMap<PriceLevel, OrderQueue>>,
}改完这一行,延迟直接降了 8 微秒。一行代码,8 微秒。
坑二:Vec 的扩容
Vec 在容量不够时会重新分配内存。前期开发时没有在意,在撮合的 hot path 上做压力测试时发现,一次扩容 = 一次灾难。
解法:预分配容量 + 用 SmallVec 优化小订单队列。大部分价格档位的订单数不超过 8 个,SmallVec 能在栈上存前 8 个元素,完全避免堆分配。
第三周进入"抠纳秒"阶段。
CPU 亲和性
把撮合线程绑定到固定核心上,不让操作系统调度器乱搬。搬一次核心,L1/L2 缓存全失效,几千纳秒就没了。
use core_affinity;
let core_ids = core_affinity::get_core_ids().unwrap();
core_affinity::set_for_current(core_ids[2]); // 绑到第 3 个核心关闭 GC——等等,Rust 没有 GC
这是 Rust 最大的优势之一。不需要像 Java/Go 那样调 GC 参数、担心 STW 停顿。Rust 的内存管理在编译期就确定了,运行时零停顿。
你唯一要注意的是:别在 hot path 上做堆分配。用前面说的内存池和 SmallVec,基本能消除所有运行时分配。
内联与分支预测
关键撮合函数加 #[inline(always)],热路径分支用 likely/unlikely 语义提示编译器优化。
这些优化单个看微不足道——省 2 纳秒、5 纳秒。但全加起来,延迟从 12 微秒降到了 5 微秒。
功能写完了,真正的考验才开始。
模拟极端行情:10 万笔/秒的订单洪流,持续 30 分钟。
结果:平均延迟 4.2 微秒,P99 延迟 8.1 微秒,P999 延迟 15 微秒。
对比老引擎:平均 47 微秒,P99 120 微秒,P999 直接超时。
但还不够。尾部延迟(P99/P999)才是真正影响交易的——你以为 3 微秒很快,但偶尔的 15 微秒一样会让你错过价格。
Day 25:一个诡异的延迟尖峰
压测第 4 天,发现一个诡异的延迟尖峰:每隔约 60 秒,出现一次 200 微秒的峰值。
排查了一整天,发现是操作系统的定时器中断。每秒触发一次的中断会抢走 CPU,导致撮合线程被打断。
解法:把撮合线程设为实时优先级(SCHED_FIFO),并在内核启动参数里用 isolcpus 隔离 CPU 核心,禁止其他进程调度到这个核上。
改完后,P999 从 15 微秒降到了 6.2 微秒。尾部延迟被削平了。
灰度上线,先跑 10% 的流量。
Day 29 没出问题。Day 30 全量切换。
最终数据对比:
指标 | 老引擎(C++) | 新引擎(Rust) | 提升 |
|---|---|---|---|
平均延迟 | 47 μs | 3.1 μs | 15x |
P99 延迟 | 120 μs | 5.8 μs | 20x |
P999 延迟 | 超时 | 6.2 μs | — |
吞吐量 | 5 万笔/秒 | 18 万笔/秒 | 3.6x |
内存安全 | 靠人保证 | 编译器保证 | — |
最后一行才是我最看重的。
C++ 那套东西,你调到 3 微秒也睡不踏实——因为你不知道什么时候会踩一个内存 bug。Rust 不一样,编译过了,这类问题就不存在。
快很重要。但可预测的快,比偶尔的快更重要。
几个关键收获:
1. 80% 的性能提升来自架构,不是微优化。
无锁设计 + 内存池,这两件事吃掉了 30 微秒。剩下的抠纳秒操作,加起来也就 10 微秒。先选对架构,再谈优化。
2. Rust 的真正优势不是"快",是"快且安全"。
C++ 也能写到 3 微秒,但你要花三倍的时间排查内存问题,还得祈祷线上别出事。Rust 让你把精力放在性能上,而不是放在"别出 bug"上。
3. 工具链很重要。
crossbeam、SmallVec、core_affinity,这些 crate 省了大量造轮子的时间。Rust 生态在高性能领域已经够用了,没有花大量时间造轮子。。
4. 压测比开发更花时间。
写代码 2 周,压测调优 2 周。但这 2 周才是决定成败的。一个你没想到的内核中断,就能毁掉前两周的所有努力。
有人问我:30 天重写一个撮合引擎,值得吗?
算笔账:每笔快 44 微秒,每天 50 万笔交易,一年省下来的滑点成本——远超一个工程师一年的工资。
值得。
低延迟的尽头不是"更快"。是"更快,且不再害怕"。
文中性能数据会因不同硬件和场景而变化。