大型加密貨幣交易所架構實戰系列(四):Leader Election、高可用切換與跨服務協調
前言
分區、分片與事件流解決的是吞吐量與資料一致性的一大半問題,但大型交易所真正容易出事故的地方,往往發生在故障切換與協調。例如同一個 symbol 的撮合工作不能同時有兩個節點接手,同一組清算任務不能被兩個 worker 重複執行,週期性對帳也不能在多個節點上同時跑到互相打架。
這時候你就會遇到 Leader Election、lease、heartbeat、fencing token、split brain 這些名詞。它們看起來像分散式系統課本內容,但在交易所場景裡非常務實,因為只要選主做錯,後果不是單純多跑一個 job,而可能是重複清算、重複扣款、重複對帳,甚至同一資源被雙寫。
本文會把 Leader Election 放回交易所真實使用場景來解釋,並補上 MySQL / PostgreSQL 在協調鎖與選主上的差異。你也會看到一個很重要的觀念:不是所有服務都要選主,但凡是「同一時間只能有一個節點負責」的工作,都必須先想清楚協調模型。
系列文章導航
- 撮合引擎、In-Memory、Ring Buffer 與批次處理
- Event Sourcing、Outbox Pattern、Message Queue 與一致性
- Partition、Sharding 與 MySQL / PostgreSQL 擴展策略
- Leader Election、高可用切換與跨服務協調(本篇)
- MySQL、PostgreSQL 的擴展、調校與效能優化
哪些交易所元件真的需要 Leader Election
不是每個服務都需要 leader。像純讀取型 API、無狀態 Web 節點,通常只要 load balancing 就能解決。但以下這些場景經常需要「同一時間只能有一個主責節點」:
- 某個 symbol 的撮合主節點
- 某組 liquidation scanner 的工作協調者
- outbox relay 的單一分區送出者
- 每日對帳與結算任務的主執行者
- 風控快照或狀態同步的主控節點
不要把所有事情都做成 leader-based
如果一個工作本來就可以透過 partition assignment 自然分配,例如 Kafka consumer group 依 partition 消費,那你未必要額外套一層 leader election。
所以第一個判斷原則是:
- 如果工作可以天然切分,就切分。
- 如果工作不能同時被兩個節點執行,就選主。
Leader Election、Distributed Lock、Partition Assignment 不一樣
這三個詞很常被混用,但在設計上其實不同。
Leader Election
目標是從多個候選節點中選出一個 leader,讓它暫時負責某個角色,例如 matching-leader-BTCUSDT。
Distributed Lock
目標是保護某段臨界區,例如「同一時間只能有一個 worker 執行每日結算」。它未必包含完整的 leader 身分與持續性租約語意。
Partition Assignment
目標是把多個工作分配給多個消費者,例如 32 個 topic partitions 分配給 8 個 consumer。這更像負載分配,不一定需要單一 leader。
理解這三者差異很重要,因為大型交易所常常三種都會用,但不能亂套。
交易所最怕的不是沒 leader,而是 split brain
Split Brain 指的是兩個節點都以為自己是 leader。對交易所來說,這種情況非常危險。
可能造成的後果
- 同一 symbol 出現雙撮合者
- 同一批 outbox 事件被重複發送
- 同一筆清算工作被做兩次
- 同一份風控狀態被兩邊覆寫
所以真正重要的不是「能不能選出 leader」,而是:
- 舊 leader 失去資格時,是否能被可靠剝奪權限
- 新 leader 接手前,是否能確定自己拿到的是最新可用狀態
這就會引出另一個重要概念:Fencing Token。
Fencing Token 是什麼
你可以把它想成每次當選 leader 時拿到的一張遞增序號門票。只有持有最新門票的節點,才被允許對外寫入。
即使舊 leader 因為網路抖動一度以為自己還活著,只要它手上的 token 已經過期,下游就應該拒絕它的寫入。
Go 示例:用 etcd lease 做簡化版 leader election
下列程式碼展示的是常見思路,而不是完整生產級樣板:
1 | package main |
這段程式碼背後的重點包括:
- leader 身分是綁在 lease 上,不是永遠有效。
- 如果節點失去 lease,領導權就應該失效。
- 失去 leadership 後,工作必須停止寫入或立刻進入降級狀態。
為什麼交易所常用 etcd / ZooKeeper / Consul 這類系統
因為它們本來就是為了協調、租約、觀察變更、避免 split brain 這類問題設計的。把這類職責交給專門的協調系統,通常比把它硬塞進主資料庫更合理。
故障切換不是切過去就好,還要能安全恢復
一個比較健康的 failover 流程通常像這樣:
flowchart TD
A[Leader 正常運作] --> B[Leader 失去 lease 或故障]
B --> C[Standby 競選 leadership]
C --> D[取得新 token]
D --> E[載入 snapshot]
E --> F[replay 未處理事件]
F --> G[對外恢復服務]
這張圖有兩個關鍵:
- 先取得合法領導權,再接手工作
- 先恢復最新狀態,再對外服務
如果少了第二步,你雖然切過去了,但可能拿舊狀態對外處理新請求,一樣會出事。
對交易所來說,順序通常比時鐘更重要
很多人在設計故障切換時會很在意時間戳,但對交易所而言,更重要的常常是同一分區內的事件順序。
為什麼不能只相信 clock time
- 不同節點時間可能有些微漂移。
- 網路延遲會讓接收時間不等於發生時間。
- 相同毫秒內可能有大量訂單進來。
因此在核心鏈路上,更常見的做法是:
- 對每個 symbol 或每個 partition 產生單調遞增 sequence
- 用 sequence 決定 replay 與接手位置
- 把時間戳當輔助資訊,而不是唯一排序依據
這也是為什麼前幾篇談到的 event log、snapshot、replay,會在 failover 時再次出現。
MySQL / PostgreSQL 可做輕量互斥,但不適合當核心選主控制平面
可以,但要知道界線在哪裡。
更精確地說,GET_LOCK() 與 advisory lock 比較像 lightweight distributed lock,適合做某些互斥任務;但如果你真正要的是帶有 lease、觀察變更、fencing token、split brain 防護的核心 control plane,那就不該把責任全部壓在主資料庫上。
MySQL 常見做法
MySQL 有 GET_LOCK() 這類能力,可以在某些輕量工作上做分散式互斥,例如:
- 某個週期性 job 同時只允許一個節點執行
- 某個小型維護任務避免重複跑
但如果你把核心 HA 協調完全壓在主 MySQL 上,就會遇到幾個問題:
- 選主系統和業務主庫耦合太深
- 主庫 failover 時,鎖語意與觀察延遲要重新驗證
- 連線中斷是否等於權限立即失效,需要嚴格測試
PostgreSQL 常見做法
PostgreSQL 有 advisory lock,也能處理一些輕量協調需求,例如:
- 單一批次任務互斥執行
- 特定 maintenance job 避免重入
但與 MySQL 類似,如果你把它當成大型 HA control plane,也會有相同的邊界:它是資料庫,不是專門的共識協調系統。
實務建議
- 輕量互斥:可以考慮 MySQL
GET_LOCK()或 PostgreSQL advisory lock。 - 核心高可用協調:優先考慮 etcd、ZooKeeper、Consul 或 Kubernetes Lease。
- 真正重要的切換:搭配 fencing token、snapshot、replay,不只做鎖本身。
Reservation 超時回收、補償與 Replayer 也需要協調
只要你導入 Redis Shift Left、outbox、DLQ、reconciliation,你就不只是在協調 leader,還是在協調誰有資格執行補償與回收工作。
幾個典型場景
- Redis 已成功扣留資金,但 PostgreSQL 的訂單與帳本資料沒有成功耐久化。
- 訂單逾時或撤單後,需要把
held資金釋放回available。 - DLQ 中的事件要重放,但同一批訊息不能被兩個 replayer 同時處理。
- 對帳 job 發現不一致,需要啟動補償流程。
這些工作如果沒有明確的擁有者,就很容易出現:
- 重複釋放資金
- 重複補償
- 重複重放事件
- 補償與正常流程互相覆蓋
所以在大型交易所裡,除了撮合主節點之外,下列 worker 也常需要清楚的 partition ownership 或 leader / lease:
- reservation sweeper
- order expiration worker
- reconciliation worker
- dead-letter replayer
- outbox relay
換句話說,HA 協調的範圍不只限於「誰來對外服務」,也包括「誰來做善後」。
Backpressure、Consumer Group 與選主常常一起出現
在交易所裡,leader election 不只出現在主流程,也常和 MQ 消費模型一起出現。
例如:
- 一個 topic 的某些 partitions 只應由某些 consumer 接手
- 一組 liquidation workers 需要避免重複掃描相同帳戶
- 一個 outbox relay 只能有一個活躍送出者,但下游慢時又要能限流
因此在真實系統中,你會看到下面幾個概念一起出現:
- Lease:領導權有效期限
- Heartbeat:持續宣告自己還活著
- Backpressure:下游慢時控制上游速度
- Circuit Breaker:下游持續異常時先保護主流程
- Dead Letter Queue:不能處理的事件先隔離
這些機制共同作用,才構成真正可用的 HA 系統。
總結
Leader Election 在大型交易所裡不是教科書裝飾,而是避免雙寫、重複執行與 split brain 的必要機制。真正要掌握的重點有四個:
- 不是所有服務都需要 leader,能切分就先切分。
- 需要單一主責節點的工作,必須有明確 lease 與失效機制。
- 只會選主還不夠,還要能安全恢復狀態與順序。
- MySQL / PostgreSQL 可以做輕量互斥,但核心 HA 協調通常更適合交給專門系統。
下一篇我們會把鏡頭拉回資料庫層,具體整理在超大交易量之下,MySQL 與 PostgreSQL 各自有哪些擴展、調教與優化策略,以及什麼場景更適合選哪一種。







