具代表性的面試主題

系統設計面試:如何設計安全的 Leader Election 服務?

系統設計困難
OfferCC 編輯團隊發佈 更新

題幹

多個無狀態 worker 需要在任意時刻由一個實例執行定時結算任務。請設計 Leader Election 服務,說明租約、續期、fencing token、故障轉移、網路分區與觀測指標。

題目與適用場景

多個無狀態 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 只用來加速重選,安全性仍由協調器與資源端校驗保證。」

分步深入解答

先定義任期記錄

記錄可包含 election_name、leader_id、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,並核對轉移耗時與拒絕記錄。

公開來源

同類題目

相關面試工具

用 Solve 整理系統設計回答

從澄清需求開始,展開規模、架構、元件選擇和取捨。

查看工具