在任何一个追求流动性的金融交易市场,无论是股票、外汇还是数字资产,做市商(Market Maker, MM)都扮演着至关重要的角色。他们通过提供持续的双边报价(Bid/Ask)为市场注入生命力。然而,对于撮合系统架构而言,处理来自做市商的高频、海量的报价更新是一个巨大的挑战。本文旨在为中高级工程师和架构师,系统性地拆解做市商报价(Quoting)机制的底层原理、架构设计与工程实践,探讨如何构建一个既能满足做市商严苛性能要求,又不拖垮核心撮合引擎的高性能系统。
现象与问题背景
在一个典型的撮合系统中,普通用户的交易行为通常是提交限价单(Limit Order)或市价单(Market Order)。这些订单一旦进入订单簿(Order Book),通常会停留一段时间,直到被撮合或被用户取消。其生命周期相对较长,更新频率也较低。
做市商的行为模式则完全不同。他们并非“下注”市场的单边走势,而是通过赚取买卖价差(Spread)来盈利。为了维持一个有竞争力的价差并控制自身风险敞口,做市商需要根据市场最新价格、自身库存、以及其他复杂模型,以极高的频率调整自己的报价。一个活跃的做市商,可能在单一交易对上每秒提交数十次甚至上百次的报价更新。当系统支持数百个交易对,并有数十个做市商同时运作时,报价更新的洪流将达到每秒数万甚至数十万次。
如果将每一次报价更新都视为一次“先取消旧订单,再提交新订单”(Cancel/Replace)的操作,将会给撮合引擎带来灾难性的后果:
- 撮合引擎过载: 核心的撮合逻辑需要处理海量的“非交易”型指令,其事件队列将被迅速填满,导致正常交易订单的撮合延迟急剧增加。
– 订单簿颠簸(Book Churn): 订单簿数据结构会经历剧烈的、无效的插入与删除操作,消耗大量 CPU 资源,并可能导致缓存颠簸,进一步降低性能。
– 网络与消息总线拥堵: 大量的 Cancel/Replace 消息会挤占网关、消息队列(如 Kafka)的带宽,影响整个系统的稳定性。
因此,核心问题浮出水面:如何设计一套独立的报价处理机制,能够高效地“吸收”做市商的高频报价流,同时将其对核心撮合引擎的干扰降到最低,仅在必要时才将流动性“物化”为真实订单参与撮合?
关键原理拆解
要解决上述问题,我们必须回归到计算机科学的基础原理。这不仅仅是一个业务逻辑问题,更是一个关于状态管理、并发控制和数据结构设计的经典CS问题。
(教授声音)
从操作系统和并发理论的视角来看,撮合引擎的核心本质是一个状态机(State Machine)。订单簿就是它的核心状态。为了保证状态转换的确定性和一致性,最高性能的撮合引擎几乎无一例外地采用单线程事件循环(Single-Threaded Event Loop)模型来处理核心撮合逻辑。这遵循了“单写入者原则”(Single Writer Principle),避免了在最关键的数据结构(订单簿)上使用任何形式的锁或昂贵的并发原语,从而将 CPU 的计算能力发挥到极致。
做市商报价的本质,是对其“交易意图”状态的持续更新。这个状态的更新频率远高于真实交易发生的频率。如果我们强行将这两种不同频率、不同性质的状态更新请求,塞进同一个单线程事件队列,就违背了系统设计的“关注点分离”(Separation of Concerns)原则。这就好比在一个CPU上,用一个高优先级的实时任务(撮合交易)去处理大量低优先级的IO中断(报价更新),最终会导致高优先级任务的响应时间(Jitter)大幅恶化。
因此,理论上的最优解是将这两种状态管理分离开来:
- 核心状态(Core State): 订单簿本身,由撮合引擎的单线程模型严格管理,只处理最终需要进入撮合的指令。
- 瞬时状态(Transient State): 做市商的实时报价。这个状态是易失的,后到的报价会覆盖先到的。它不直接参与撮合,因此不需要与核心状态共享同一个严格的并发控制域。
基于此,我们可以设计一个专门的“报价处理器”,它运行在独立的线程或进程中,维护做市商的瞬时报价状态。撮合引擎只在需要流动性时,才从这个处理器中“拉取”最新的、有效的报价。这个“拉取”动作,才是真正进入撮合引擎事件队列的指令。这种模式,本质上是一种生产者-消费者模型的变体,其中做市商是生产者,撮合引擎是消费者,而报价处理器则扮演了那个至关重要的、具备“状态压缩”能力的缓冲区。
系统架构总览
基于上述原理,一个健壮的做市商报价处理系统架构通常如下(以文字描述一幅清晰的架构图):
整个系统分为三层:接入层、报价处理层和核心撮合层。
- 接入层 (Gateway): 负责与做市商建立长连接(通常是 TCP,基于 FIX 或 WebSocket 等协议)。它处理认证、会话管理、消息解码和初步校验。同一个做市商可能会建立多个连接以提高吞吐和可用性。Gateway 将解码后的报价消息,通过低延迟的消息总线或直接的进程间通信(IPC),发送给报价处理层。
- 报价处理层 (Quote Processor): 这是设计的核心。它是一个或多个独立的服务/进程。其内部维护着一个内存数据结构,实时存储所有做市商对所有交易对的最新报价。它完全独立于撮合引擎,可以水平扩展。它的唯一职责就是以极高的性能接收并更新报价,其内部不进行任何撮合。
- 核心撮合层 (Matching Engine): 这是我们熟悉的撮合引擎,内部是单线程事件循环。当一个Taker订单(例如市价单)进入时,撮合引擎除了匹配订单簿上已有的Maker订单外,还会向报价处理层发起一个“查询请求”(Query),获取当前最优的做市商报价。如果查询到的报价可以与Taker订单成交,撮合引擎才会将这个报价“物化”(Materialize)成一个真实的、临时的限价单,放入订单簿并立即完成撮合。
这个架构的关键优势在于:做市商每秒一万次的报价更新,可能只在报价处理层的高速内存中完成了一万次简单的哈希表覆写操作。而核心撮合层对此毫无感知,它的事件队列依旧干净、清爽,只有当真实交易机会出现时,报价数据才会被“拉”过界,转化为一个高优先级的撮合事件。
核心模块设计与实现
(极客声音)
理论听起来很美,但魔鬼在细节里。我们来聊聊几个关键模块的实现坑点和代码思路。
模块一:Quote Processor 的设计
这玩意儿的核心就是一个高性能的内存缓存。别想着用 Redis 或其他外部系统,网络开销会杀死你。它必须在进程内,直接操作内存。
数据结构怎么选?最简单粗暴、也最有效的就是一个嵌套的哈希表(`Map` in Go, `ConcurrentHashMap` in Java)。
map[Symbol][MarketMakerID] -> Quote
其中 Symbol 是交易对(如 `BTC/USDT`),MarketMakerID 是做市商的唯一标识,Quote 是一个包含买价、卖价、数量、时间戳等信息的结构体。当一个新的报价消息进来,操作就是一次 O(1) 的哈希表覆写。简单、高效、无锁(或者在多线程更新时,只需要对第二层Map进行分段锁,粒度非常小)。
代码看起来大概是这样:
// Quote 代表一个双边报价
type Quote struct {
BidPrice int64 // 使用定点数表示价格,避免浮点数精度问题
BidSize int64
AskPrice int64
AskSize int64
Timestamp int64 // 纳秒时间戳,用于判断报价新鲜度
}
// QuoteProcessor 维护所有做市商的最新报价
// 真实场景下,读写需要锁保护,或者采用更精细的并发控制
// 例如每个 symbol 一个读写锁,或者使用 lock-free map
type QuoteProcessor struct {
// a map from symbol -> (map from marketMakerID -> Quote)
quotes sync.Map // 使用 sync.Map 应对高并发读写
}
func (qp *QuoteProcessor) UpdateQuote(symbol string, mmID uint32, newQuote Quote) {
mmQuotes, _ := qp.quotes.LoadOrStore(symbol, &sync.Map{})
// 直接覆盖,这就是"状态压缩"的核心
// 后来的报价直接覆盖之前的,无需任何复杂逻辑
mmQuotes.(*sync.Map).Store(mmID, newQuote)
}
// GetBestQuotesForSymbol 被撮合引擎调用
func (qp *QuoteProcessor) GetBestQuotesForSymbol(symbol string) (bestBid Quote, bestAsk Quote) {
mmQuotes, ok := qp.quotes.Load(symbol)
if !ok {
return
}
// 遍历该 symbol 下所有做市商的报价,找出最优的
mmQuotes.(*sync.Map).Range(func(key, value interface{}) bool {
quote := value.(Quote)
// 伪代码: 检查报价是否过期、是否满足做市义务等
// if isQuoteValid(quote) { ... }
if quote.BidPrice > bestBid.BidPrice {
bestBid = quote
}
if quote.AskPrice > 0 && (bestAsk.AskPrice == 0 || quote.AskPrice < bestAsk.AskPrice) {
bestAsk = quote
}
return true
})
return
}
注意几个细节:第一,价格和数量必须用定点数(通常是`int64`)表示,避免浮点数计算带来的精度和性能问题。第二,报价里必须有时间戳,用于风控和有效性判断,防止因为网络延迟等问题,撮合引擎用了一个“过时”的报价。第三,在 `GetBestQuotesForSymbol` 中,你需要遍历当前符号下所有做市商的报价,找到最优的买价(最高)和卖价(最低)。这里的计算开销是 O(N),N是该交易对的做市商数量,通常不大,可以接受。
模块二:撮合引擎与报价处理器的交互
这是整个设计的精髓所在:报价的物化(Quote Materialization)。
当一个市价买单(Taker Buy Order)进入撮合引擎时,标准流程是:
- 匹配订单簿: 撮合引擎首先在自己的订单簿(Order Book)里,从最低的卖价开始,依次匹配限价卖单。
- 查询做市商流动性: 假设订单簿上的流动性不足以完全成交这个市价单,或者市价单希望获得更好的价格。此时,撮合引擎会同步调用 `QuoteProcessor.GetBestQuotesForSymbol()` 方法。
- 比较与选择: 撮合引擎拿到订单簿上最优的卖价 P1,和从 Quote Processor 拿到的最优做市商卖价 P2。它会选择 `min(P1, P2)` 来成交。
- 物化与撮合: 如果 P2 更优(即 P2 < P1),撮合引擎会执行一个原子操作:
- 生成一个临时的、与 P2 报价内容一致的限价卖单(Internal Maker Order)。
- 将这个临时订单“插入”到订单簿的顶部(或者逻辑上认为它在顶部)。
- 立即与市价买单进行撮合。
- 撮合完成后,无论这个临时订单是否完全成交,都立即将其从订单簿中移除。
这个过程保证了做市商的报价只有在“即将成交”的那一刻才真正进入核心撮合域,变成一个订单。它在订单簿里的生命周期可能只有几百纳秒,将对撮合引擎状态的干扰降到了最低。
性能优化与高可用设计
对于追求极致性能的交易系统,上述架构还有很多可以压榨的地方。
- 内存与CPU Cache:
Quote结构体要精心设计,保证数据对齐,尽量让一个结构体能装在一个或两个 CPU Cache Line 内(通常是 64 字节)。在 `GetBestQuotesForSymbol` 的遍历中,这种连续的内存访问能极大地提高缓存命中率。 - 网络优化: 接入层 Gateway 和做市商之间的网络延迟是关键。使用 `TCP_NODELAY` 禁用 Nagle 算法是基本操作。更极致的,会采用 Kernel Bypass 技术(如 DPDK, Solarflare Onload),让应用程序直接接管网卡,绕过操作系统的内核协议栈,将网络延迟从几十微秒降低到几微秒。
- 无锁化设计: 在多核环境下,Quote Processor 内部的并发控制是瓶颈。除了用 `sync.Map`,更激进的方案是使用 CPU 核绑定的数据分片。例如,一个 16 核的机器,可以启动 16 个 Quote Processor 线程,每个线程处理一部分交易对的报价更新,彼此之间无锁。撮合引擎在查询时,需要向对应的核发请求。
- 高可用(HA): Quote Processor 是一个有状态的服务,它的高可用必须考虑。通常会采用主备(Primary/Standby)模式。主节点处理所有实时更新,并同步将报价日志通过低延迟网络复制给备节点。当主节点宕机时,可以秒级切换到备节点,由于备节点拥有几乎完整的实时状态,业务中断时间可以控制在毫秒级。
架构演进与落地路径
没有一个系统是一开始就设计得如此复杂的。理解其演进路径对于在不同业务阶段做出正确的技术决策至关重要。
- 阶段一:初创期 (MVP):
在这个阶段,系统交易量小,做市商数量也少。完全可以采用最简单的“报价即订单”(Quotes as Orders)模型。即做市商的每次报价都通过标准的 `Cancel/Replace` 接口实现。这套方案开发成本最低,能快速验证业务模型。你需要监控的是撮合引擎的事件队列长度和撮合延迟,一旦这些指标开始恶化,就是架构升级的信号。 - 阶段二:成长期 (主流方案):
当系统面临性能瓶颈时,就必须引入本文详述的“解耦的报价处理器”(Decoupled Quote Processor)架构。这是目前绝大多数交易所采用的主流方案。实施时可以分步走:首先构建独立的 Quote Processor,但暂时只用于展示和监控,撮合依然走老路;待其稳定后,再修改撮合引擎逻辑,引入“报价物化”流程,完成切换。这个阶段的投入产出比最高。 - 阶段三:成熟期 (极致性能):
对于顶级交易所或高频交易平台,当软件层面的优化已经做到极致后,就需要向硬件和底层要性能。这包括引入FPGA(现场可编程门阵列)进行消息解码和过滤,使用Kernel Bypass 网络,以及将整个撮合与报价系统部署在物理托管(Co-location)的数据中心,让做市商的服务器和交易所的服务器放在同一个机柜里,用光纤直连,将网络延迟降到物理极限。这是一个资本和技术都高度密集的阶段。
总而言之,处理做市商报价的核心思想是“隔离与延迟物化”。通过将高频易变的“瞬时状态”与低频稳定的“核心状态”分离开,我们构建了一个能够从容应对市场洪流的、具备弹性的高性能撮合系统。这不仅是对一个具体业务场景的解决方案,更是分布式系统设计中“分而治之”思想的经典体现。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。