清算服务分片设计:从单线程 Pipeline 到 Symbol 分片架构

4 阅读4分钟

在交易系统中,强平(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 上限

👉 本质问题:

“一个全局状态机 + 单线程”无法支撑多市场并行计算


三、事件路由的本质矛盾

不同事件的“作用域”完全不同:

事件类型路由键影响范围
MarkPricesymbol所有该 symbol 仓位
Fillaccount + symbol单个 RiskUnit
BalanceUpdateaccount该账户所有仓位
RiskParamUpdatesymbol所有该 symbol 仓位

核心矛盾

👉 symbol 维度 / account 维度是正交的

这意味着:

无论按哪个分片键,总会有一类事件需要跨分片广播


四、关键组件的分片亲和性

1)天然可分片组件

组件分片适配
Ledger可按 RiskUnit 切分
SymbolIndexsymbol 内闭合
DangerQueue跟随 Ledger
ExecutorRiskUnit 粒度

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 分离)