本文參考自:《ZooKeeper: Distributed process coordination》
Zookeeper 簡(jiǎn)介
Zookeeper 最初是由 Yahoo 公司開發(fā)的,主要用于支持 robust distributed system,后期貢獻(xiàn)給了 Apache 基金會(huì)。
官方定義:Apache ZooKeeper, a distributed coordination service for distributed systems
技術(shù)層面上:ZooKeeper 也可以理解為 distributed file system
不像單體應(yīng)用,在分布式系統(tǒng)中各個(gè)節(jié)點(diǎn)間的 Coordinating 是困難的。zk 正是為了解決分布式系統(tǒng)的協(xié)調(diào)工作(coordinating task),它通過提供通用的功能,讓 application developers 能專注于自身的業(yè)務(wù)功能,而不用過多的關(guān)注分布式系統(tǒng)的協(xié)調(diào)。
zk 從文件系統(tǒng) API 受到啟發(fā),并通過暴露一些簡(jiǎn)單的 client API,來讓開發(fā)人員實(shí)現(xiàn)通用的協(xié)調(diào)任務(wù),例如:主節(jié)點(diǎn)選舉,管理組內(nèi)成員關(guān)系,管理員數(shù)據(jù)等。
Zookeeper 名字的來源也非常的有趣且到位。分布式系統(tǒng)可以比做 zoo,里面有各種各樣不同的應(yīng)用(animals),而 zookeeper 的目的就是保證各個(gè)應(yīng)用間的可控和有序。
需要注意的是,zk 并不為我們直接實(shí)現(xiàn) leader election, distributed lock 等功能,我們需要通過 zk 暴露的簡(jiǎn)單的 api 來自己實(shí)現(xiàn)。好在現(xiàn)在已經(jīng)有一些開源庫實(shí)現(xiàn)了這些功能,如 Curator
What ZooKeeper Doesn’t Do
zk 不適合作為海量數(shù)據(jù)存儲(chǔ)中心。對(duì)于需要存儲(chǔ)大量數(shù)據(jù)的場(chǎng)景,建議使用 db 或者分布式文件系統(tǒng)。
Building Distributed Systems with ZooKeeper
分布式系統(tǒng)的概念這里不提了。
分布式系統(tǒng)中的應(yīng)用進(jìn)程可以通過兩種方式進(jìn)行通信:
- 直接通過網(wǎng)絡(luò)交換信息
- 讀寫某些共享存儲(chǔ)
zk 通過共享存儲(chǔ)模型來實(shí)現(xiàn)各個(gè)應(yīng)用間的協(xié)作,當(dāng)然,對(duì)于共享存儲(chǔ)本身,需要進(jìn)程和存儲(chǔ)間進(jìn)行網(wǎng)絡(luò)交換。
Znode
zk 操作 & 維護(hù)節(jié)點(diǎn)樹,這些節(jié)點(diǎn)被稱為 znode,類似于文件系統(tǒng)的層級(jí)樹狀結(jié)構(gòu)。

API for Znode
zk 暴露了一些 api 來操作 znode:
- create /path data: Creates a znode named with /path and containing data
- delete /path: Deletes the znode /path
- exists /path: Checks whether /path exists
- setData /path data: Sets the data of znode /path to data
- getData /path: Returns the data in /path
- getChildren /path: Returns the list of children under /path
Multiop
Multiop 可以原子性地執(zhí)行多個(gè) ZooKeeper 的操作,即在 multiop 代碼塊中的所有操作要不全部成功,要不全部失敗。
Different Modes for Znodes
Persistent and ephemeral znodes
從節(jié)點(diǎn)持久度上,zk 的 node 可分為兩種:
- Persistent znodes 持久節(jié)點(diǎn)
- Ephemeral znodes 臨時(shí)節(jié)點(diǎn):目前臨時(shí)節(jié)點(diǎn)不能創(chuàng)建子節(jié)點(diǎn)
delete node:
- 持久節(jié)點(diǎn)只能顯示調(diào)用 delete 方法來刪除
- 臨時(shí)節(jié)點(diǎn)可通過:
- 當(dāng)創(chuàng)建該節(jié)點(diǎn)的 client 主動(dòng)關(guān)閉與 zk server 的連接,或者崩潰,或者與 zk server 的連接超時(shí),就被 zk server 自動(dòng)刪除
- zk client 主動(dòng)刪除
Sequential
znode 還可被設(shè)置為 sequential node(有序節(jié)點(diǎn))。
sequential znode 被 zk 分配唯一的,單調(diào)自增的 int 值,通過這個(gè)自增值,也可以看出節(jié)點(diǎn)的創(chuàng)建順序。
組合起來后,zk 共有四種 znode:persistent, ephemeral, persistent_sequential, and ephemeral_sequential.
Versions
每個(gè) znode 都有一個(gè)版本號(hào),它隨著每次的 change 而自增。但是 znode 節(jié)點(diǎn)被 delete & recreated 后,其版本號(hào)將會(huì)被 reset 回 0。

setData, delete 是有條件執(zhí)行 的 api。client 執(zhí)行 setData, delete 操作時(shí),會(huì)把自身所有的 version 傳過來,如果這個(gè) version 和 zk server 上的 version 相等,才能執(zhí)行該操作。
可以理解成樂觀鎖
ZooKeeper Quorums

