1. 程式人生 > >深入ZooKeeper——ZooKeeper原語和架構

深入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的所有操作均關聯在一個會話上。當一個會話因某種原因而中止時,在這個會話期間建立的臨時節點將會消失。