在交易系统中,强平(Liquidation)与 ADL(Auto-Deleveraging)引擎是最核心、也是最难扩展的部分之一。
本文基于一个单线程顺序 Pipeline 架构的清算服务,系统性分析其分片(Sharding)演进路径,并给出最终推荐方案:以 Symbol 为核心的分片设计(Symbol-based Sharding) 。
一、当前架构:单线程 Pipeline
传统清算引擎采用典型的单线程顺序处理模型:
flowchart TD
subgraph singlePipeline ["单实例 Pipeline - single writer, 顺序处理"]
EventCh[EventCh] --> WAL[WAL 写入]
WAL --> ProcessEvent[ProcessEvent]
ProcessEvent --> Ledger[Ledger - 全量 RiskUnit]
Ledger --> SymbolIndex[SymbolIndex - symbol倒排索引]
Ledger --> DangerQueue[DangerQueue - 4桶分级]
DangerQueue --> Scheduler["Scheduler - 全局优先级堆 + 令牌桶"]
Scheduler --> Executor[Executor - 强平执行]
Executor --> OutCh[outCh - 强平命令]
Executor -->|失败| ADL["ADL Engine - ledger.All()排名"]
ADL --> ADLCh[adlOutCh - ADL命令]
ProcessEvent --> Breaker["CircuitBreaker - 全局熔断"]
end
Snapshot[快照] -.-> Ledger
Recovery[恢复] -.-> WAL
Recovery -.-> Snapshot
核心流程:
EventCh → WAL → ProcessEvent → Ledger
↓
SymbolIndex / DangerQueue
↓
Scheduler → Executor → OutCh
↓
ADL → ADLCh
核心设计假设
- 单线程顺序处理(Single-threaded pipeline)
- 所有状态无锁(in-memory mutation)
- 事件全局有序(Global Seq)
二、问题:为什么必须分片?
这个模型在单机阶段非常高效,但随着规模增长,会遇到瓶颈:
- RiskUnit 数量爆炸
- MarkPrice 高频驱动全局扫描
- ADL 排名复杂度高
- 单线程 CPU 上限
👉 本质问题:
“一个全局状态机 + 单线程”无法支撑多市场并行计算
三、事件路由的本质矛盾
不同事件的“作用域”完全不同:
| 事件类型 | 路由键 | 影响范围 |
|---|---|---|
| MarkPrice | symbol | 所有该 symbol 仓位 |
| Fill | account + symbol | 单个 RiskUnit |
| BalanceUpdate | account | 该账户所有仓位 |
| RiskParamUpdate | symbol | 所有该 symbol 仓位 |
核心矛盾
👉 symbol 维度 / account 维度是正交的
这意味着:
无论按哪个分片键,总会有一类事件需要跨分片广播
四、关键组件的分片亲和性
1)天然可分片组件
| 组件 | 分片适配 |
|---|---|
| Ledger | 可按 RiskUnit 切分 |
| SymbolIndex | symbol 内闭合 |
| DangerQueue | 跟随 Ledger |
| Executor | RiskUnit 粒度 |
2)困难组件(决定架构方向)
🔴 ADL Engine(最关键)
ADL 需要:
- 找同 symbol
- 找反向持仓
- 全局排序(盈利 × 杠杆)
👉 如果按 account 分片:
- 对手方分散在所有 shard
- 需要 分布式 Top-K / 排序 / 一致性
👉 如果按 symbol 分片:
- 完全在本地闭合
✔ 结论:
ADL 强制要求 symbol 维度聚合
🟡 Scheduler(中等难度)
- 当前:全局最小堆 + 令牌桶
- 分片后:每 shard 独立
问题:
- 丢失“全市场最危险优先”
但:
在高吞吐系统中,局部最优 > 全局最优
🟡 CircuitBreaker
- 当前:全局强平速率
- 分片后:局部不可见
解决:
- 引入全局协调器(aggregator)
🟡 BalanceUpdate
- 按 account 扇出
- 在 symbol 分片下需要广播
但频率较低,可接受。
五、三种分片方案对比
方案 A:按 Symbol 分片
flowchart LR
EventRouter[事件路由层] -->|"BTC events"| ShardBTC[Shard-BTC Pipeline]
EventRouter -->|"ETH events"| ShardETH[Shard-ETH Pipeline]
EventRouter -->|"BalanceUpdate(精确广播)"| ShardBTC
EventRouter -->|"BalanceUpdate(精确广播)"| ShardETH
优势
- MarkPrice 无广播
- ADL 本地闭合(关键)
- RiskParam 本地处理
- 业务语义清晰(每 shard = 子市场)
劣势
- BalanceUpdate 需要广播
- 账户状态分散
- 热点 symbol(BTC)可能成为瓶颈
方案 B:按 Account 分片
flowchart TD
EventRouter[事件路由层] --> ShardGroup1["Shard-1: BTC, SOL, DOGE"]
EventRouter --> ShardGroup2["Shard-2: ETH, AVAX, LINK"]
EventRouter --> ShardGroup3["Shard-3: BNB, ADA, DOT"]
优势
- BalanceUpdate 天然闭合
- 账户视图完整
致命问题
- MarkPrice 全局广播(高频)
- ADL 需要跨分片排序(极难)
👉 基本不可行
方案 C:Symbol Group 分片(推荐)
flowchart TD
subgraph ingress [入口层]
GlobalSeqGen["GlobalSeq 生成器"]
AEI["AccountExposureIndex"]
Router["EventRouter"]
end
subgraph shardN ["Shard-N (single writer)"]
ShardWAL["WAL (ShardSeq)"]
ShardPipeline["Pipeline"]
subgraph shardComponents [shard-local 组件]
SLedger["Ledger"]
SIndex["SymbolIndex"]
SDQ["DangerQueue"]
SADL["ADL Engine"]
SExec["Executor"]
end
subgraph schedulerSplit [调度拆分]
LRS["LocalRiskScheduler"]
ERL["ExecutionRateLimiter"]
end
LocalBreaker["Local Breaker"]
end
subgraph coordination [全局协调层]
GlobalBreaker["Global Risk Coordinator"]
RateCoord["Rate Coordinator"]
ShardMgr["ShardManager"]
end
GlobalSeqGen --> Router
AEI --> Router
Router -->|"symbol-routed events"| ShardWAL
ShardWAL --> ShardPipeline
ShardPipeline --> shardComponents
ShardPipeline --> schedulerSplit
LocalBreaker -.->|上报| GlobalBreaker
GlobalBreaker -.->|广播| LocalBreaker
ERL -.->|可选| RateCoord
- 将多个 symbol 组合成 shard
- 进行负载均衡
例如:
Shard-1: BTC, SOL, DOGE
Shard-2: ETH, AVAX, LINK
六、推荐架构:Symbol Owned Shard
这里是本文最重要的结论👇
核心原则
每个 symbol 只属于一个 shard(single-writer per symbol)
关键设计
1)路由层(EventRouter)
- MarkPrice → symbol shard
- Fill → symbol shard
- BalanceUpdate → 精确广播
👉 需要一个核心组件:
AccountExposureIndex
(account → active symbols)
2)双序号模型
不要强行保持全局顺序:
- GlobalSeq:幂等 / 审计 / tracing
- ShardSeq:本地 WAL 顺序
3)单写原则(Single Writer)
不是“单线程”,而是:
每个 shard 只有一个状态写入者
但:
- IO 可以并发
- Executor 可以并发
- 计算可以并发
4)调度模型拆分
原:
Scheduler = heap + token bucket
建议拆成:
- LocalRiskScheduler(优先级)
- ExecutionLimiter(限流)
5)熔断模型
分两层:
- shard-local breaker
- global coordinator(汇总多指标)
七、一个关键前提:逐仓 vs 全仓
当前设计默认:
👉 逐仓(Isolated Margin)
如果未来支持全仓(Cross Margin):
问题:
- 账户净值跨 symbol
- 风险计算需要全局聚合
解决思路:
- 限制 cross account 在同一 shard group
- 或引入 account-level 风险中心
八、分阶段演进路径
Phase 1:抽象层
- 引入 EventRouter
- 支持 shardID
- 仍然单实例运行
Phase 2:多 shard(单进程)
- 多 pipeline
- symbol 路由
- Balance 广播
- 独立 WAL / Snapshot
Phase 3:全局协调
- 限流协调器
- 熔断协调器
- 监控与热点检测
Phase 4:多节点 + 动态迁移
- symbol 迁移
- shard rebalance
- 跨节点部署
九、风险与挑战
1)热点 Symbol(BTC)
- 需要动态 rebalance
- 最终可能需要 symbol 级迁移
2)BalanceUpdate 一致性
- 采用 at-least-once
- 必须幂等
3)Seq 语义变化
- 放弃“全局执行顺序”
- 改为“局部有序 + 全局标识”
4)分片迁移
需要:
- drain
- snapshot
- catch-up replay
- route switch
十、总结
这次分片设计的核心不是“如何切分数据”,而是:
找到真正的“计算闭合边界”
最终答案是:
✔ ADL → 按 symbol 闭合
✔ MarkPrice → 按 symbol 扩散
✔ Risk → 按 symbol 聚合
因此:
Symbol 是唯一正确的一阶分片键
最终结论
👉 推荐方案:
Symbol-based Sharding + Group-based Deployment
同时遵循:
- 单写(single writer per shard)
- 双序号(GlobalSeq + ShardSeq)
- 精确路由(AccountExposureIndex)
- 分层调度(Scheduler / Limiter 分离)