我們和 zk 交互時(shí),一般都是通過 zk 提供的 client 來和 zk server 通信。
zk 可以運(yùn)行在兩種模式下:
- standalone: 單機(jī)
- quorum: 集群
quorum 可以翻譯為:仲裁/法定人數(shù)
quorum mode 下的 zk 又叫做 ZooKeeper ensemble,server 內(nèi)部進(jìn)行狀態(tài)數(shù)據(jù)的同步。
集群模式下,選擇一個(gè)服務(wù)器做為 leader,其他服務(wù)器追隨 leader,被稱為 follower。
leader:
- leader 處理所有的寫請(qǐng)求(create, delete, setData)
follower:
- followers 只能同步 leader 發(fā)出的寫操作更新,不能直接執(zhí)行 client 的寫請(qǐng)求
- followers 只能處理 client 發(fā)起的讀請(qǐng)求(exists, getData, getChildren)
除了 leader, follower 外還存在第三類服務(wù)器,稱為觀察者(observer)。 觀察者不會(huì)參與決策哪些請(qǐng)求可被接受的過程,只是觀察決策的結(jié)果,觀察者的設(shè)計(jì)只是為了系統(tǒng)的可擴(kuò)展性
Requests, Transactions, and Identifiers
leader 將每一個(gè)寫請(qǐng)求轉(zhuǎn)換為一個(gè)事務(wù)(transaction)。只有改變 ZooKeeper 狀態(tài)的操作才會(huì)產(chǎn)生事務(wù),對(duì)于讀操作并不會(huì)產(chǎn)生任何事務(wù)。事務(wù)包含:
- 要修改的路徑
- 修改的值
- 新的版本號(hào)
- zid
事務(wù)的特點(diǎn):
- 一個(gè)事務(wù)為一個(gè)單位,也就是說所有的變更處理需要以原子方式執(zhí)行。
- 事務(wù)還具有冪等性,我們可以對(duì)同一個(gè)事務(wù)執(zhí)行兩次,得到的結(jié)果是一樣的,甚至還可以對(duì)多個(gè)事務(wù)執(zhí)行多次,同樣也會(huì)得到一樣的結(jié)果,前提是我們確保多個(gè)事務(wù)的執(zhí)行順序每次都是一樣的。事務(wù)的冪等性可以讓我們?cè)谶M(jìn)行恢復(fù)處理時(shí)更加簡(jiǎn)單。
當(dāng) leader 產(chǎn)生一個(gè) transaction,就會(huì)為該 transaction 分配一個(gè) ZooKeeper transaction ID(zxid)。
zxid 為一個(gè)64位 long 整數(shù),分為兩部分,每個(gè)部分32位:
- 時(shí)間戳(epoch)
- 計(jì)數(shù)器(counter)
epoch 代表了管理權(quán)隨時(shí)間的變化情況,epoch 的值在每次進(jìn)行 leader election 時(shí)增加,一個(gè) epoch 表示某個(gè)服務(wù)器行使管理權(quán)的這段時(shí)間。在一個(gè) epoch 內(nèi),leader 根據(jù) counter 定義每一個(gè) transaction。zxid 可以很容易地與 transaction 被創(chuàng)建 epoch 相關(guān)聯(lián)。

綜合來看,zxid 有兩個(gè)主要作用:
- 通過 zxid 對(duì)事務(wù)標(biāo)識(shí),就可以按照 leader 指定的順序在各個(gè)服務(wù)器中順序執(zhí)行
- 服務(wù)器之間在進(jìn)行新的 leader election 時(shí)交換 zxid 信息,哪個(gè)無故障服務(wù)器接收了更多的事務(wù),就選舉那個(gè)作為新的 leader
Leader Elections
zk 可通過臨時(shí)節(jié)點(diǎn)為別的 master-slave 架構(gòu)的分布式集群提供 leader election,如 Kafka,但是 zk cluster 如何進(jìn)行自身集群 leader 的選舉呢?
每個(gè)服務(wù)器啟動(dòng)后進(jìn)入 LOOKING 狀態(tài),開始選舉一個(gè)新的 leader 或查找已經(jīng)存在的 leader,如果 leader 已經(jīng)存在,其他服務(wù)器會(huì)通知這個(gè)新啟動(dòng)的服務(wù)器,告知哪個(gè)服務(wù)器是 leader ,與此同時(shí),新的服務(wù)器會(huì)與 leader 建立連接,以確保自己的狀態(tài)與 leader 一致。如果集群中所有的服務(wù)器均處于 LOOKING 狀態(tài),這些服務(wù)器之間就會(huì)進(jìn)行通信來選舉一個(gè) leader。在選舉過程中勝出的服務(wù)器將進(jìn)入 LEADING 狀態(tài),而集群中其他服務(wù)器將進(jìn)入 FOLLOWING 狀態(tài)。
服務(wù)間進(jìn)行 leader election 時(shí)會(huì)發(fā)送 leader election notifications,當(dāng)一個(gè)服務(wù)器進(jìn)入 LOOKING 狀態(tài),就會(huì)發(fā)送向集群中每個(gè)服務(wù)器發(fā)送一個(gè) notification,notification 包括該服務(wù)器的投票(vote)信息,投票中包含服務(wù)器標(biāo)識(shí)符(sid)和最近執(zhí)行的事務(wù)的 zxid 信息,比如,一個(gè)服務(wù)器所發(fā)送的投票信息為(1,5),表示該服務(wù)器的sid為1,最近執(zhí)行的事務(wù)的 zxid 為 5。擁有最近一次的 zxid的服務(wù)器將贏得選舉。

上圖是 leader election 的理想情況,考慮下圖:

