【從一台 Server 到分散式架構】第 18 篇:多間廚房誰是值日生?——分散式協調與領導者選舉
WebSocket 上線後,小明他們的通知系統終於讓人滿意。學生交作業,老師的頁面會跳出提示;老師回覆,學生也能立刻看到。
但某天早上,客服群組傳來一段對話截圖:
「我買課後收到三封確認信,是系統出 bug 嗎?」
小明打開後台查了一下,發現那個時間點剛好有一次背景任務的調度——系統每天早上八點會跑一支「寄送每日學習提醒信」的定時腳本。
問題是,他們的後端現在有三台機器,而這三台機器同時都在執行同一支腳本。
問題的根源:多台機器,各自為政
在只有一台機器的時候,「每天八點執行一次」是個簡單的問題——開一個 cron job,設定排程,搞定。
有了多台機器之後,事情就不一樣了。
每台機器上都跑著同一份程式,每台都看得到同一份設定,每台都在八點整同時觸發。
結果:
- 三台機器各自查出「今天還沒發信的用戶列表」
- 三台機器各自把這份列表走完,各自發了一次信
- 每個用戶收到三封信
這不是 bug,是分散式系統的預設行為——你沒有明確告訴它「只讓一台做」,它就讓全部都做了。
生活化的比喻:掃地值日生的混亂
想像一間教室,老師說:「今天請一個人掃地。」
教室裡有 30 個學生。如果老師沒有明確指定,可能發生:
- 沒人去掃(大家都以為別人會去)
- 三個人同時去搶同一把掃把,重複打掃同一個區域
多台伺服器的處境很像這樣。分散式協調要解決的,就是:
如何讓「多個競爭者」可靠地決定「誰來做這件事」,而且只有一個人做。
解法一:分散式鎖(Distributed Lock)
最直覺的思路:在任務開始前先「搶鎖」,搶到的人才能做,沒搶到的跳過。
概念:
任務開始前:
嘗試 SET lock:daily-email "server-1" NX EX 300
→ 成功(拿到鎖):執行任務,結束後釋放鎖
→ 失敗(鎖被別人拿走):這台機器跳過,不做
用 Redis 的 SET key value NX EX seconds 可以很簡單地做到分散式鎖:
NX:只有 key 不存在時才設定(搶鎖)EX seconds:設定過期時間(避免拿到鎖的機器掛掉後鎖永遠不釋放)
這樣,三台機器同時來搶,只有一台搶到,另外兩台發現鎖已存在,直接跳過。
小明看完之後說:「這跟第 05 篇的 Redis 差不多?」
小傑點頭:「Redis 已經是你們系統裡的工具了。能用現有工具解決,就先用現有工具。」
分散式鎖的陷阱:設了還是不夠安心
小明很快就發現,用 Redis 做分散式鎖有一些邊界情況需要注意:
1) 鎖的過期時間要設多長?
太短:任務還沒跑完,鎖就過期了,另一台機器以為沒人在做,又搶進來 → 還是重複執行。
太長:持鎖的機器如果當機,後面的調度都在等鎖釋放 → 系統卡住。
沒有完美答案,通常的做法是:把鎖的過期時間設成任務預期執行時間的 2~3 倍,配合任務結束後主動釋放鎖。
2) 釋放鎖時要確認是「自己的鎖」
這一點看起來很「龜毛」,但它在防一個很常見、也很致命的情境:A 以為自己還握有鎖,但其實鎖早就過期,被 B 拿走了。
用一個時間線例子就清楚了:
- 10:00:00:Server A 搶到鎖
lock:daily_report = "A-uuid" (TTL=10s) - 10:00:08:A 還在跑(比預期久一點)
- 10:00:10:鎖過期(TTL 到了),Redis 自動刪掉這把鎖
- 10:00:11:Server B 來搶,搶到了
lock:daily_report = "B-uuid" (TTL=10s) - 10:00:12:A 跑完了,順手
DEL lock:daily_report
結果:A 刪掉的是 B 的鎖 → B 以為自己安全,下一台又可能進來重複執行
所以「釋放鎖」不能只做 DEL key,而是要做:
- 只有當 value 還是自己的 uuid 時,才刪除
而且這個「確認 value + 刪除」必須是原子操作(不能先 GET 再 DEL,因為中間可能被別人搶走)。
最常見的做法是用 Redis Lua script(概念是 compare-and-delete):
-- if redis.get(key) == my_uuid then redis.del(key) end
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end3) Redis 本身掛了怎麼辦?
如果你只用一台 Redis,它一掛,分散式鎖也跟著失效。
這個問題可以用 Redis 的 Redlock 演算法(對多台 Redis 節點同時搶鎖,超過半數成功才算搶到)來緩解,但實作複雜度也隨之上升。
小明的結論:分散式鎖能解決大部分的「重複執行」問題,但要用得對,細節不少。
解法二:領導者選舉(Leader Election)
分散式鎖是「每次任務臨時搶」的做法。有另一種思路是:
直接選出一台機器當「老大(Leader)」,只有 Leader 負責排程工作;其他機器都是「跟班(Follower)」,等 Leader 掛了再重新選。
這就是領導者選舉(Leader Election)。
比喻:
就像一班選出一個班長,班長負責統籌協調;其他同學不用各自行動、重複做事。如果班長轉學了,就重新選一個班長。
在系統層面:
Leader 掛了之後:
這種機制的核心是:Leader 要持續向協調服務「打卡(heartbeat)」,表示自己還活著;一旦斷卡,協調服務就把 Leader 的位置釋放出來,讓其他機器競爭。
為什麼需要專門的協調服務?
小明問:「分散式鎖和領導者選舉,用 Redis 不就夠了?」
小傑說:「單純的鎖用 Redis 沒問題。但協調本身涉及的問題更微妙——你怎麼確定 Redis 知道的『Leader 是誰』是正確的?」
這引出了分散式系統裡一個很根本的困難:
多台機器對「同一份狀態」的看法,可能不一致。
舉個例子:
- Leader 機器跑得很慢,heartbeat 超時了
- etcd / Zookeeper 認為 Leader 掛了,選出了新 Leader
- 但舊 Leader 其實還活著,還在執行任務
此時系統裡有兩個 Leader同時在跑,這叫「腦裂(split-brain)」。
Zookeeper 和 etcd 這類協調服務,花了大量工程力在處理這類問題:
- 用 Paxos(Zookeeper 的 ZAB 協議)或 Raft(etcd)保證分散式共識
- 只要超過半數節點(quorum)同意,寫入才算成功
- 保證任一時刻「誰是 Leader」這件事,全叢集的答案是一致的
這些協議的細節超出這篇的範圍,但直覺是:它們用「多數決 + 嚴格的一致性協議」讓「協調這件事本身」是可信的。
Zookeeper vs etcd:選哪個?
兩者都是「分散式協調服務」,核心能力類似:
| Zookeeper | etcd | |
|---|---|---|
| 來自 | Apache / Yahoo | CoreOS(現為 CNCF 項目) |
| 共識協議 | ZAB(類 Paxos) | Raft |
| 介面 | 類檔案系統(ZNode) | 簡單的 key-value |
| 常見搭配 | Kafka、Hadoop 生態系 | Kubernetes、現代雲原生 |
| 學習曲線 | 較高 | 較低 |
現代新系統選型,通常傾向用 etcd(設定簡單、介面直覺、K8s 已內建)。如果你的技術棧已經有 Kafka,那 Zookeeper 就已經在了(Kafka 2.8 以前強依賴 Zookeeper;新版 Kafka 已支援 KRaft 模式,可以不再依賴 Zookeeper)。
小明的原則還是那一句:不要為了引入新工具而引入新工具。
小明的落地版本:先用 Redis 鎖,有需要再評估 etcd
小明他們做了分層評估:
第一層(現在):
用 Redis 分散式鎖解決「定時任務重複執行」的問題。
代價:Redis 故障時鎖失效,任務可能跑多次。
可接受嗎?→ 定時報表或提醒信,偶爾多一次可以接受。
第二層(之後,如果必要):
如果有「絕對只能執行一次」的核心業務邏輯(例如:訂單結帳扣款)
→ 靠應用層的冪等設計(idempotency key)來保證,而不是只靠鎖。
第三層(系統規模更大時):
如果有跨服務的協調需求、需要可靠的 Leader Election
→ 評估引入 etcd。
這個思路提醒了小明一件事:
分散式鎖能降低「重複執行」的機率,但不能消除它。真正的防護是讓任務本身具備冪等性(idempotent)——就算執行兩次,結果也跟執行一次一樣。
比如發信任務,在發信前先記錄「這個 user 這個任務今天已經發過」,每次都先查這份記錄,就算鎖失敗、任務跑了兩次,第二次也會跳過。
架構全貌
🏗️ 本篇結束後的架構 — 排程協調層的角色
小結與預告
這篇小明學到的重點是:
- 多台機器預設是「各自為政」的:你沒明確協調,大家就各做各的,重複執行是正常行為,不是 bug。
- 分散式鎖解決「臨時搶工作」的問題:用 Redis
SET NX EX可以快速落地,但要注意過期時間與正確釋放。 - 領導者選舉解決「誰來統籌調度」的問題:選一台 Leader 統一負責排程,掛掉就重選;Zookeeper 和 etcd 是保證這件事可靠的協調服務。
- 鎖是概率防護,冪等才是根本保障:真正重要的任務,應該設計成「就算跑兩次也安全」,而不是只靠鎖。
到這裡,小明他們的系統已經長得相當複雜:多台後端、多層快取、訊息佇列、WebSocket、協調服務……
有一天,半夜三點,小明的手機響了。告警通知說系統回應異常。他開著筆電,想查是哪個環節出了問題——卻發現自己完全不知道從哪裡開始看。
下一篇,我們來談讓複雜系統「可以被看見」的關鍵——監控、指標與告警(Prometheus / Grafana 的直覺)。