題目與適用場景
多個無狀態 worker 需要在任意時刻由一個實例執行定時結算任務。實例會崩潰、重啟或發生網路分區,系統不能讓兩個 worker 長時間同時寫入。請設計 Leader Election 服務,說明協調儲存、租約、續期、fencing token、故障轉移與運維邊界。
面試官考察重點
是否定義安全與活性
安全性是同一期別內最多一個有效 leader 被下游接受;活性是在舊 leader 失效且確認租約過期後,健康候選最終能接任。網路分區下不能為了可用性無條件宣布新 leader。
是否選擇有共識語意的協調器
leader 記錄必須放在具備線性化 CAS、租約與 watch 語意的系統中,例如 etcd。僅靠 Redis TTL 或本地時鐘無法獨立證明舊 leader 已失效。
是否防止舊 leader 寫入
租約過期後,舊程序可能暫時恢復並繼續工作。每次下游寫入都應帶遞增 fencing token,並由資源端拒絕過期 token,避免雙主造成資料破壞。
回答前要釐清的問題
- 任務是否允許重複執行,還是下游必須嚴格串行接受?
- 選主範圍是全域、租戶、分片還是單一任務?
- 可接受的故障轉移時間與停頓窗口是多少?
- 協調器部署幾個故障域,法定人數與備份策略為何?
- 下游能否校驗 fencing token,任務是否支援冪等恢復?
- 是否需要 watch 事件、審計記錄、告警與人工強制轉移?
30 秒回答框架
「我會先定義選主範圍與故障預算,使用具備共識與線性化條件寫入的協調器保存 leader 記錄。候選透過交易建立或比較更新租約,成功者取得遞增 fencing token,並在 TTL 內續期;續期失敗就停止承接新任務。所有下游寫入都校驗 token,舊 leader 即使恢復也不能寫。watch 只用來加速重選,安全性仍由協調器與資源端校驗保證。」
分步深入解答
先定義任期記錄
記錄可包含 electionname、leaderid、lease_id、term、候選元資料與更新時間。term 或 fencing token 必須單調遞增,並由協調器在原子交易中分配,不能由客戶端本地時鐘生成。
選擇協調器與寫入條件
候選先建立帶租約的暫存記錄;若記錄不存在,線性化交易才允許寫入。已有 leader 時,候選 watch 記錄變化並重試。etcd election API 用同一 election name 競爭,確保同一時刻只有一個成功者。
續期與主動降級
leader 使用獨立 keepalive 定期續租,續期逾時、連線失效、程序暫停或本地時鐘異常都應進入 suspect,停止新任務與下游寫入。無法聯絡協調器時不能繼續工作。
處理故障轉移
候選不能只根據本地 TTL 判斷舊 leader 已死亡;必須看到協調器確認租約過期或記錄被刪除,再透過 CAS 競爭。轉移時間由 TTL、偵測間隔與排程延遲共同決定,應設上限並留出抖動餘量。
加入 fencing token
新 leader 取得更大的 term,把 token 附在資料庫更新、訊息發布或外部 API 請求上。資源端保存已接受的最大 token,拒絕更小 token;這讓舊程序即使尚未消失也不能寫入。
面對網路分區與腦裂
少數派分區不能繼續發放新任期。若候選無法與法定人數協調器通訊,就只能停止或唯讀。恢復連線後,舊 leader 仍需重新觀察當前任期並重新競爭。
偽程式碼
~~~text campaign(): lease = coordinator.grant(ttl) result = coordinator.txn(key absent -> put(candidate, lease, next_term)) if result.succeeded: token = result.term keepalive(lease) runwithfencing(token) else: watch(key)
onkeepalivefailureorexpiry: stopnewwork() stopdownstreamwrites() ~~~
複雜度、恢復與觀測
每次選舉與續期都依賴協調器往返,候選數增加會帶來 watch 與重試負載;應使用指數退避與抖動。記錄當前 leader、term、續期 RTT、租約過期次數、選舉耗時、fencing 拒絕、重複任務與協調器法定人數。
| 機制 | 解決的問題 | 仍需補強 |
|---|---|---|
| 線性化 CAS | 避免兩個候選同時成功 | 協調器必須有法定人數 |
| 租約 keepalive | 偵測程序失效 | 暫停與分區仍可能留下舊程序 |
| fencing token | 拒絕舊 leader 寫入 | 下游必須持久化並校驗 token |
| watch 與退避 | 加速重選並降壓 | 不能取代安全性證明 |
高品質示範回答
「我會使用具備共識與線性化交易的協調器保存帶租約的 leader 記錄。候選透過『記錄不存在才寫入』的 CAS 競爭,成功者取得遞增 term,並在 TTL 內 keepalive;無法續租就立即停止新任務與下游寫入。每個資料庫寫入、訊息或外部呼叫都帶 fencing token,資源端拒絕小於已接受 token 的請求,因此舊 leader 即使從暫停或分區中恢復也不能繼續寫。少數派不能發放新 term,watch 只負責降低重選等待。監控續期 RTT、任期、轉移耗時、fencing 拒絕、重複任務與 quorum,並保留人工停用開關。」
常見錯誤
只使用 Redis TTL
TTL 到期與客戶端看到到期不是同一件事,網路延遲和暫停可能讓兩個客戶端都認為自己可以工作。需要協調器的線性化語意與下游 fencing。
把租約當成寫入保護
租約只能幫助偵測失效,不能讓舊程序瞬間停止。沒有 token 校驗,舊 leader 仍可能覆蓋新 leader 的結果。
用本地時間生成任期
機器時鐘可能漂移、回撥或暫停。任期必須由協調器原子分配與持久化。
網路分區時強行自動接管
少數派無法確認舊 leader 狀態,強行接管會製造 split-brain。安全設計應允許短暫不可用。
讓 watch 事件直接決定安全狀態
watch 可能遺失、延遲或重連。它只能觸發重新讀取和 CAS,不能取代線性化讀寫。
沒有任務級冪等
即使選主正確,崩潰恢復與訊息重投仍可能重複執行。任務需要冪等鍵、進度記錄或可重播交易。
追問與應對
Leader Election 和分散式鎖有何不同?
鎖通常保護一次臨界區;選主維護較長生命週期的協調角色,需要任期、續期、watch、fencing 與故障轉移語意。兩者可共用協調器,但安全問題範圍不同。
TTL 應設定多長?
它要覆蓋正常續期 RTT、GC 或排程暫停、網路抖動與可接受故障轉移時間。太短會頻繁重選,太長會延遲接管,應透過故障注入校準。
為什麼 fencing token 要由資源端檢查?
協調器無法讓所有舊程序立即停止。資源端拒絕舊 token,才能在舊程序仍存活時阻斷危險寫入。
etcd 叢集失去 quorum 時怎麼辦?
不能提交新的任期或租約狀態;現有 leader 也應在無法續期後停止寫入。恢復 quorum 後再重新競爭。
如何處理 leader 長時間暫停?
暫停期間續期失敗,協調器可讓新候選接任。舊程序恢復後必須攜帶舊 token,被下游拒絕並重新參加選舉。
如何驗證沒有雙主?
注入程序暫停、網路分區、時鐘跳變與協調器節點故障,檢查同一任期的接受寫入是否始終只有一個 token,並核對轉移耗時與拒絕記錄。