在從服務(wù)器 s1 向服務(wù)器 s2 傳送消息時(shí)發(fā)生了網(wǎng)絡(luò)故障導(dǎo)致長(zhǎng)時(shí)間延遲,與此同時(shí),服務(wù)器 s2 選擇了服務(wù)器 s3 作為 leader ,最終,服務(wù)器 s1 和服務(wù)器 s3 組成了仲裁數(shù)量(quorum),并將忽略服務(wù)器 s2。
如果讓 s2 在進(jìn)行 leader 選舉時(shí)多等待一會(huì),它就能做出正確的判斷。如下圖:

但是很難確定服務(wù)器需要等待多長(zhǎng)時(shí)間,在現(xiàn)在的實(shí)現(xiàn)中,默認(rèn)的 leader 選舉使用固定值 200ms,但與恢復(fù)時(shí)間相比還不夠長(zhǎng)。如果由于各種原因?qū)е乱惠嗊x舉無法選出 leader,就需要進(jìn)行另一輪的 leader 選舉。
一個(gè) zk 服務(wù)器必須被至少 quorum 數(shù)量的服務(wù)器所認(rèn)可,才能被選為 leader,以避免 split brain(這種情況將導(dǎo)致整個(gè)系統(tǒng)狀態(tài)的不一致性,最終客戶端也將根據(jù)其連接的服務(wù)器而獲得不同的結(jié)果)。如果在 leader election 時(shí)無法達(dá)到仲裁法定數(shù)量,ZooKeeper cluster 也將無法正常服務(wù)。
Zab: Broadcasting State Updates
在接收到一個(gè) client 的寫請(qǐng)求后,follower 會(huì)將請(qǐng)求轉(zhuǎn)發(fā)給 leader,leader 將探索性地執(zhí)行該請(qǐng)求,并將執(zhí)行結(jié)果以事務(wù)的方式對(duì)寫請(qǐng)求進(jìn)行廣播。
zk 提供了一種協(xié)議 ZooKeeper Atomic Broadcast protocol 來進(jìn)行 transaction commit 操作,ZAB 協(xié)議和 2PC 有相似之處:
- leader 向所有 followers 發(fā)送一個(gè) PROPOSAL消息
- 當(dāng)一個(gè) follower 接收到 PROPOSAL 后,會(huì)響應(yīng) leader 一個(gè) ACK 消息,通知 leader 其已接受該提案(proposal)。
- 當(dāng)收到仲裁數(shù)量的服務(wù)器發(fā)送的確認(rèn)消息后(該仲裁數(shù)包括 leader 自己),leader 就會(huì)發(fā)送消息通知 followers 進(jìn)行 COMMIT 操作。
regular message pattern to commit proposals.jpg
ZAB 保證
- 事務(wù)在服務(wù)器之間的傳送順序的一致
- 服務(wù)器不會(huì)跳過任何事務(wù)
在仲裁模式下,記錄已接收的提案消息非常關(guān)鍵,這樣可以確保所有的服務(wù)器最終提交了被某個(gè)或多個(gè)服務(wù)已經(jīng)提交完成的 transaction。
Zab 協(xié)議提供了以下保障:
- 一個(gè)被選舉的 leader 確保在提交完所有之前的 epoch 內(nèi)需要提交的 transactions,之后才開始廣播新的 transaction。
- 在任何時(shí)間點(diǎn),都不會(huì)出現(xiàn)兩個(gè)被仲裁支持的 leader 。
Observers
除了 leader & follower 外,zk 中還存在一種 server:observer。
| follower | observer | |
|---|---|---|
| leader election | true | false |
| forward write request | true | true |
| leader message | leader分階段給follower發(fā)兩條: proposal+commit(zxid) | 在transaction committed后,leader一次性給observer發(fā)一條 inform(proposal + commit zxid) |
followers 和 observers,這兩種服務(wù)器也都被稱為學(xué)習(xí)者。
引入 observer 的一個(gè)主要原因是提高讀請(qǐng)求的可擴(kuò)展性。follower 是可以提高讀效率,但是它是需要參與寫操作的投票的,越多的 followers 需要越多的 quorum 來決策,會(huì)導(dǎo)致越慢的寫性能。通過加入多個(gè) observers,我們可以在不犧牲寫操作的吞吐率的前提下服務(wù)更多的讀操作。
當(dāng)然,增加 observer 也不是完全沒有開銷的。每一個(gè)新加入的 observer 將對(duì)應(yīng)于每一個(gè)已提交事務(wù)點(diǎn)引入的一條額外消息。然而,這個(gè)開銷相對(duì)于增加參與投票的 followers 來說小很多。
配置 ZooKeeper 集群使用觀察者,需要在觀察者服務(wù)器的配置文件中添加以下行:peerType=observer
同時(shí),還需要在所有服務(wù)器的配置文件中添加該服務(wù)器的:server.1:localhost:2181:3181:observer
observer 的設(shè)計(jì)很有創(chuàng)意,保證寫性能的情況下提高讀性能
Quorum 值的配置
quorum:
- zk 集群正常運(yùn)行所需的最少 servers 數(shù)。在這里理解為參與投票的最少可用法定人數(shù)。
- server 在返回 client success 前,最少寫成功的 servers 數(shù),同步 & 異步的結(jié)合。這種意義上來說,和 kafka 中的 acks 是一個(gè)概念。
quorum 的選擇很關(guān)鍵,它需要保證在 cluster 正常的情況下(至少 quorum 正常),寫入成功的數(shù)據(jù)不會(huì)丟失。
建議 quorum >= n/2+1:
- 保證至少有 quorum 個(gè) servers 運(yùn)行:防止腦裂 split-brain
- 保證至少有 quorum 個(gè) servers 保存成功數(shù)據(jù):當(dāng)少于 n/2+1 個(gè) servers 宕機(jī)時(shí),至少還有一臺(tái)機(jī)器存有最新數(shù)據(jù)
集群模式下,一個(gè)重要的問題就是:數(shù)據(jù)一致性問題。同步寫所有 replicas 的話,性能很低,延遲很大,只寫一個(gè) replica 的話,如果在數(shù)據(jù)同步到別的 replicas 之前,該節(jié)點(diǎn)宕機(jī),存在數(shù)據(jù)丟失的風(fēng)險(xiǎn)。
Cluster 中 server 的配置
在 quorum 模式下,zk cluster 會(huì)有多個(gè) servers,每個(gè) server 都有自己的配置文件,該文件中需要設(shè)置 cluster 中其他 server 的信息:
server.1=127.0.0.1:2222:2223
server.2=127.0.0.1:3333:3334
server.3=127.0.0.1:4444:4445
第二個(gè) port 用于 quorum communication,第三個(gè) port 用于 leader election。
客戶端隨機(jī)連接到 zk servers 中的一臺(tái)服務(wù)器。這樣可以針對(duì) ZooKeeper 做一個(gè)簡(jiǎn)單的負(fù)載均衡。不過,客戶端無法指定優(yōu)先選擇的服務(wù)器來進(jìn)行連接。如果有優(yōu)先級(jí)的業(yè)務(wù)需求,可以在給 client 配置 server list 時(shí),只配置優(yōu)先的地址。當(dāng)然了,如果這個(gè)優(yōu)先地址的 server 宕掉,client 也能正常的連接到別的未配置在 client 本地 property 中的地址。
Cluster Configuration
擔(dān)任 leader 的 zk server 需要做很多工作,它需要與所有 followers 進(jìn)行通信并會(huì)執(zhí)行所有的變更操作,也就意味著 leader 的負(fù)載會(huì)比 followers 的負(fù)載高,如果 leader 過載,整個(gè)系統(tǒng)可能都會(huì)受到影響??梢耘渲?zookeeper.leaderServes=no,使 leader 除去服務(wù) client 連接的負(fù)擔(dān),使 leader 將所有資源用于處理 followers 發(fā)送給它的變更請(qǐng)求,這樣可以提高系統(tǒng)寫操作的吞吐能力。但如果 leader 不處理任何與其直連的客戶端,followers 就需要承擔(dān)有更多的客戶端,當(dāng)集群中 servers 比較少的時(shí)候,followers 壓力也會(huì)比較大,這也是需要權(quán)衡的。默認(rèn)情況下,leaderServes的值為“yes”。
Sessions
會(huì)話(Session)是 zk 的一個(gè)重要的抽象。在對(duì) zks server 執(zhí)行任何操作前,zk client 需要先和 server 建立 session。當(dāng) session 由于任何原因關(guān)閉時(shí),ephemeral nodes 都將被 zk 刪除。
保證請(qǐng)求有序、臨時(shí) znode 節(jié)點(diǎn)刪除、watch 都與 session 密切相關(guān)。
在 Java 中,當(dāng) new Zookeeper() 時(shí),就創(chuàng)建了一個(gè) session。zk client 通過 TCP 協(xié)議和 zk server 通信。zk client 連接到 server 后,后臺(tái)就會(huì)有一個(gè)線程來維護(hù)這個(gè) session。正常情況下不需要開發(fā)人員維護(hù)與 zk 的連接,zk client 會(huì)監(jiān)控與 zk server 之間的連接,客戶端庫不僅告訴我們連接發(fā)生問題,還會(huì)主動(dòng)嘗試重新建立通信。一般客戶端開發(fā)庫會(huì)很快重建會(huì)話,以便最小化對(duì)應(yīng)用的影響。所以不要關(guān)閉會(huì)話后再啟動(dòng)一個(gè)新的會(huì)話,這樣會(huì)增加系統(tǒng)負(fù)載,并導(dǎo)致更長(zhǎng)時(shí)間的中斷。
session 提供了 order guarantees,這里需要注意:
- one client 使用 single session 時(shí),可以保證消息的 order,即 FIFO。
- one client 使用 multiple sessions 時(shí),不保證 cross sessions 間的 order。
States and the Lifetime of a Session
Session 的生命周期是指 session 從創(chuàng)建到結(jié)束(不管是優(yōu)雅的關(guān)閉/timeout 導(dǎo)致的過期)的過程。Session 狀態(tài)分為:CONNECTING, CONNECTED, CLOSED, and NOT_CONNECTED.

