🌐 This page hasn't been translated yet — showing the original Chinese. Translated posts

Apache ZooKeeper:分散式系統的協調核心

· tech

#distributed-systems#zookeeper

📑 目錄

上一篇共識帶到 Zab 是 ZooKeeper 的引擎,但 ZooKeeper 本身值得單獨講一篇。它是分散式系統的協調核心(coordination service):你很少直接用它,卻天天間接依賴它——舊版 Kafka、HBase、Hadoop 的高可用,底下都是它在撐。它最漂亮的地方,是把「分散式協調」這件難事,收斂成一棵像檔案系統的小樹加上幾個簡單原語,讓上層系統不必自己去碰共識那個泥沼。

它不是資料庫,是「協調」的專用工具

先擺正定位:ZooKeeper 不是拿來存資料的,它存的是少量、但極關鍵的中繼資料(誰是 leader、有哪些節點活著、設定是什麼),資料量是 KB 等級。它的價值不在容量,而在高可用 + 強一致 + 一組剛好夠用的協調原語。你把「一群機器怎麼對關鍵狀態達成一致」這個最難的問題外包給它,自己就能專心做業務。

資料模型:一棵像檔案系統的樹

ZooKeeper 對外的樣子出乎意料地簡單:一棵樹,每個節點叫 znode,用路徑定址(像 /services/node-1),每個 znode 能存一小塊資料、也能有子節點。真正的巧思在 znode 的三種型別watch:

資料模型:一棵像檔案系統的樹(znode) / /config[P]data: db_url=… /services[P] /lock[P] node-1[E] node-2[E] lock-0000000001[E+S] lock-0000000002[E+S] clientwatch client session一斷 → [E] 消失 [P] persistent 永久(除非刪除)  [E] ephemeral 綁 session、斷線即消失 [S] sequential 自動加遞增編號  watch 訂閱變更、一次性通知
三種 znode 是所有魔法的來源:ephemeral(綁在 client 的 session 上,client 一斷線就自動消失)讓「偵測誰死了」變成資料模型的自然結果;sequential(ZooKeeper 自動幫名字補上遞增編號)給出全域唯一的排序;兩者合起來(E+S)就是鎖與選主的積木。再配上 watch(訂閱某個 znode,一變更就收到一次性通知),你就有了一套 event-driven、免輪詢的協調工具

這裡最關鍵的設計,是 ephemeral 節點綁定 session:client 和 ZooKeeper 之間維持一個有心跳的 session,一旦心跳停了(client 崩潰、網路斷)、session 逾時,它建立的所有 ephemeral 節點會自動消失。這一招把分散式系統裡最惱人的「怎麼知道某個節點死了」直接內建成資料模型的行為——你不用自己寫心跳偵測,只要看那個 ephemeral 節點還在不在。

架構:一個 ensemble,寫走 leader、讀就地

ZooKeeper 自己也是分散式的——它由一組(奇數台,通常 3 或 5)伺服器組成一個 ensemble,靠 Zab 選出一個 leader,其餘是 follower。讀寫走的是兩條很不對稱的路:

架構:一個 ensemble,寫走 leader、讀就地 client A client B Leader(Zab 選出) Follower Follower 寫 → 一律走 leader Zab 廣播過半 ack 讀 → 任何一台就地回答 寫:線性化、全序(過半才算數) · 讀:可擴展、就地回答(可能稍舊,要最新用 sync()) 另有 observer(不投票)專門擴充讀 throughput
ZooKeeper 的讀寫刻意不對稱:一律繞到 leader,經 Zab 原子廣播、過半 ack 才 commit——所以寫是線性化的、有全序,但也較慢、受過半往返限制。則由任何一台就地回答,因此讀可以水平擴展,代價是可能讀到稍舊的值(要保證最新可先呼叫 sync())。這個「寫強一致、讀可擴展但容許稍舊」的取捨,正是它能扛住海量協調流量的關鍵

一致性上抓三個重點就夠:寫是線性化的(全序、過半才算數)、同一個 client 的操作依送出順序生效、而讀可能稍舊(有界的、不是任意舊)。這組保證對「協調」剛剛好——你要的是「對關鍵狀態達成一致」,不是「每次讀都毫秒級最新」。

怎麼用它:協調原語都是「znode + watch」拼出來的

ZooKeeper 只給你這些積木,不直接給你「鎖」或「leader 選舉」——那些是你用積木出來的「食譜(recipes)」。最經典的一道,是用 ephemeral + sequential 做分散式鎖 / leader 選舉:

