深入ZooKeeper——ZooKeeper原語和架構
ZooKeeper基礎
設計一個用於協作需求的服務的方法往往是:
提供原語列表,暴露出每個原語的例項化呼叫方法,並直接控制這些例項。
這種設計存在一些重大的缺陷:首先,我們要麼預先提出一份詳盡的原語列表,要麼提供API的擴充套件,以便引入新的原語;其次,以這種方式實現原語的服務使得應用喪失了靈活性。
因此,在ZooKeeper中我們另闢蹊徑。ZooKeeper並不直接暴露原語,取而代之,它暴露了由一小部分呼叫方法組成的類似檔案系統的API,以便允許應用實現自己的原語。
我們通常使用菜譜(recipes)來表示這些原語的實現。菜譜包括ZooKeeper操作和維護一個小型的資料節點,這些節點被稱為znode,採用類似於檔案系統的層級樹狀結構進行管理。
圖描述了一個znode樹的結構,根節點包含4個子節點,其中三個子節點擁有下一級節點,葉子節點儲存了資料資訊。
針對一個znode,沒有資料常常表達了重要的資訊。比如,在主-從模式的例子中,主節點的znode沒有資料,表示當前還沒有選舉出主節點。
/workers
節點作為父節點,其下每個znode子節點儲存了系統中一個可用從節點資訊。如圖所示,有一個從節點(foot.com:2181)。/tasks
節點作為父節點,其下每個znode子節點儲存了所有已經建立並等待從節點執行的任務的資訊。主-從模式的應用的客戶端在/tasks下新增一個znode子節點,用來表示一個新任務,並等待任務狀態的znode節點。/assign
節點作為父節點,其下每個znode子節點儲存了分配到某個
從節點的一個任務資訊,當主節點為某個從節點分配了一個任務,就會
在/assign下增加一個子節點。
API概述
znode節點可能含有資料,也可能沒有。如果一個znode節點包含任何資料,那麼資料儲存為位元組陣列(byte array)。
位元組陣列的具體格式特定於每個應用的實現,ZooKeeper並不直接提供解析的支援。我們可以使用如Protocol Buffers、Thrift、Avro或MessagePack等序列化(Serialization)包來方便地處理保存於znode節點的資料格式,不過有些時候,以UTF-8或ASCII編碼的字串已經夠用了。
ZooKeeper的API暴露了以下方法:
- create/path data
建立一個名為/path的znode節點,幷包含資料data。 - delete/path
刪除名為/path的znode。 - exists/path
檢查是否存在名為/path的節點。 - setData/path data
設定名為/path的znode的資料為data。 - getData/path
返回名為/path節點的資料資訊。 - getChildren/path
返回所有/path節點的所有子節點列表。
需要注意的是,ZooKeeper並不允許區域性寫入或讀取znode節點的資料。當設定一個znode節點的資料或讀取時,znode節點的內容會被整個替換或全部讀取進來。
znode的不同型別
當新建znode時,還需要指定該節點的型別(mode),不同的型別決定了znode節點的行為方式。
1.持久和臨時
znode節點可以是持久(persistent)節點,還可以是臨時(ephemeral)節點。持久的znode,如/path,只能通過呼叫delete來進行刪除。臨時的znode,當建立該znode的客戶端的會話因超時或主動關閉而中止時被刪除,或則當某個客戶端(不一定是建立者)主動刪除該節點時被刪除。
因為臨時的znode在其建立者的會話過期時被刪除,所以我們現在不允許臨時節點擁有子節點。
2.有序節點
一個znode還可以設定為有序(sequential)節點。一個有序znode節點被分配唯一個單調遞增的整數。
當建立有序節點時,一個序號會被追加到路徑之後。例如,如果一個客戶端建立了一個有序znode節點,其路徑為/tasks/task-,那麼ZooKeeper將會分配一個序號,如1,並將這個數字追加到路徑之後,最後該znode節點為/tasks/task-1。
監視與通知
ZooKeeper通常以遠端服務的方式被訪問,如果每次訪問znode時,客戶端都需要獲得節點中的內容,這樣的代價就非常大。因為這樣會導致更高的延遲,而且ZooKeeper需要做更多的操作。
考慮圖2-2中的例子,第二次呼叫getChildren/tasks返回了相同的值,一個空的集合,其實是沒有必要的。
這是一個常見的輪詢問題。為了替換客戶端的輪詢,我們選擇了基於通知(notification)的機制。
這樣做還有一個問題:
因為通知機制是單次觸發的操作,所以在客戶端接收一個znode變更通知並設定新的監視點時,znode節點也許發生了新的變化。比如下面這個情況:
1.客戶端c1 設定監視點來監控/tasks資料的變化。
2.客戶端c1 連線後,向/tasks中添加了一個新的任務。
3.客戶端c1 接收通知。
4.客戶端c1 設定新的監視點,在設定完成前,第三個客戶端c3 連線後,向/tasks中添加了一個新的任務。
客戶端c1 最終設定了新的監視點,但由c3 新增資料的變更並沒有觸發一個通知。為了觀察這個變更,在設定新的監視點前,c1 實際上需要讀取節點/tasks的狀態,通過在設定監視點前讀取ZooKeeper的狀態,最終,c1 就不會錯過任何變更。
版本
每一個znode都有一個版本號,它隨著每次資料變化而自增。兩個API操作可以有條件地執行:setData和delete。這兩個呼叫以版本號作為轉入引數,只有當轉入引數的版本號與伺服器上的版本號一致時呼叫才會成功。
ZooKeeper架構
ZooKeeper伺服器端運行於兩種模式下:獨立模式(standalone)和仲裁模式(quorum)。
獨立模式幾乎與其術語所描述的一樣:有一個單獨的伺服器,ZooKeeper狀態無法複製。在仲裁模式下,具有一組ZooKeeper伺服器,我們稱為ZooKeeper集合(ZooKeeper ensemble),它們之前可以進行狀態的複製,並同時為服務於客戶端的請求。
ZooKeeper仲裁
在仲裁模式下,ZooKeeper複製叢集中的所有伺服器的資料樹。但如果讓一個客戶端等待每個伺服器完成資料儲存後再繼續,延遲問題將無法接受。
在公共管理領域,法定人數是指進行一項投票所需的立法者的最小數量。而在ZooKeeper中,則是指為了使ZooKeeper工作必須有效執行的伺服器的最小數量。這個數字也是伺服器告知客戶端安全儲存資料前,需要儲存客戶端資料的伺服器的最小個數。
為了明白這到底是什麼意思,讓我們先來通過一個例子來看看:
假設有5個伺服器並設定法定人數為2,現在伺服器s1 和s2 確認它們需要對一個請求建立的znode/z進行復制,服務返回客戶端,指出znode建立完成。
現在假設在複製新的znode到其他伺服器之前,伺服器s1 和s2 與其他伺服器和客戶端發生了長時間的分割槽隔離(更關鍵在於長時間)。這樣會被認定為通訊故障,將叢集分割成兩個分割槽,分別包含 2 臺機器和 3 臺機器,因為機器數量都不小於法定人數,所以能獨立執行。這樣出現了腦裂!
在集合中,伺服器的個數並不是必須為奇數,只是使用偶數會使得系統更加脆弱。假設在集合中使用4個伺服器,那麼多數原則對應的數量為3個伺服器。然而,這個系統僅能容許1個伺服器崩潰,因為兩個伺服器崩潰就會導致系統失去多數原則的狀態。
會話
在對ZooKeeper集合執行任何請求前,一個客戶端必須先與服務建立會話。會話的概念非常重要,對ZooKeeper的執行也非常關鍵。客戶端提交給ZooKeeper的所有操作均關聯在一個會話上。當一個會話因某種原因而中止時,在這個會話期間建立的臨時節點將會消失。