當(dāng) client 丟失和當(dāng)前 zk server 的連接或者無法接到 server 的響應(yīng)時(shí),session 會(huì)切換回 CONNECTING 狀態(tài),并嘗試重連另外一個(gè) zk server。
- 如果 client 能盡快連到一個(gè)新的 server 或者重連到舊的 server,并且 server 確認(rèn) session 有效后,session 切換回 CONNECTED 狀態(tài)。
- 如果 client 較長(zhǎng)時(shí)間未能聯(lián)系上 server,server 會(huì)認(rèn)為 session expired
- 如果 client 一直聯(lián)系不上 server,client 會(huì)一直處于 connecting 狀態(tài),直到 client 顯示關(guān)閉 session
- 如果最終 client 聯(lián)系上了 server,server 會(huì)告知 client session expired & closed
zk 中只有 server 才能判定 session expired,但是 client 可以選擇主動(dòng) close session
Session timeout
所以在創(chuàng)建 session 時(shí),要合理的設(shè)置 timeout,這個(gè)參數(shù)設(shè)置了 zk server 允許 session 被聲明為 expired 前存在的時(shí)間。
- server: 如果經(jīng)過 timeout 時(shí)間后,server 還未收到這個(gè) session 的任何消息,server 就會(huì)認(rèn)為該 session expired。
- client: 在 1/3 of timeout 時(shí),未從 server 收到任何消息,它會(huì)向該 server 發(fā)送 heartbeat。在 2/3 of timeout 后,client 開始從 server list 中尋找滿足要求(這個(gè) server 的狀態(tài)要最少與 client 最后連接的 server 的狀態(tài)一樣)的 zk server,此時(shí)留給它 1/3 of timeout 去尋找。
Client reconnecting
client 不能連接到自身已發(fā)現(xiàn)的更新(zxid)而 server 未發(fā)現(xiàn)更新的 server。因此,如果一個(gè) client 在位置 i 觀察到一個(gè)更新,它就不能連接到只觀察到 i'<i 的 server 上。

