很遗憾,没有一篇文章能讲清楚ZooKeeper
作為分布式系統(tǒng)解決方案的 ZooKeeper,被廣泛應(yīng)用于多個分布式場景。例如:數(shù)據(jù)發(fā)布/訂閱,負載均衡,命名服務(wù),集群管理等等。
因此,ZooKeeper 在分布式系統(tǒng)中扮演著重要的角色,今天通過一個簡單的例子來看看它的實現(xiàn)原理。
從一個簡單的例子開始
在分布式系統(tǒng)中經(jīng)常會遇到這種情況,多個應(yīng)用讀取同一個配置。例如:A,B 兩個應(yīng)用都會讀取配置 C 中的內(nèi)容,一旦 C 中的內(nèi)容出現(xiàn)變化,會通知 A 和 B。
一般的做法是在 A,B 中按照時鐘頻率詢問 C 的變化,或者使用觀察者模式來監(jiān)聽 C 的變化,發(fā)現(xiàn)變化以后再更新 A 和 B。那么 ZooKeeper 如何協(xié)調(diào)這種場景?
ZooKeeper 會建立一個 ZooKeeper 服務(wù)器,暫且稱為 ZServer,用它來存放 C 的值。為 A,B 兩個應(yīng)用分別生成兩個客戶端,稱作 ClientA 和 ClientB。
這兩個客戶端連接到 ZooKeeper 的服務(wù)器,并獲取其中存放的 C。保存 C 值的地方在 ZooKeeper 服務(wù)端(Server)中稱為 ZNode。
ClientA 和 ClientB 通過 ZooKeeper Server 獲取 C 的值ZNode
通過上面的例子,客戶端 ClientA 和 ClientB 需要讀取 C 的內(nèi)容。這個 C 就作為樹的葉子節(jié)點存放在 ZooKeeper 的 ZNode 中。
通常來說,為了提高效率 ZNode 是被存放在內(nèi)存中的。ZNode 的數(shù)據(jù)模型是一棵樹(ZNode Tree)。
好像我們從上圖中看到的一樣,樹中的每個節(jié)點都可以存放數(shù)據(jù),并且每個節(jié)點下面都可以存放葉子節(jié)點。
ZooKeeper 客戶端通過?“/” 作為訪問路徑,訪問數(shù)據(jù)。例如可以通過路徑 “/RootNode/C” 來訪問 C 變量。
為了方便客戶端調(diào)用,ZooKeeper 會暴露一些命令:
訪問 Znode 命令作為存儲媒介來說,ZNode分為持久節(jié)點和臨時節(jié)點:
-
持久節(jié)點(PERSISTENT),該數(shù)據(jù)節(jié)點被創(chuàng)建后,就一直存在于?ZooKeeper?服務(wù)器上,除非刪除操作(delete)清除該節(jié)點。
-
臨時節(jié)點(EPHEMERAL),該數(shù)據(jù)節(jié)點的生命周期會和客戶端(Client)會話(Session)綁定在一起。如果客戶端(Client)會話丟失了,那么節(jié)點就自動清除掉。
如果把臨時節(jié)點看成資源的話,當客戶端和服務(wù)端產(chǎn)生會話并生成臨時節(jié)點,一旦客戶端與服務(wù)器中斷聯(lián)系,節(jié)點資源會被從 ZNode 中刪除。
順序節(jié)點(SEQUENTIAL),ZNode 節(jié)點被分配唯一個單調(diào)遞增的整數(shù)。例如多個客戶端在服務(wù)器 /tasks 上申請節(jié)點時,根據(jù)客戶端申請的先后順序,將數(shù)字追加到 /tasks/task 后面。
如果有三個客戶端申請節(jié)點資源,那么在 /tasks 下面建立三個順序節(jié)點,分別是?/tasks/task1,/tasks/task2,/tasks/task3。
順序節(jié)點,在處理分布式事務(wù)的時候非常有幫助,當多個客戶端(Client)協(xié)作工作的時候,會按照一定的順序執(zhí)行。
如果將上面的兩類節(jié)點和順序節(jié)點進行組合的話,就有四種節(jié)點類型,分別是持久節(jié)點,持久順序節(jié)點,臨時節(jié)點,臨時順序節(jié)點。
Watcher
上面說了 ZooKeeper 用來存放數(shù)據(jù)的 ZNode,并且把 C 的值存儲在里面。如果 C 被更新了,兩個客戶端(ClientA、ClientB)如何獲得通知呢?
ZooKeeper 客戶端(Client)會在指定的節(jié)點(/RootNote/C)上注冊一個 Watcher,ZNode 上的 C 被更新的時候,服務(wù)端就會通知 ClientA 和 ClientB。
通過三步來實現(xiàn):
-
客戶端注冊 Watcher
-
服務(wù)端處理?Watcher
-
客戶端回調(diào) Watcher
①客戶端注冊 Watcher
ZooKeeper 客戶端創(chuàng)建 Watcher 的實例對象:
同時這個 Watcher 會保存在客戶端本地,一直作為和服務(wù)端會話的 Watcher。
客戶端可以通過 getData,getChildren 和 exist 方法來向服務(wù)端注冊 Watcher。
客戶端注冊 Watcher 簡圖同時需要注意的是在客戶端發(fā)送 Watcher 到服務(wù)端注冊的時候,會將這個要發(fā)送的 Watcher 在本地的 ZKWatchManager 中保存。
這樣做的好處,就是當獲得服務(wù)端的注冊成功的信息以后,就不用將 Watcher 的具體內(nèi)容回傳給客戶端了。
客戶端只用在接到服務(wù)端響應(yīng)以后,從本地的 ZKWatchManager 中獲取 Watch 的信息進行處理即可。
②服務(wù)端處理 Watcher
服務(wù)端收到客戶端的請求以后,交給 FinalRequestProcessor 處理,這個進程會去 ZNode 中獲取對應(yīng)的數(shù)據(jù),同時會把 Watch 加入到 WatchManager 中。
這樣下次這節(jié)點上的數(shù)據(jù)被更改了以后,就會通知注冊 Watch 的客戶端了。
服務(wù)端處理 Watch 過程③客戶端回調(diào) Watcher
客戶端在響應(yīng)客戶端 Watcher 注冊以后,會發(fā)送 WathcerEvent 事件。作為客戶端有對應(yīng)的回調(diào)函數(shù)接受這個消息。
這里會通過 readResponse 方法統(tǒng)一處理:
在 SendTread 接受到服務(wù)端的通知以后,會將事件通過 EventThread.queueEvent 發(fā)送給 EventThread。
正如前面提到的,在客戶端注冊時,已經(jīng)將 Watcher 的具體內(nèi)容保存在 ZKWatchManager 一樣了。
所以,EventTread 通過 EventType 就可以知道哪個 Watcher 被響應(yīng)了(數(shù)據(jù)變化了)。
然后從 ZKWatchManager 取出具體 Watch 放到 waitingEvent 隊列等待處理。
最后,由 EventThread 中的 processEvent 方法依次處理數(shù)據(jù)更新的響應(yīng)。
?
版本(Version)
介紹完了 Watcher 機制,回頭再來談?wù)?ZNode 的版本(Version)。如果有一個客戶端(ClientD),它嘗試修改 C 的值,此時其他兩個客戶端會收到通知,并且進行后續(xù)的業(yè)務(wù)處理了。
那么在分布式系統(tǒng)中,會出現(xiàn)這么一種情況:在 ClientD 對 C 進行寫入操作的時候,又有一個 ClientE 也對 C 進行寫入操作。這兩個 Client 會去競爭 C 資源,通常這種情況需要對 C 進行加鎖操作。
兩個 Client 競爭一個資源因此引入 ZNode 版本(Version)概念。版本是用來保證分布式數(shù)據(jù)原子性操作的。
ZNode 的版本(Version)信息保存在 ZNode 的 Stat 對象中。有如下三種:
本例只關(guān)注“數(shù)據(jù)節(jié)點內(nèi)容的版本號”,也就是 Version。
如果說 ClientD 和 ClientE 對 C 進行寫入操作視作是一個事務(wù)的話。在執(zhí)行寫入操作之前,兩個事務(wù)分別會獲取節(jié)點上的值,即節(jié)點保存的數(shù)據(jù)和節(jié)點的版本號(Version)。
以樂觀鎖為例,對數(shù)據(jù)的寫入會分成以下三個階段:數(shù)據(jù)讀取,寫入校驗和數(shù)據(jù)寫入。例如 C 上的數(shù)據(jù)是 1, Version 是 0。
此時 ClientD 和 ClientE 都獲取了這些信息。假設(shè) ClientD 先做寫入操作,在做寫入校驗的時候,發(fā)現(xiàn)之前獲得的 Version 和節(jié)點上的 Version 是相同的,都是 1,因此直接執(zhí)行數(shù)據(jù)寫入。
寫入以后,Version 由原來的 1 變成了 2。當 ClientE 做寫入校驗時,發(fā)現(xiàn)自己持有的 Version=1 和節(jié)點當前的 Version=2,不一樣。于是,寫入失敗,重新獲取 Version 和節(jié)點數(shù)據(jù),再次嘗試寫入。
除了上述方案以外,還可以利用 ZNode 的有序性。在 C 下面建立多個有序的子節(jié)點。每當一個 Client 準備寫入數(shù)據(jù)的時候,創(chuàng)建一個臨時有序的節(jié)點。
節(jié)點的順序根據(jù) FIFO 算法,保證先申請寫入的 Client 排在其前面。每個節(jié)點都有一個序號,后面申請的節(jié)點按照序號依次遞增。
ClientD,ClientE?分別建立子 ZNode每個 Client 在執(zhí)行修改 C 操作的時候,都要檢查有沒有比自己序號小的節(jié)點,如果存在那么就進入等待。
直到比自己序號小的節(jié)點進行完畢以后,才輪到自己執(zhí)行修改操作。從而保證了事物處理的順序性。
會話(Session)
說完版本(Version)的概念,例子從原來的 ClientAB 已經(jīng)擴充到了 ClientDE。這些客戶端都會和 ZooKeeper 的服務(wù)端進行通信,或讀取數(shù)據(jù)或修改數(shù)據(jù)。
我們將客戶端與服務(wù)端完成的這種連接稱為會話。ZooKeeper 的會話有 Connecting,Connected,Reconnecting,Reconnected 和 Close 這幾種狀態(tài)。
并且在服務(wù)端由專門的進程來管理他們,客戶端初始化的時候就會根據(jù)配置自動連接服務(wù)器,從而建立會話,客戶端連接服務(wù)器時會話處于 Connecting 狀態(tài)。
一旦連接完成,就會進入 Connected 狀態(tài)。如果出現(xiàn)延遲或者短暫失聯(lián),客戶端會自動重連,Reconnecting 和 Reconnected 狀態(tài)也就應(yīng)運而生。
如果長時間超時,或者客戶端斷開服務(wù)器,ZooKeeper 會清理掉會話,以及該會話創(chuàng)建的臨時數(shù)據(jù)節(jié)點,并且關(guān)閉和客戶端的連接。
Session 作為會話實體,用來代表客戶端會話,其包括 4 個屬性:
-
SessionID,用來全局唯一識別會話。
-
TimeOut,會話超時事件。客戶端在創(chuàng)造 Session 實例的時候,會設(shè)置一個會話超時的時間。
-
TickTime,下次會話超時時間點。后面“分桶策略”會用到。
-
isClosing,當服務(wù)端如果檢測到會話超時失效了,會通過設(shè)置這個屬性將會話關(guān)閉。
既然,會話是客戶端與服務(wù)器之間的連接。在服務(wù)器端由 SessionTracker 管理會話。
SessionTracker 有一個工作就是,將超時的會話清除掉。于是“分桶策略”就登場了。
由于每個會話在生成的時候都會定義超時時間,通過當前時間+超時時間可以算出會話的過期時間。
由于 SessionTracker 不是實時監(jiān)聽會話超時,它是按照一定時間周期來監(jiān)聽的。
也就是說,如果沒有到達 SessionTracker 的檢查時間周期,即使有會話過期,SessionTracker 也不會去清除。由此,就引入會話超時計算公式,也就是 TickTime 的計算公式。
TickTime=((當前時間+會話過期時間)/檢查時間間隔+1)*檢查時間間隔。
將這個值計算出來以后,SessionTracker 會把對應(yīng)的會話按照這個時間放在對應(yīng)的時間軸上面。SessionTracker 在對應(yīng)的 TickTime 檢查會話是否過期。
計算會話下次的過期時間每當客戶端連接上服務(wù)器都會做激活操作,同時每隔一段時間客戶端會向服務(wù)器發(fā)送心跳檢測。
服務(wù)器收到激活或者心跳檢測以后,會重新計算會話過期時間,根據(jù)“分桶策略”進行重新調(diào)整。把會話從“老的區(qū)塊“放到”新的區(qū)塊“中去。
重新計算過期時間并且調(diào)整“分桶策略”對于超時的會話,SessionTracker 也會做如下清理工作:
-
標記會話狀態(tài)為“已關(guān)閉”,也就是設(shè)置 isClosing 為 True。
-
發(fā)起“會話關(guān)閉”的請求,讓關(guān)閉操作在整個集群生效。
-
收集需要清理的臨時節(jié)點。
-
添加“節(jié)點刪除”的事務(wù)變更。
-
刪除臨時節(jié)點
-
移除會話
-
關(guān)閉客戶端與服務(wù)端的連接
會話關(guān)閉以后客戶端就無法從服務(wù)端獲取/寫入數(shù)據(jù)了。
服務(wù)群組(Leader,Follower,Observer)
前面提到了客戶端如何通過會話與服務(wù)端保持聯(lián)系,以及服務(wù)端是如何管理客戶端會話(Session)的。
我們繼續(xù)思考一下,這么多的服務(wù)端都依賴一個 ZooKeeper 服務(wù)器。一旦服務(wù)掛了,客戶端就無法工作了。
為了提高 ZooKeeper 服務(wù)的可靠性,引入服務(wù)器集群的概念。從原來的單個服務(wù)器,擴充成多個服務(wù)器,即使某一臺服務(wù)器掛了,其他的服務(wù)器也可以頂上來。
ZooKeeper 的服務(wù)器集群這樣看起來不錯了,新的問題是,存在多個 ZooKeeper 服務(wù)器,那么客戶端的請求發(fā)給哪臺呢?服務(wù)器之間如何同步數(shù)據(jù)呢?如果一個服務(wù)掛掉了其他的服務(wù)器如何替代?這里介紹兩個概念 Leader 和 Follower。
Leader 服務(wù)器,是事務(wù)請求(寫操作)的唯一調(diào)度者和處理者,保證集群事務(wù)處理的順序性。也是集群內(nèi)部服務(wù)器的調(diào)度者。
它是整個集群的老大,其他的服務(wù)器接到事務(wù)請求都會轉(zhuǎn)交給它,讓它協(xié)調(diào)處理。
Follower 服務(wù)器,處理非事務(wù)請求(讀操作),轉(zhuǎn)發(fā)事務(wù)請求給 Leader 服務(wù)器。參與選舉 Leader 的投票和事務(wù)請求 Proposal 的投票。
既然 Leader 是集群的老大,那么這個老大是如何產(chǎn)生的。ZooKeeper 有仲裁機制,通過服務(wù)器的選舉產(chǎn)生這個 Leader,按照少數(shù)服從多數(shù)的原則。
因此,集群中服務(wù)器的個數(shù)一般都是奇數(shù),例如:1,3,5。當然這里是建議。關(guān)于選舉和仲裁都有一定的算法,一起來看看吧。
當眾多服務(wù)器啟動的時候,互相都不知道誰是 Leader,因此都會進入 Looking 狀態(tài),也就是在網(wǎng)絡(luò)中尋找 Leader。
尋找的過程也是投票的過程,每個服務(wù)器會將服務(wù)器 ID 和事務(wù) ID 作為投票信息發(fā)送給網(wǎng)絡(luò)中其他的服務(wù)器。假設(shè)稱它為投票信息 VOTE,它包括:(ServerID,ZXID)。
其中,ServerID 是服務(wù)器注冊的 ID,隨著服務(wù)器啟動的順序自動增加,后啟動的服務(wù)器 ServerID 就大;ZXID 是服務(wù)器處理事物的 ID,隨著事物的增加自動增加,同樣后提交的事務(wù) ZXID 也大一些。
其他的服務(wù)器收到 VOTE 信息以后會和自己的 VOTE 信息(ServerID,ZXID)進行比較。
如果收到的 VOTE(ServerID,ZXID)中的 ZXID 比自己的 ZXID 要大,那么把自己的 VOTE 修改成收到的 VOTE。
如果 ZXID 一樣大,那么就比較 ServerID,將大的那個 ServerID 作為自己 VOTE 的 ServerID,轉(zhuǎn)發(fā)給其他服務(wù)器。
再簡單點說,如果事務(wù) ID(ZXID)比自己的事務(wù) ID(ZXID)要大,就把票投給這個服務(wù)器。如果事務(wù) ID 一樣,就把票投給 ServerID 大的服務(wù)器。
來個具體的例子,有三個服務(wù)器,他們的投票值分別是:
-
S1 (1,6)
-
S2 (2,5)
-
S3 (3,5)
三個服務(wù)器分別把自己的 VOTE 發(fā)給其他兩臺服務(wù)器,S2 和 S3 收到 VOTE 以后發(fā)現(xiàn) ZXID 為 6 的來自 S1 的 VOTE 比自己持有的 ZXID 要大,因此把自己的 VOTE 修改為(1,6)投出去,因此 S1 稱為 Leader。
Leader 選舉實例同樣,如果 S1 作為 Leader,因為某種原因掛掉或者長時間沒有響應(yīng)請求,其他的服務(wù)器也會進入 Looking 狀態(tài),開啟投票仲裁模式尋找下一個 Leader。
成為新 Leader 以后會通過廣播的方式將 ZNode 上的數(shù)據(jù)同步到其他的 Follower。
Leader 有了,整個服務(wù)器集群有了領(lǐng)袖,它可以處理客戶端的事物請求。客戶端的請求可以發(fā)給集群中任意一臺服務(wù)器,無論是哪個服務(wù)器都會將事物請求轉(zhuǎn)交給 Leader。
Leader 在將數(shù)據(jù)寫入 ZNode 之前會向 ZooKeeper 的其他 Follower 進行廣播。
這里廣播用到了 ZAB 協(xié)議(Atomic?Broadcast?Protocol)是 Paxos 協(xié)議的實踐。說白了就是一個兩段提交。
PS:對分布式事務(wù)比較了解的同學(xué)應(yīng)該知道兩段提交和三段提交。
這里 ZooKeeper 通過以下方式實現(xiàn)兩段提交:
-
Leader 向所有 Follower 發(fā)送一個 PROPOSAL。
-
當 Follower 接收到 PROPOSAL 后,返回給 Leader 一個 ACK 消息,表示我收到 PROPOSAL,并且準備好了。
-
Leader 仲裁數(shù)量(過半數(shù))的 Follower 發(fā)送的 ACK 后(包括 Leader 自己),會發(fā)送消息通知 Follower 進行 COMMIT。
-
收到 COMMIT 以后,Follower 就開始干活,將數(shù)據(jù)寫入到 ZNode 中。
選舉了 Leader 領(lǐng)導(dǎo)集群,Leader 接受到 Client 的請求以后,也可以協(xié)調(diào) Follower 工作了。
那么如果 Client 很多的情況下,特別是這些客戶端都是做讀操作的時候,ZooKeeper 服務(wù)器如何處理如此多的請求呢?這里引入 Observer 的概念。
Observer 和 Follower 基本一致,對于非事務(wù)請求(讀操作),可以直接返回節(jié)點中的信息(數(shù)據(jù)從 Leader 中同步過來的)。
對于事務(wù)請求(寫操作),會轉(zhuǎn)交給 Leader 做統(tǒng)一處理。Observer 的存在就是為了解決大量客戶端讀請求。
Observer 和 Follower 的區(qū)別是,Observer 不參與仲裁投票,選舉 Leader。
Observer 加入 Leader 和 Follower 大家庭總結(jié)
全文用了一個簡單的例子講 ZooKeeper 的主要特性和實現(xiàn)原理,最后做個總結(jié)。
ZooKeeper 被用來協(xié)調(diào)和管理分布式系統(tǒng),發(fā)揮著重要的作用。分布式系統(tǒng)由于其特性,應(yīng)用分布在不同的物理主機或者網(wǎng)絡(luò)中。
為了讓它們協(xié)同工作,ZooKeeper 中的 ZNode 成為統(tǒng)一協(xié)調(diào)的重要部分,客戶端通過 Client 間接到服務(wù)端的 ZNode 上,監(jiān)聽 ZNode 數(shù)據(jù)的變化。
同時 ZNode 支持的持久,臨時和順序性,以及版本(Version)控制,這些特性支持了分布式事務(wù)和鎖的功能。
如果說,每一個 ZooKeeperClient 對 Server 的寫入操作都是一次事務(wù)的話,ZooKeeper 服務(wù)端維護了大量的事務(wù),并且通過“分桶策略”來管理它們,保證了 Client 與 Server 端協(xié)調(diào)工作。
為了提高 Server 的可靠性,ZooKeeper 引入了 Server 集群的概念。通過仲裁機制選舉 Leader 來領(lǐng)導(dǎo)其他 Follower。
事物都由 Leader 來處理,通過兩段提交的方式對其他 Server 發(fā)起廣播。為了增強對非事務(wù)請求的處理效率,ZooKeeper 加入了 Observer 來幫忙。
ZooKeeper 包含的內(nèi)容遠不止上面說的這些,由于篇幅的原因無法一一道來。
為了方便大家理解,文中將一些原理做了簡化處理,希望有機會和大家做深入的探討,咱們下次見。
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的很遗憾,没有一篇文章能讲清楚ZooKeeper的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 忘掉 Java 并发,先听完这个故事。。
- 下一篇: 这一次彻底搞懂 Git Rebase