技术博客

灾备与同城双活:撮合状态怎么同步——内存状态的复制难题

第 35 / 141 篇 · 2026-06-13 · secfin · 灾备高可用撮合引擎

Web 服务的灾备答案是标准动作:无状态实例多活,状态扔数据库,数据库主备同步。撮合核心把这条路线堵死了——订单簿是高频读写的内存结构,"扔数据库"等于放弃低延迟(内存撮合篇刚算过这笔账)。这篇讲高可用目标怎么翻译到撮合系统上,以及内存状态复制的三条技术路线。

先把目标量化:RPO 和 RTO

灾备设计从两个数字开始:

  • RPO(Recovery Point Objective):允许丢多少数据,通常用"宕机时已确认但丢失的事件数"衡量;
  • RTO(Recovery Time Objective):允许多久恢复服务,从宕机到新主开始撮合的时间。

交易系统的特殊性在于 RPO 接近零是硬要求:已经给客户发了成交确认的委托,宕机后不能消失;反过来,没发确认的单子丢了可以靠查询重报补回来。所以真实的 RPO 约束是"已确认的不能丢,未确认的允许补"——确认回报本身成了一致性边界,这和回报篇"幂等键消化重复"正好接上。

可用性账也要先算。单机可用性 $A$ 由平均无故障时间和平均修复时间决定:

$$ A = \frac{\text{MTBF}}{\text{MTBF} + \text{MTTR}} $$

主备串联(备机平时不接流量)时系统可用性是 $1 - (1-A)^2$ 的前提是切换可靠——切换本身有概率失败(脑裂、数据不全、判断失误),这部分概率要算进 MTTR。灾备架构一半的工作是在把"切换失败"的概率往零压。

为什么撮合不能双活

先澄清一个概念误区:"同城双活"对撮合核心是个伪命题。双活意味着两个节点同时对外服务、同时写状态。订单簿的撮合规则要求同一合约的委托按到达顺序串行处理,两个写入方必然引入"谁排前面"的协调,协调本身要一次网络往返——同城光纤往返约 300μs(两地机房 30km 量级),这笔延迟加进每笔撮合,等于把内存撮合的优势全赔进去。交易所自己也是单点撮合加主备,行业里没人给撮合做双活。

所以撮合核心的高可用形态只有一种:主备(active-standby),加上把"备"做成"热"的各种手段。难点全部集中在:备机的状态怎么跟。

内存状态的三条复制路线

路线一:共享存储。 状态写共享磁盘阵列(SAN),主机死了备机挂同一块盘接着跑。数据库时代的老思路。问题:内存撮合每次状态变更(指针调整、档位更新)都落到共享存储,延迟回到磁盘时代,直接自杀。此路不通,只存在于历史系统。

路线二:状态同步复制。 主机每处理一笔,把状态增量(订单簿 diff)同步发给备机。直观,但有两个深坑:一是顺序依赖,增量必须按撮合顺序应用,网络重排要么靠 TCP(有队头阻塞)要么自己设计序列号和重传;二是同步等待,发完增量等备机 ACK 再处理下一笔,等于在热路径上加了同城往返 300μs——异步发送则回到"备机落后多少"的老问题。折中方案是批量异步 + 流水窗口,但崩溃时窗口内的状态丢失量就是 RPO,数学上绕不开。

路线三:输入日志重放(主流选择)。 回到内存撮合篇的确定性性质:撮合是输入的纯函数。那就不需要复制"状态",只需要复制"输入"——主机的输入日志按序流给备机,备机跑一个只演算不发回报的影子撮合,状态自然与主机一致。好处一箭三雕:

  • 传输量小(输入是原始报文,比状态 diff 省一个量级);
  • 天然保序(日志本身有序列号);
  • 备机持续演算,本身就是最强的主机健康校验——两边状态哈希定期对拍,不一致立刻报警。

RPO 账:丢失的只是日志流里"已发未达"的那一段,靠可靠性更高的同步通道(或双通道并发)压缩到毫秒级。RTO 账:备机状态是热的,切换只需接管会话和开始发回报,秒级可达。

切换的艺术:脑裂与 fence token

主备架构的终极敌人是脑裂:网络分区时主机其实没死,备机也升了主,两个"主"同时撮合同一批委托,成交重复、订单簿分叉——这比宕机可怕得多。标准解法是一套组合拳:

  1. 第三方仲裁:升主前必须拿到仲裁节点(或仲裁集群)的多数票租约(lease),租约有有效期,原主失去仲裁后自动降级;
  2. fence token:每个主节点持单调递增的代际号,报文里带上;交易所侧或下游校验代际号,旧代的报文直接拒绝——即使脑裂真的发生,旧主发的单也进不了市场;
  3. 会话接管协议:备机升主后向交易所重建会话,用委托查询报文(Q 类)拉全量在途委托,与本地状态对拍,缺的补、多的废,全部靠幂等键防重复。

切换决策也值得较真:心跳丢失后等多久升主?太短误切(原主只是网络抖动,切换反而制造缺口),太长 RTO 超标。一般取心跳间隔的 3~5 倍,并接受偶发误切的代价——误切的恢复路径(原主降级为备、重新追赶日志)是演练出来的,不是文档写出来的。

主备热备:输入日志流 + 影子重放 + 定期状态对拍主撮合(发回报)持有仲裁租约备机影子撮合只演算不发回报输入日志(有序 + 双通道)状态哈希对拍仲裁(租约多数票)切换流程:心跳丢失 3~5 个周期 → 拿仲裁租约升主 → 向交易所重建会话、Q 类报文拉在途委托对拍 → 以幂等键消化重复回报防脑裂:fence token 单调代际号,旧主报文被交易所侧拒绝

收尾一句话:撮合高可用没有银弹,只有取舍——用"输入复制"换掉"状态复制",用"确定性重放"换掉"双活幻想",用"仲裁 + fence"锁住脑裂。把 RPO 压到"已确认不丢"、把 RTO 压到秒级,剩下的概率风险交给演练和预案。灾备系统平时的状态就是浪费,它唯一的价值在宕机那几分钟——而那几分钟的表现,取决于过去十二个月做了多少次真实切换演练。