ZooKeeper 通過 zxid 確保 change 的有序性。
Servers and Sessions
在獨(dú)立模式下,單個(gè) zk server 會(huì)跟蹤所有的 session,而在仲裁模式下則由 leader 來跟蹤和維護(hù),followers 僅僅是把 client 連接的 session 轉(zhuǎn)發(fā)給 leader。為了保證 session 的存活,client 需要定期發(fā)送 heartbeat 到 zk server。server 收到 heartbeat 后,通過更新 session 的 expiration time 來觸發(fā) session 活躍。
在仲裁模式下,leader 每半個(gè) tick 發(fā)送一個(gè) PING 消息給它的 followers,followers 返回自從最新一次 PING 消息之后的一個(gè) session 列表。
Watches and Notifications
對(duì)于 znode 來說,它可能被創(chuàng)建、添加 data、修改 data、增加子節(jié)點(diǎn)、刪除。client 可能需要及時(shí)獲取到這些改變:
- pull mode: client 對(duì)改變敏感,為了保證及時(shí)獲取到更新,需要非常頻繁的 poll,但是 changes 畢竟不高頻,poll 會(huì)浪費(fèi)大量的資源
- notification mode / push mode: client 向 zk 注冊(cè)需要接受通知的 znode,通過對(duì) znode 設(shè)置 watch 來實(shí)現(xiàn)
一個(gè) watch(監(jiān)視點(diǎn))由以下元素組成:
- znode
- event
具體可分為:
- data watches
- create a znode event
- delete a znode event
- set the data of a znode event
- child watches
- changes to the children of a znode event
NodeCreated: 通過 exists 調(diào)用設(shè)置一個(gè)監(jiān)視點(diǎn)。
NodeDeleted: 通過 exists 或 getData 調(diào)用設(shè)置監(jiān)視點(diǎn)。
NodeDataChanged: 通過 exists 或 getData 調(diào)用設(shè)置監(jiān)視點(diǎn)。
NodeChildrenChanged: 通過 getChildren 調(diào)用設(shè)置監(jiān)視點(diǎn),這種 watch 只有在 znode 子節(jié)點(diǎn)創(chuàng)建或刪除時(shí)才被觸發(fā)。
當(dāng)一個(gè) watch 被一個(gè) event 觸發(fā)時(shí),就會(huì)產(chǎn)生一個(gè) notification??蛻舳藭?huì)以 callback function 的形式收到 notification,并根據(jù)自己的業(yè)務(wù)邏輯實(shí)現(xiàn)一個(gè) watcher 來處理收到的 notification。
One-Time Triggers
一個(gè) watch(監(jiān)視點(diǎn)) 是一個(gè) one-time trigger,這里的 one-time 是指 watch 最多只被觸發(fā)一次,發(fā)送一個(gè) notification。
客戶端設(shè)置的每個(gè) watch 與 session 關(guān)聯(lián)
- 如果 session expired / close,等待中的 watch 將會(huì)被刪除
- 如果 session 沒有 expired / close,而是 connecting,watch 就可以跨越不同 server 的連接而保持。例如,當(dāng)一個(gè) client 與一個(gè) ZooKeeper server 的連接斷開后連接到 zk cluster 中的另一個(gè) server,client 會(huì)發(fā)送未觸發(fā)的 watch list,在新 server 注冊(cè) watch 時(shí),server 將檢查被 watch 的 znode 節(jié)點(diǎn)在之前注冊(cè) watch 之后是否已經(jīng)變化,如果已發(fā)生變化,一個(gè) notification 就會(huì)被發(fā)送給 client,否則在新的 server 上注冊(cè) watch。
Usage scenario
watch 的使用場(chǎng)景非常廣泛,下面僅舉例說明:
- master-slaves 架構(gòu)中的 master election
- 分布式應(yīng)用服務(wù)中 server-a 和 server-b 之間的調(diào)用,server-b 多機(jī)部署,server-b 的 address infos 可以存在 zk 中,sever-a 從 zk 拿到 server-b 的信息 & cache 到本地,并且 set watch,當(dāng) server-b list 變化時(shí),實(shí)時(shí)更新 cache。這種場(chǎng)景即是把 zk 當(dāng)成注冊(cè)中心。
The Herd Effect and the Scalability of Watches
當(dāng) change 發(fā)生時(shí),ZooKeeper 會(huì)觸發(fā)這個(gè) znode 節(jié)點(diǎn)的變化導(dǎo)致的所有 watch。如果有 1000 個(gè)客戶端通過 exists 操作監(jiān)視這個(gè) znode 節(jié)點(diǎn),那么當(dāng) znode 節(jié)點(diǎn)創(chuàng)建后就會(huì)發(fā)送 1000 個(gè) notification,因而被監(jiān)視的 znode 節(jié)點(diǎn)的一個(gè)變化會(huì)產(chǎn)生一個(gè)尖峰的通知。這會(huì)對(duì)性能帶來影響。
假設(shè)有 n 個(gè)客戶端爭(zhēng)相獲取一個(gè)鎖(例如 /lock)。為了獲取鎖,一個(gè)進(jìn)程試著創(chuàng)建 /lock 節(jié)點(diǎn),如果 znode 節(jié)點(diǎn)存在了,客戶端就會(huì)監(jiān)視這個(gè) znode 節(jié)點(diǎn)的刪除事件。當(dāng) /lock 被刪除時(shí),所有監(jiān)視/lock 節(jié)點(diǎn)的客戶端收到通知。另一個(gè)不同的方法,讓客戶端創(chuàng)建一個(gè)有序的節(jié)點(diǎn) /lock/lock-,回憶之前討論的有序節(jié)點(diǎn),ZooKeeper 在這個(gè) znode 節(jié)點(diǎn)上自動(dòng)添加一個(gè)序列號(hào),成為 /lock/lock-xxx,其中 xxx 為序列號(hào)。我們可以使用這個(gè)序列號(hào)來確定哪個(gè)客戶端獲得鎖,通過判斷 /lock 下的所有創(chuàng)建的子節(jié)點(diǎn)的最小序列號(hào)。在該方案中,客戶端通過 /getChildren方法來獲取所有/lock 下的子節(jié)點(diǎn),并判斷自己創(chuàng)建的節(jié)點(diǎn)是否是最小的序列。如果客戶端創(chuàng)建的節(jié)點(diǎn)不是最小序列號(hào),就根據(jù)序列號(hào)確定序列,并在前一個(gè)節(jié)點(diǎn)上設(shè)置監(jiān)視點(diǎn)。
Servers and Watches
監(jiān)視點(diǎn)只會(huì)保存在 server 的內(nèi)存中,而不會(huì)持久化到硬盤。當(dāng) client 與 zk server 的連接斷開時(shí),它的所有監(jiān)視點(diǎn)會(huì)從 server 的內(nèi)存中清除。因?yàn)?client 庫也會(huì)維護(hù)一份監(jiān)視點(diǎn)的數(shù)據(jù),在重連之后監(jiān)視點(diǎn)數(shù)據(jù)會(huì)再次被同步到 zk server。
需要注意,watch 是一次性的(one-shot operation),為了接受多次通知,client 需要在每次收到 notification 后,注冊(cè)一個(gè)新的 watch。收到 notification <--> 注冊(cè)新 watch 中間,znode 可能又發(fā)生了改變。
To observe this change, c1 has to actually read the state of /tasks, which it does when setting the watch because we set watches with operations that read the state of ZooKeeper. Consequently, c1 does not miss any changes.
set watch 前,會(huì)自動(dòng)再 read 一次,所以不會(huì)丟消息
更多關(guān)于 watch 的使用案例,可參考 # Master-Worker Application
Ordering Guarantees
Order of Writes
zk cluster 保證使用相同的順序執(zhí)行狀態(tài)的更新。例如,如果一個(gè) ZooKeeper server 執(zhí)行了先建立一個(gè) /z 節(jié)點(diǎn)的狀態(tài)變化之后再刪除 /z 節(jié)點(diǎn)的狀態(tài)變化這個(gè)順序的操作,所有的在 zk cluster 中的 server 均需以相同的順序執(zhí)行這些變化,注意:這里的相同順序并不強(qiáng)制是同步實(shí)時(shí)。
Order of Reads
ZooKeeper client 可能是在不同時(shí)間觀察到了更新,但是總會(huì)觀察到相同的更新順序,即使它們連接到不同的 server 上,這是一種最終一致性的實(shí)現(xiàn)。
考慮下圖情況:

