用 actor 模式分片 OMS
一个 clustered 撮合引擎就是一台单一的确定性状态机:它一次一条消息地消费 replicated log,在一条 逻辑线程上、以一个固定顺序处理。这正是它可重放、可崩溃恢复的原因——也是它的天花板。单个 cluster 的撮合吞吐受限于单核能顺序处理多少。当一台机器不够用时,你不会把状态机改成并发的(那会破坏 确定性);你运行更多台,每台拥有 instrument 全集的一个切片。actor 模式就是干净地做到这一点 的纪律。
actor 即分片
Section titled “actor 即分片”actor 是一个单线程单元,拥有一块状态分区,一次一条地处理它的 inbound mailbox,没有共享可变 状态、没有锁。这段描述其实已经就是 Aeron Cluster 的 service:消息 handler 就是 actor, replicated log 就是 mailbox。用 actor 模式分片只是把这一点显式化,并复制 N 份——每个 actor 拥有 一组互不相交的订单簿,绝不伸手去碰另一个 actor 的状态。
让它成立的性质:
- 每个订单簿单写者。 某个 symbol 的所有订单由且仅由一个 actor 全序化。订单簿从不存在争用, 因此没有锁要调、没有竞态要推敲。
- 每个分片各自保持确定性。 每个 actor 仍是一台确定性状态机(见 下文),所以每个分片可独立重放。
- 并行来自数量,而非并发。 吞吐靠增加分片来扩展,而不是在分片内加线程。
在 Aeron 上落地的两种方式
Section titled “在 Aeron 上落地的两种方式”方案 A —— 每个分片一个 Aeron Cluster。 每个分片是独立的 Raft group,有自己的 replicated log、 自己的 leader、自己的 snapshot。这是唯一能真正水平扩展吞吐的选项:每个分片的 log 在不同主机的 核上被消费,所以总撮合能力随分片数增长。它还带来独立的爆炸半径(一个分片的 failover 不影响其他), 代价是更多运维面——更多 leader 要放置、更多 log 要监控、更多升级要滚动。
方案 B —— 在一个 cluster 内复用多个 actor。 单个 clustered service 把每条 inbound 消息按分片路由 到一个进程内 actor。这对代码隔离和状态组织很干净,但它不扩展吞吐:单条 Raft log 仍在一个核 上被顺序消费,所以所有 actor 共享那一个核的预算。用它把订单簿在你已知能装进一个 cluster 的容量 包线内做逻辑分离——而不是用来提速。
经验法则: 隔离逻辑 → 方案 B 够用。隔离负载 → 你需要方案 A。混用(少数几个 cluster, 每个托管若干逻辑分离的 actor)很常见也很合理。
在一个 cluster 内复用多个订单簿(方案 B 细节)
Section titled “在一个 cluster 内复用多个订单簿(方案 B 细节)”当一个 cluster 托管多个 symbol 时,clustered service 不需要一套根本不同的设计——它按 symbol 复用。 因为订单簿天然独立(BTC/USDT 的一次撮合绝不会碰到 ETH/USDT 的订单簿),这几乎是纯粹的分区 ——是简单的方向,与下文的跨分片账户问题形成鲜明对比。
相对单订单簿 cluster 的变化:
- 按 symbol 分发。 handler 读取订单的 symbol,路由到该 symbol 的进程内
OrderBook——一张symbol → OrderBook映射。一个 handler,N 个订单簿。 - 每个 symbol 的状态干净分区。 每个订单簿是自己的结构(bids/asks/sequence)。没有订单簿读或写 另一个的状态——同样的单写者纪律,只是现在 N 个单写者活在一条线程上。
- snapshot = cluster 内每个订单簿。 Aeron snapshot 序列化所有常驻订单簿,所以 snapshot 大小与 恢复时间随每 cluster 的 symbol 数增长——这是实打实的容量规划成本。
- 每个 symbol 的定时器跑在共享的 consensus clock 上。 GTD/GTT 到期、集合竞价排程——每个订单簿 各自装配自己的定时器,但它们在这个 cluster 的 replicated time 上触发,并在这一条线程上处理。
不变的——也正是方案 B 的要义——是所有订单簿共享一条 Raft log、一个 leader、一个核,被 顺序消费。增加订单簿是组织状态,而非增加吞吐。两个后果:
- 共享线程上的公平性。 某个热订单簿的突发流量会拖累其他订单簿的延迟,因为它们在 log 顺序里排 在其后。对多数交易所而言,按 committed-log 顺序处理即可;若不行,你需要一套跨订单簿的公平分发/ 批处理策略——或者说你在那个 symbol 上已经撑破了方案 B,应当把它隔离出去(方案 A)。
- 放置才是真正的决策。 把低成交或相关的交易对放在一起(它们装得进一个核的预算,可能共享竞价
逻辑);把热 symbol 隔离到它自己的 cluster。这正是选择分片键里的
symbol → shard映射。
分片键必须是一致性单元,而对撮合引擎而言这个单元就是订单簿——某个 symbol 的每一笔订单都 必须与该 symbol 的其他所有订单全序化。所以你按 instrument / symbol 分片,绝不低于它。
- 静态分区映射(
symbol → shard) 是默认。它可审计,允许你有意地把相关 instrument 放在一起, 也允许你把某个热 symbol 隔离到它自己的分片。代价是平衡决策由你自己负责。 - 一致性哈希 免去手工放置,但也拿走了那份控制力——而且哈希无法尊重「这两个 symbol 一起交易, 放在同一分片」。对一个已知、缓慢变化的 instrument 列表的交易所,带版本的静态映射通常更优。
- 单个热 symbol 无法拆分。 它的订单簿本质上单线程。你手上的杠杆是放置——给一个非常活跃的 symbol 它自己的分片,而不是试图并行化它的订单簿。
路由:gateway 与映射
Section titled “路由:gateway 与映射”ingress gateway 把每笔订单映射到它归属的分片,并转发到那个分片的 cluster ingress。这张路由映射 是承重的共享真相:gateway 与各分片必须认同同一张带版本的映射。漂移——一个 gateway 把 symbol X 发到 shard 2,而 shard 1 仍以为自己拥有 X——就是经典的「配置导致的脑裂」bug。给映射加版本、原子地 分发,并让分片拒绝它不拥有的 symbol 的订单,而不是默默接受。
跨分片问题(难点所在)
Section titled “跨分片问题(难点所在)”按 symbol 分片很干净,直到出现跨 symbol 的东西。三个常见的麻烦制造者:
- 账户余额 / 保证金 / 风险。 买力是按账户的,不是按 symbol 的,所以它不天然存在于某个 按-symbol 的分片里。常见形态是一个独立的、按账户分片的 risk/account actor,配以交易前的 信用预留:下单分片在撮合前对账户分片做预留,之后再释放或结算。这是一个 saga,而不是分布式 事务。
- 多腿 / 篮子订单 触及多个订单簿,需要一个 coordinator 驱动各腿并在部分失败时补偿——同样是 saga,其所有步骤都流经 log,以保证整体可重放。
- 跨账户的自成交防护(STP),当一个账户的订单落在不同分片时,需要账户级 actor(或一个共享的、 replicated STP 检查),而不是每订单簿一个。
指导原则:让热路径保持单分片。 跨分片协调是给例外情况用的;如果你的常规订单需要两个分片 同步达成一致,那说明分片边界划错了地方。
确定性不会放松,只会成倍
Section titled “确定性不会放松,只会成倍”分片不会让你在那条使 Aeron 重放成立的规则上得到任何松动。每一个 actor 仍必须是确定性状态机: handler 里没有墙钟读取、没有 RNG、没有外部 I/O、没有非确定的迭代顺序。(基础见 确定性状态机。)两条分片特有的推论:
- 跨分片协调必须流经 log。 从下单分片发往账户分片的预留消息,必须是双方都真实记录的 ingress/egress 消息——绝不能是带外 RPC——否则你就引入了破坏重放的旁路。
- 时间与定序是按分片的。 每个分片有自己的 log position 与自己对「现在」的理解(从它的 log 确定性 地推导)。别假设两个分片的时间线可比;若你需要跨分片的全局排序,必须显式地构建它。
重分片:在分片间移动一个 symbol
Section titled “重分片:在分片间移动一个 symbol”再平衡是一次小型迁移,也是一个分片化 OMS 里最危险的例行操作。安全的形态:
- 静默(quiesce) 源分片上的该 symbol(停止为它接收新订单;让在途工作排空)。
- snapshot / 序列化 那个订单簿的状态。
- 移交 序列化状态给目标分片并让它加载。
- 原子地翻转路由映射 —— 提升版本号,让 gateway 开始把该 symbol 发往新拥有者,旧拥有者开始 拒绝它,在同一个逻辑瞬间发生。
- 在目标分片上恢复。
做好容量规划以尽量少做这件事:给分片数与每分片 symbol 数留出余量,并把热 symbol 隔离,使单个订单簿 的增长绝不会逼你重分片。
Failover 与运维
Section titled “Failover 与运维”- 按分片的 Raft。 方案 A 下每个分片选举自己的 leader。把 leader 分散到不同主机与 AZ, 这样丢一台主机不会同时斩首多个分片——见 cluster standby 与 HA 设计。
- 按分片监控。 commit position、每个分片 ingress 的背压、NAK/error 计数——一个安静的聚合值可能 掩盖一个生病的分片。
- 升级按分片数成倍。 一次滚动升级现在是 N 次滚动升级;把它自动化,并对每个分片套用与 滚动升级相同的 follower 优先、leader 最后的纪律。
- actor 之间共享可变状态。 如果两个 actor 能碰同一个字段,你就不再拥有 actor——你拥有一把忘了 写的锁。
- 热路径上的同步跨分片调用。 把独立的分片变成一个分布式的锁步系统,延迟等于最慢参与者的延迟。
- 路由映射漂移。 gateway 与分片对谁拥有某 symbol 各执一词。给映射加版本,并让所有权在分片上具 权威性。
- 拆分单个热订单簿。 在保持全序的前提下做不到——隔离它,别拆它。
本站相关基础:cluster 与 Raft 概览、 扩展 Aeron Cluster、 性能调优,以及本节的 滚动升级流程。
本站与 Adaptive Financial Consulting Limited 或 Aeron 项目无任何关联,未获其背书或赞助。 Aeron 是 Adaptive Financial Consulting Limited 的注册商标。
Aeron 是 Adaptive Financial Consulting Limited 在英国及其他国家/地区的商标。