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