c2 未能成功讀到 /z=B 的數(shù)據(jù)是因?yàn)檫@個(gè) notification 不是由它連接的 server 發(fā)送的,而是 c1 通過 hidden channel(非 zk)通知的,這時(shí) c2 getData 時(shí),它連接的 server 暫時(shí)還未同步到 /z=B,導(dǎo)致獲取失敗。
Zookeeper 中針對(duì)這種隱蔽通道(hidden channel)可能獲取不到最新數(shù)據(jù)的情況提供了方法:sync。
client 向 server-1 發(fā)起 getData 前先調(diào)用 sync。當(dāng) server-1 收到 sync 調(diào)用時(shí),會(huì)刷新 zk leader server 與 server-1 之間的通道,刷新的意思就是說在調(diào)用 getData 的返回?cái)?shù)據(jù)的時(shí)候,server-1 確保返回所有 client 調(diào)用 sync 方法時(shí)所有可能的變化情況。
突然想起來 kafka 中的實(shí)現(xiàn),kafka 其實(shí)是實(shí)現(xiàn)了強(qiáng)一致性的,雖然 replication 是異步的,但是 consumers 消費(fèi)時(shí)只能獲取到寫入所有 replicas 的 msg(kafka consumer 只從 leader 讀取 msg),不像 zk client 連接任意 server 讀取數(shù)據(jù)。
Locks with ZooKeeper / Mutual exclusion lock / Distributed Lock
我們可以通過 zk 提供的 api 來實(shí)現(xiàn)一個(gè) distributed lock。
假設(shè)有一個(gè)應(yīng)用由 n 個(gè)進(jìn)程組成,這些進(jìn)程嘗試獲取一個(gè)鎖。為了獲得一個(gè)鎖,每個(gè)進(jìn)程 p 嘗試創(chuàng)建 znode,名為/lock。如果進(jìn)程 p 成功創(chuàng)建了 znode,就表示它獲得了鎖并可以繼續(xù)執(zhí)行其臨界區(qū)域的代碼。一個(gè)潛在的問題是進(jìn)程 p 可能崩潰,導(dǎo)致這個(gè)鎖永遠(yuǎn)無法釋放。在這種情況下,沒有任何其他進(jìn)程可以再次獲得這個(gè)鎖,整個(gè)系統(tǒng)可能因死鎖而失靈。為了避免這種情況,可以在創(chuàng)建這個(gè)節(jié)點(diǎn)時(shí)指定 /lock 為臨時(shí)節(jié)點(diǎn)。
其他進(jìn)程因 znode 存在而創(chuàng)建 /lock 失敗。因此,進(jìn)程監(jiān)聽 /lock 的變化,并在檢測(cè)到 /lock 刪除時(shí)再次嘗試創(chuàng)建節(jié)點(diǎn)來獲得鎖,如果其他進(jìn)程又已經(jīng)創(chuàng)建了,就繼續(xù)監(jiān)聽節(jié)點(diǎn)。
/lock 被刪除的原因有多種,即前面提到的 ephemeral node 被 zk 刪除的原因
- 當(dāng)創(chuàng)建該節(jié)點(diǎn)的 client 崩潰,或者與 zk 的連接超時(shí)
- 當(dāng)創(chuàng)建該節(jié)點(diǎn)的 client 主動(dòng)關(guān)閉 session
爭(zhēng)奪分布式鎖其實(shí)和爭(zhēng)奪 master 節(jié)點(diǎn)非常類似。
Master-Worker Application design by Zookeeper