食譜:ephemeral-sequential 做鎖 / 選 leader /lock 下,每個 client 各建一個 [E+S] 節點: lock-0000000001最小 = 持有 ✓ lock-0000000002等待中 lock-0000000003等待中 只 watch前一個 持有者掛掉([E] 消失)→ 只喚醒下一個接手 → 沒有驚群(不是全部人搶同一個)
食譜的精髓有兩層:一是編號最小者持有——因為 sequential 給了全域唯一順序,誰先到誰先得,天然公平;二是每個等待者只 watch「比自己小一號」的那一個,而不是全部盯著鎖本身。這樣持有者一釋放(或 client 崩潰、ephemeral 節點消失),只有緊接在後的那一個會被喚醒去接手,避免了所有人同時被叫醒、一起搶鎖的驚群(thundering herd)——這跟 可靠 cron 那篇談的午夜驚群,是同一種要刻意迴避的模式

同一套積木還能拼出其他協調原語:

  • 服務發現 / 成員管理:每個服務在 /services 下建一個 ephemeral 節點,它活著節點就在、它一死節點就消失——/services 底下有哪些子節點,就是「現在誰還活著」的即時名單。
  • 設定管理:把設定存成一個 znode,所有 client 對它下 watch;設定一改,大家立刻收到通知去重載,不用輪詢、也不用重啟。
  • 屏障(barrier)、佇列:一樣是 znode + watch 的變形,協調「大家都到齊了才開始」這類同步點。

它的極限,與為什麼有些系統開始離開它

ZooKeeper 很強,但不是萬靈丹,用之前要知道它的邊界:

  • 只放小中繼資料:它是協調服務,不是資料庫。znode 資料是 KB 級,別拿它存業務資料。
  • 它本身是一套要維運的系統:多一個 ZooKeeper ensemble,就多一套要顧的叢集、一個共享的故障點;而且當上層的 metadata 操作量很大時,它會變成瓶頸。這正是 Kafka 走向 KRaft 的原因——把原本外包給 ZooKeeper 的 metadata 管理,用內建的 Raft 收回自己家,少養一套系統。
  • 語意有陷阱:watch 是一次性的(觸發後要重新註冊,中間的變更可能漏看)、session 逾時與 ephemeral 節點消失的時機,都是實務上容易踩的坑。

另外補一個對照:ZooKeeper 的現代同類是 etcd(用 Raft),它是 Kubernetes 的狀態儲存核心——你可以把「ZooKeeper 之於 Kafka/HBase」類比成「etcd 之於 Kubernetes」:都是把最難的一致性,收斂到一個被驗證過的協調核心裡。

反思

ZooKeeper 是「別自己造輪子」最具體的化身

共識那篇我下過一個結論:別自己造共識輪子。ZooKeeper 就是那句話最實際的樣子——它把 Zab、leader 選舉、線性化寫這些硬核的東西全部封裝好,對你只露出「一棵樹 + 幾個原語」。我特別欣賞這種抽象的收斂:它沒有要你懂 Paxos/Zab,而是把那些理論的成果,包裝成連新手都能用的 createwatchgetChildren。好的基礎設施就該是這樣——把最難的複雜度吃進肚子裡,對外只吐出簡單。每次我想「這個分散式協調我自己寫一下就好」,都會想起 ZooKeeper 背後那些論文和踩過的坑,然後乖乖去用現成的。

「ephemeral + watch」把「偵測死亡」變成資料模型的副作用

ZooKeeper 設計裡我最拍案叫絕的,是 ephemeral 節點。分散式系統最惱人的問題之一,是「怎麼可靠地知道某個節點死了」——心跳、逾時、誤判,一堆坑。而 ZooKeeper 的解法是:把「活著」這件事,綁進資料模型——你活著,你的 ephemeral 節點就在;你一斷線,它自動消失。於是「偵測死亡」不再是一段你要自己寫的邏輯,而是查一下那個節點還在不在就好。這種「把一個難問題,轉化成另一個系統天生就會處理的形式」的思路,是我覺得最高級的一種工程設計——不是把問題解掉,而是讓它根本不需要被解

協調核心的真正價值:把一致性收斂到一個地方

用久了 ZooKeeper(以及它的表親 etcd),我體會到一個架構層面的智慧:與其讓系統裡每個元件都自己處理一致性,不如把「需要強一致的那一小撮狀態」,全部收斂到一個專門的協調核心。這跟我在共識那篇說的「只讓最關鍵的狀態走共識」是同一件事的放大版——ZooKeeper 就是那個「唯一需要昂貴一致性」的地方,其他所有服務都變成它的 client,自己維持無狀態、可水平擴展。把最難的部分集中管理、其餘保持簡單,這不只是 ZooKeeper 的設計哲學,幾乎是所有耐得住規模的分散式架構共通的骨架。