本文专为高频交易、数字货币交易所及其他对低延迟和高吞吐有极致要求的系统架构师与资深工程师设计。我们将深入探讨做市商(Market Maker)在提供流动性时面临的核心技术挑战——批量报价(Mass Quote)的原子性与高性能处理。文章将从现象出发,下探到操作系统内核与数据结构的底层原理,剖析一套兼顾原子性、高吞吐与低延迟的 Mass Quote 处理架构,并提供从单体到分布式架构的清晰演进路径。
现象与问题背景
在任何一个流动性良好的金融市场,做市商都扮演着至关重要的角色。他们通过持续地、双边地(同时报出买价和卖价)为市场提供报价,确保交易者随时可以找到对手方。在一个成熟的交易所中,一个做市商可能需要同时为成百上千个交易对(Instruments)提供报价,例如,在数字货币衍生品市场,一个做市商可能需要覆盖 BTC-USDT, ETH-USDT, BTC-PERP, ETH-PERP 等所有热门合约。
如果对每个交易对的每次价格更新都通过一个独立的网络请求(例如,一个 FIX NewOrderSingle 消息)发送,将会产生巨大的网络开销和服务器处理压力。假设一个做市商需要为 500 个交易对报价,市场每秒波动一次,他需要调整所有报价,这将产生 500 * 2 = 1000 次/秒的请求。这在网络层面和系统调用层面都是难以接受的。
因此,批量报价(Mass Quote)应运而生。它允许做市商将多个交易对的报价打包在一个消息中发送给交易所。这极大地降低了网络开-销,将 N 个请求的头部开销和 RTT 聚合为 1 个。然而,这引入了一个更为棘手的工程问题:原子性(Atomicity)。
想象一个场景:做市商发送了一个包含 500 个报价的 Mass Quote 消息(我们称之为 MQ-2),用以替换他当前在市场上的 500 个旧报价(MQ-1)。如果系统在处理这个消息时,只更新了其中的 200 个报价就宕机了,那么做市商的在市头寸将处于一个极其危险的“中间状态”——部分是新价格,部分是旧价格。这种状态不符合他的定价模型,可能导致巨大的亏损。因此,对做市商而言,交易所必须保证 Mass Quote 的处理是“全有或全无”的。要么 MQ-2 中的 500 个报价全部生效,同时 MQ-1 的报价全部被替换;要么 MQ-2 完全失败,MQ-1 保持不变。这种原子替换是设计 Mass Quote 系统的核心约束。
关键原理拆解
要解决上述原子性问题,我们不能仅仅停留在应用层代码的逻辑打磨上,而必须回到计算机科学的基础原理中寻找坚实的理论支撑。这本质上是一个并发状态更新和数据一致性的问题。
-
原子性与事务 (Atomicity & Transaction)
我们所追求的“全有或全无”,正是数据库事务 ACID 四大特性中的“A”——原子性。虽然交易系统核心链路通常会避免引入传统数据库的开销,但我们必须在内存中实现轻量级的事务语义。这意味着,一系列操作(取消旧报价、增加新报价)必须被封装成一个逻辑单元,这个单元要么完整执行,要么完全不执行,并且在执行过程中其“中间状态”对系统的其他部分(尤其是撮合引擎)是不可见的。
-
数据结构与访问模式
做市商的报价最终会进入每个交易对的订单簿(Order Book)。订单簿本身是性能热点,通常使用定制的数据结构(如跳表、平衡二叉树,甚至在特定场景下是针对价格档位优化的数组或哈希表)来实现。对于 Mass Quote 处理器而言,它需要一个高效的方式来查找和管理一个做市商的所有报价。一个典型的实现是为每个做市商维护一个哈希表(Hash Map),其中 Key 是 `QuoteID` 或 `Symbol`,Value 是指向订单簿中具体订单对象的指针或引用。这使得对单个报价的增、删、改操作的平均时间复杂度达到 O(1)。
-
并发控制:从锁到无锁 (Concurrency Control: From Locking to Lock-Free)
Mass Quote 处理器在更新做市商的报价集合时,撮合引擎正在高速地读取订单簿进行撮合。这是一个典型的读写冲突场景。最简单的方案是使用一个全局的读写锁(Read-Write Lock)来保护整个订单簿集合。然而,在高频场景下,任何锁竞争都会成为性能瓶颈,导致延迟抖动。更优越的范式是采用无锁(Lock-Free)或接近无锁的设计。
写时复制 (Copy-on-Write, COW) 是一种实现无锁读取的强大模式,非常契合我们的原子替换需求。其核心思想是:写入者从不直接修改正在被读取的数据。当需要修改时,它会创建一个数据的副本,在副本上执行所有修改,完成后,通过一个原子操作(通常是指针交换)将指向旧数据的指针切换到指向新数据。这样,读取者(撮合引擎)在整个写入过程中都可以无锁地、安全地访问旧的、一致的数据快照。一旦指针切换完成,后续的读取者将看到全新的数据。旧数据可以在确定没有任何读取者引用后被安全地回收(例如通过垃圾回收或引用计数)。
-
CPU 指令级的原子性 (CPU-Level Atomicity)
COW 模式的最后一步——指针交换,其原子性必须由硬件保证。在现代多核 CPU 架构中,这依赖于原子指令,如 `xchg` 或 `CMPXCHG` (Compare-and-Swap)。当我们在高级语言中使用 `std::atomic::store` (C++) 或 `atomic.StorePointer` (Go) 时,编译器会将其翻译成这些对应的机器指令,确保即使在多核并发环境下,指针的更新也是一个不可分割的操作,不会出现“指针撕裂”等问题。
系统架构总览
一个健壮的 Mass Quote 处理系统并非孤立存在,它是一个完整交易链路中的关键一环。以下是一个典型的高性能交易系统架构,以及 Mass Quote 消息在其中的流转路径:
[文字描述架构图]
一个典型的交易系统可以垂直分为以下几个逻辑层:
- 接入层 (Gateway): 这是系统的入口,负责维护与客户端(做市商)的 TCP/WebSocket 连接。它解析协议(如 FIX、SBE 或自定义二进制协议),并将解码后的消息对象传递给下一层。这一层是水平可扩展的。
- 风控与预校验层 (Risk & Pre-validation): 在消息进入核心撮合逻辑之前,必须经过一系列快速校验。对于 Mass Quote,这包括:检查做市商的保证金是否充足、持仓是否超出限制、报价价格是否偏离市场价过大(价格合理性检查)等。这是一个纯内存计算的无状态服务,同样可以水平扩展。
- 序列化层 (Sequencer): 这是保证系统确定性的核心。所有对订单簿状态有修改的请求(下单、撤单、Mass Quote)都必须经过一个单一的序列化节点(或一个基于 Raft/Paxos 的共识集群),该节点为每个消息分配一个全局唯一的、严格递增的序列号。系统的所有后续处理都必须严格按照这个序列进行,从而保证了即使在分布式环境下,所有节点处理事件的顺序也是一致的,最终状态也是一致的。
- 核心处理层 (Core Processor / Matching Engine): 这是状态机所在。它按照序列号消费来自 Sequencer 的消息。我们的 Mass Quote 处理器就位于这一层。它执行原子替换逻辑,并将更新后的订单簿状态应用。撮合引擎也在此层,它持续扫描订单簿顶部,执行价格和时间优先的匹配算法。
- 行情与数据推送层 (Market Data Publisher): 当撮合引擎产生交易或订单簿发生变化时,它会生成事件,推送给这一层。这一层负责将这些内部事件编码成行情数据(Trades, Order Book Depth),广播给所有订阅行情的用户。
Mass Quote 消息的生命周期是:Gateway -> Risk -> Sequencer -> Core Processor,最终其效果通过 Market Data Publisher 体现出来。
核心模块设计与实现
我们将聚焦于核心处理层内部的 Mass Quote Processor 模块,并使用 Copy-on-Write 模式来实现原子替换。
数据结构定义
首先,我们需要定义用于管理一个做市商所有在市报价的数据结构。一个 `map` 是理想选择。
// Quote 代表一个具体的报价
type Quote struct {
QuoteID string
Symbol string
Side string // "BUY" or "SELL"
Price int64 // 使用定点数表示价格,避免浮点数精度问题
Quantity int64
// 指向其在具体OrderBook中节点的指针
OrderNodePtr unsafe.Pointer
}
// MarketMakerQuoteSet 代表一个做市商在某一时刻的所有报价的集合
// Key 是 QuoteID,方便快速查找
type MarketMakerQuoteSet map[string]*Quote
// 我们可以为每个做市商维护一个指向其当前有效报价集的原子指针
// var mmQuotes map[string]*atomic.Pointer[MarketMakerQuoteSet]
在工程实践中,为了极致的性能,我们通常会使用 C++ 并自定义内存分配器。但为了清晰地展示逻辑,Go 的示例更具表达力。`unsafe.Pointer` 在这里用于示意,实际实现中会是一个具体的订单簿节点类型指针。
原子替换算法实现
以下是处理一则 Mass Quote 消息的核心伪代码逻辑。假设我们有一个原子指针 `activeSetPtr`,它指向当前对撮合引擎可见的报价集合。
// a.activeSetPtr 是一个 *atomic.Pointer[MarketMakerQuoteSet]
// incomingMsg 是解码后的 Mass Quote 消息
func (p *MassQuoteProcessor) processMassQuote(a *Account, incomingMsg *MassQuoteMessage) error {
// 1. 获取当前活跃的报价集
// 这是一个原子读操作,不会与撮合引擎的读取发生冲突
currentActiveSet := a.activeSetPtr.Load()
// 2. 创建一个“暂存”或“影子”报价集 (Staging Set)
// 这是一个浅拷贝,成本很低。我们只复制map本身,而不是map里的所有Quote对象。
stagingSet := make(MarketMakerQuoteSet, len(*currentActiveSet))
for k, v := range *currentActiveSet {
stagingSet[k] = v
}
// 3. 在 Staging Set 上应用所有变更
// 这一系列操作对撮合引擎完全不可见
// 3a. 处理需要取消的报价 (QuoteCancel)
for _, quoteToCancel := range incomingMsg.QuotesToCancel {
if quote, exists := stagingSet[quoteToCancel.QuoteID]; exists {
// 从订单簿中移除该报价(这是一个需要加锁或使用其他并发控制的操作)
p.orderBook[quote.Symbol].Remove(quote.OrderNodePtr)
delete(stagingSet, quoteToCancel.QuoteID)
}
}
// 3b. 处理新增或修改的报价
for _, newQuoteData := range incomingMsg.QuotesToAddOrModify {
// 如果已存在,则是修改
if existingQuote, exists := stagingSet[newQuoteData.QuoteID]; exists {
// 先从订单簿撤销旧的
p.orderBook[existingQuote.Symbol].Remove(existingQuote.OrderNodePtr)
// 更新报价对象,并重新插入订单簿
existingQuote.Price = newQuoteData.Price
existingQuote.Quantity = newQuoteData.Quantity
newNodePtr := p.orderBook[existingQuote.Symbol].Add(existingQuote)
existingQuote.OrderNodePtr = newNodePtr
} else { // 否则是新增
newQuote := &Quote{ /* ... from newQuoteData ... */ }
newNodePtr := p.orderBook[newQuote.Symbol].Add(newQuote)
newQuote.OrderNodePtr = newNodePtr
stagingSet[newQuote.QuoteID] = newQuote
}
}
// 4. 对Staging Set进行最终的业务校验(例如总风险暴露等)
if err := p.riskChecker.ValidateQuoteSet(stagingSet); err != nil {
// 如果校验失败,则放弃所有变更。
// 因为所有操作都在Staging Set上,所以activeSet完全没受影响。
// 需要将刚才在订单簿中的变更回滚(这是COW模式的一个复杂点,需要仔细设计回滚逻辑)。
// 一个更纯粹的COW实现是连同订单簿一起复制,但成本极高。
// 实践中,通常是对订单簿的修改采用细粒度锁,而对报价集的“激活”采用指针交换。
return err // 返回错误,原子性保证
}
// 5. 原子地将 Staging Set 切换为 Active Set
// 这是整个流程的“提交点”。一旦执行,所有变更同时对撮合引擎可见。
a.activeSetPtr.Store(&stagingSet)
// 6. 旧的 Active Set 可以被回收了。
// 在有GC的语言中,当没有指针再引用它时,它会被自动回收。
// 在C++中,需要一个安全的内存回收机制,如Epoch-Based Reclamation或Hazard Pointers。
return nil
}
极客工程师的犀利点评:上面的伪代码简化了一个关键细节——对订单簿的修改。一个“纯粹”的 COW 会连同订单簿一起复制,但这在实践中内存和 CPU 开销太大。现实世界的妥协方案是,对订单簿的修改(`Add`/`Remove`)本身使用更细粒度的锁(例如,对每个交易对的订单簿分别加锁),而将整个做市商的“报价意图集合”的切换,通过 COW 的指针交换来实现。这样,我们把原子性保证的粒度放在了“做市商的逻辑视图”上,这正是业务所需要的。撮合引擎在遍历订单簿时,看到的是已经被修改的订单簿,但做市商的管理后台查询自己有“哪些在市订单”时,通过读取 `activeSetPtr`,能得到一个完全一致的快照。
性能优化与高可用设计
在每微秒都至关重要的交易世界,架构设计和代码实现只是起点,极致的性能压榨和系统韧性设计才是决胜的关键。
性能优化 (Performance Tuning)
- CPU 亲和性与内核隔离 (CPU Affinity & Kernel Isolation): 将交易系统的核心线程(Gateway I/O 线程、Sequencer 线程、撮合线程)绑定到独立的 CPU 核心上,通过 `taskset` 或在启动时设置 `isolcpus` 内核参数。这可以消除操作系统的线程调度开销和跨核迁移导致的 CPU Cache Miss,带来显著且稳定的性能提升。
- 内存管理 (Memory Management): 在 C++/Java 这类语言中,避免在核心路径上进行动态内存分配(`new`/`malloc`)。使用对象池(Object Pool)预先分配好大量的 Quote 和 Order 对象。当需要时从池中获取,使用完毕后归还池中,而不是释放给操作系统。这可以完全消除堆内存分配和垃圾回收(GC)带来的延迟抖动。
– 网络栈优化 (Network Stack Tuning):
– TCP_NODELAY: 必须开启此选项,禁用 Nagle 算法,确保小数据包能被立即发送。
– 内核旁路 (Kernel Bypass): 对于延迟要求最苛刻的场景(如顶级做市商的专线接入),可以采用 DPDK 或 Solarflare 的 OpenOnload 等技术。这些技术允许应用程序直接在用户态操作网卡,绕过整个内核网络协议栈,将网络收发的延迟从数十微秒降低到个位数微秒。这意味着,你的应用程序代码直接负责处理 TCP/IP 报文,代价是极高的实现复杂性。
高可用设计 (High Availability)
- 无状态服务水平扩展: Gateway 和风控层是无状态的,可以部署多个实例,通过负载均衡器(如 LVS/HAProxy)分发流量,轻松实现高可用和扩容。
- 核心引擎主备复制: 撮合引擎是状态化的,通常采用主备(Primary-Secondary)模式。主节点处理所有请求,并将包含了序列号的指令流(Command Log)实时复制给备用节点。备用节点在内存中应用同样的指令流,保持与主节点几乎完全一致的状态。
- 确定性与快速故障切换: 由于系统是确定性的(相同的输入序列产生相同的输出状态),当主节点宕机时(通过心跳检测),可以快速切换到备用节点。因为备用节点已经拥有了最新的状态,它几乎可以立即接管服务。这个切换过程可以由 ZooKeeper/etcd 等协调服务来自动管理,实现秒级甚至亚秒级的故障恢复(RTO)。
架构演进与落地路径
一套复杂的系统并非一蹴而就。根据业务发展阶段和技术团队能力,可以分阶段进行演进。
第一阶段:单体巨石 (Monolith)
在业务初期,可以将所有逻辑(Gateway, Risk, Matching)都放在一个进程中。不同模块通过函数调用交互。此时,Mass Quote 的原子替换可以通过在进程内共享内存和原子指针交换来实现。这种架构简单、易于开发和调试,延迟最低(因为没有网络开销)。但它的缺点是可扩展性差,任何一个模块的 bug 都可能导致整个系统崩溃。
第二阶段:面向服务的拆分 (Service-Oriented)
随着流量增长,将无状态的 Gateway 和 Risk 模块拆分为独立的服务。它们与核心的撮合引擎(仍然是单体)通过低延迟的进程间通信(IPC)或消息队列(如 Aeron, ZeroMQ)进行交互。这使得 Gateway 和 Risk 层可以独立扩展和部署。核心撮合引擎仍然是系统的“单点”,但整体系统的吞吐量和稳定性得到了提升。
第三阶段:按交易对分片 (Sharding by Instrument)
当单个撮合引擎无法处理所有交易对时,需要进行水平扩展。最常见的方法是按交易对进行分片(Sharding)。例如,所有 BTC 相关的交易对(BTC-USDT, BTC-PERP)在一个撮合引擎实例上,所有 ETH 相关的在另一个实例上。此时,Mass Quote 消息如果跨越了多个分片,就需要一个“协调层”(通常在 Gateway 或其后)进行消息的拆分、路由和结果的聚合。这引入了分布式事务的复杂性,需要仔细设计以保证跨分片的原子性。
第四阶段:多区域部署 (Geo-Redundancy)
为了服务全球用户和满足合规要求,交易所可能需要在全球多个数据中心进行部署。这引入了跨地域数据复制、一致性和网络延迟的巨大挑战。此时,通常每个区域都有一个完整的交易系统集群,并通过专门的跨区同步机制来共享部分流动性或进行结算,但这已经超出了单个 Mass Quote 系统的范畴,是整个交易平台架构的顶层设计问题。
总而言之,处理 Mass Quote 的核心在于理解并解决其原子性要求。通过借鉴数据库事务和并发控制的经典理论,并结合现代硬件的原子操作特性,我们可以在应用层构建出既高效又健壮的解决方案。而架构的演进,则是一个在系统复杂度、性能、可用性和开发成本之间不断权衡和取舍的过程。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。