分布式系統(tǒng)中非常常見的一種架構(gòu)是 master/worker (master/slave),包括 Kafka, Es 等都采用該架構(gòu)。master 負(fù)責(zé)集群的調(diào)度、從節(jié)點(diǎn)狀態(tài)跟蹤、任務(wù)的分配等(部分應(yīng)用中 master 也執(zhí)行任務(wù)),worker 執(zhí)行任務(wù)。
master-worker 架構(gòu)中需要處理的問題包括:
- Master crashes -> master election / leader election
- Worker crashes -> worker crash detection
- Communication failures
- maintaining application metadata: 通過第三方 zk 維護(hù)
Leader Election
應(yīng)用 client 通過搶占式的創(chuàng)建臨時(shí)的 /master 節(jié)點(diǎn)來推選自己為主節(jié)點(diǎn)(我們稱為“主節(jié)點(diǎn)競(jìng)選”)。如果 /master 已存在,client 在 /master 節(jié)點(diǎn)上設(shè)置一個(gè)監(jiān)視點(diǎn)。
可以在 /master 中添加主機(jī)信息,以便 slaves 與 master 通信。此時(shí),master-slaves 集群正常啟動(dòng)。
當(dāng) master crash 時(shí),/master znode 自動(dòng)被刪除,同時(shí) zk server 通知所有 slave clients。一旦 slave clients 收到通知,它們就可以開始進(jìn)行主節(jié)點(diǎn)選舉,來接管之前 master 的工作。
master election 需要關(guān)注兩個(gè)問題:
- recoverability of the master state
- false suspicion master crash
recoverability of the master state:對(duì)于 new master 來說,不僅是直接處理新來的請(qǐng)求,還需要恢復(fù) old master 崩潰時(shí)的狀態(tài)。對(duì)于 old master 節(jié)點(diǎn)狀態(tài)的可恢復(fù)性,我們不能指望已經(jīng) crash 的 old master,這時(shí) zk 就發(fā)揮作用了。集群的狀態(tài)信息可以存儲(chǔ)在 zk 中,即使 master 宕機(jī),新的 master 也可以正常接管。
false suspicion master crash:
- 當(dāng) master 負(fù)載很高,響應(yīng)緩慢時(shí),可能無法及時(shí)的與 zk 發(fā)送 heartbeats,導(dǎo)致 zk 以為 master down,從而啟動(dòng) master election
- network partition 發(fā)生,master & 部分 slaves 和 zk 斷開網(wǎng)絡(luò)鏈接,從而啟功 master election,導(dǎo)致系統(tǒng)拆分為兩個(gè)集群,引發(fā) split-brain
Worker Failures
master 要 watch slaves 創(chuàng)建的臨時(shí) /slaves 的狀態(tài)。如果 slave crash,master 要能及時(shí)被 notification 到,并將任務(wù)調(diào)度到別的 available 的 slaves 上。
Communication Failures
如果某個(gè) slave 和 master 的鏈接斷掉,master 不知道是否需要繼續(xù)分配這個(gè)任務(wù)給其他 available slaves:
- 如果分配給新的 slave,舊的 slave 可能已經(jīng)處理完這個(gè)任務(wù),導(dǎo)致重復(fù)執(zhí)行任務(wù)
- 如果不分配給新的 slave,舊的 slave 可能沒有處理完這個(gè)任務(wù),導(dǎo)致任務(wù)丟失
這需要根據(jù)任務(wù)是否可被重復(fù)執(zhí)行來做對(duì)應(yīng)的處理。
這有點(diǎn)像 kafka consumer group 面臨的問題。當(dāng) consumer 宕機(jī) 時(shí),consumer group 并不能確定消息是否成功消費(fèi)完,所以對(duì)于消息不可重復(fù)消費(fèi)的場(chǎng)景, offset 的處理需要格外謹(jǐn)慎。
Dealing with Failure
故障發(fā)生的主要點(diǎn)有三個(gè):ZooKeeper server、network、Zookeeper client。故障恢復(fù)取決于所找到的故障發(fā)生的具體位置。
Recoverable Failures
遇到可恢復(fù)的故障,ZooKeeper client 庫會(huì)積極地嘗試重連另一個(gè) ZooKeeper server,直到最終重新建立了 session。一旦 session 重新建立,ZooKeeper 會(huì)產(chǎn)生一個(gè) SyncConnected 事件,并開始處理請(qǐng)求。ZooKeeper 還會(huì)注冊(cè)之前已經(jīng)注冊(cè)過的 watch,并會(huì)對(duì)失去連接這段時(shí)間發(fā)生的變更產(chǎn)生監(jiān)視點(diǎn)事件。
ConnectionLossException 異常為 recoverable failure
如果在連接丟失時(shí),客戶端沒有進(jìn)行中的請(qǐng)求,這種情況只會(huì)對(duì)客戶端產(chǎn)生很小的影響。但是,如果存在進(jìn)行中的請(qǐng)求,連接丟失就會(huì)產(chǎn)生很大的影響??紤]下圖場(chǎng)景:

當(dāng)接收到 ConnectionLossException 異常時(shí), 客戶端無法判斷請(qǐng)求是否已經(jīng)被處理,處理連接丟失會(huì)使我們的代碼復(fù)雜,因?yàn)閼?yīng)用程序必須判斷請(qǐng)求是否已經(jīng)完成。
The Exists Watch and the Disconnected Event
為了使連接斷開與重現(xiàn)建立 session 之間更加平滑,ZooKeeper client 庫會(huì)在新的服務(wù)器上重新建立所有已經(jīng)存在的監(jiān)視點(diǎn)。當(dāng)客戶端連接 ZooKeeper server,client 會(huì)發(fā)送 watch list 和最后已知的 zxid(詳見之前的介紹),server 會(huì)接受這些監(jiān)視點(diǎn)并檢查 znode 節(jié)點(diǎn)的修改時(shí)間戳與這些監(jiān)視點(diǎn)是否對(duì)應(yīng),如果任何已經(jīng)監(jiān)視的 znode 節(jié)點(diǎn)的修改時(shí)間戳晚于最后已知的 zxid,服務(wù)器就會(huì)觸發(fā)這個(gè)監(jiān)視點(diǎn)。
然后存在一種錯(cuò)過監(jiān)視點(diǎn)事件的特殊情況,即通過 exist 注冊(cè)的監(jiān)視點(diǎn):

Unrecoverable Failures
有時(shí)一些更糟的事情發(fā)生,導(dǎo)致 session 無法恢復(fù)而必須被關(guān)閉。這種情況最常見的原因是:
- session 過期
- 已認(rèn)證的 session 無法再次與 ZooKeeper 完成認(rèn)證
這兩種情況下,ZooKeeper 都會(huì)丟棄 session 的狀態(tài)。
處理不可恢復(fù)故障的最簡(jiǎn)單方法就是中止進(jìn)程并重啟,這樣可以使進(jìn)程恢復(fù)原狀,通過一個(gè)新的 session 重新初始化自己的狀態(tài)。如果該進(jìn)程繼續(xù)工作,首先必須要清除與舊 session 關(guān)聯(lián)的應(yīng)用內(nèi)部的進(jìn)程狀態(tài)信息,然后重新初始化新的狀態(tài)。
故障恢復(fù)很復(fù)雜 & 容易出現(xiàn)不一致性的問題。ZooKeeper server 無法保護(hù)與外部設(shè)備的交互操作。
當(dāng)運(yùn)行客戶端進(jìn)程的主機(jī)發(fā)生過載,系統(tǒng)顛簸或因已經(jīng)超負(fù)荷的主機(jī)資源的競(jìng)爭(zhēng)而導(dǎo)致的進(jìn)程延遲,這些都會(huì)影響與 ZooKeeper server 交互的及時(shí)性。一方面,ZooKeeper client 無法及時(shí)地與 ZooKeeper server 發(fā)送心跳信息,導(dǎo)致 client 的 session 超時(shí),另一方面,主機(jī)上本地線程的調(diào)度會(huì)導(dǎo)致不可預(yù)知的調(diào)度:應(yīng)用線程認(rèn)為 session 仍然處于活動(dòng)狀態(tài),并持有主節(jié)點(diǎn),即使 ZooKeeper 線程有機(jī)會(huì)運(yùn)行時(shí)才會(huì)通知 session 已經(jīng)超時(shí)。解決方案如下:

不過,隔離方案需要修改客戶端與資源之間的協(xié)議,需要在協(xié)議中添加 zxid,外部資源也需要持久化保存來跟蹤接收到的最新的 zxid。
時(shí)鐘偏移也可能導(dǎo)致類似的問題,在 HBase 環(huán)境中,因系統(tǒng)超載而導(dǎo)致時(shí)鐘凍結(jié),有時(shí)候,時(shí)鐘偏移會(huì)導(dǎo)致時(shí)間變慢甚至落后,使得客戶端認(rèn)為自己還安全地處于超時(shí)周期之內(nèi),因此仍然具有管理權(quán),盡管其 session 已經(jīng)被 ZooKeeper server 置為過期。

