ZooKeeper分佈式過程協同技術詳解-第二章

瞭解zookeeper

一、zookeeper基礎

很多用於協作的原語常常在很多應用之間共享,因此,設計一個用於協作需求的服務的方法往往是提供原語列表,暴露出每個原語的實例化調用方法,並直接控制這些實例。比如,我們可以說分佈式鎖機制組成了一 個重要的原語,同時暴露出創建(create)、獲取(acquire)和釋放 (release)三個調用方法。

所謂原語,一般是指由若干條指令組成的程序段,用來實現某個特定功能,在執行過程中不可被中斷。

這種設計存在一些重大的缺陷:

  • 首先,我們要麼預先提出一份詳盡的原語列表,要麼提供API的擴展,以便引入新的原語;
  • 其次,以這種方式 實現原語的服務使得應用喪失了靈活性。

因此,在ZooKeeper中我們另闢蹊徑。ZooKeeper並不直接暴露原語, 取而代之,它暴露了由一小部分調用方法組成的類似文件系統的API,以便允許應用實現自己的原語。

我們通常使用菜譜(recipes)來表示這些原語的實現。菜譜包括ZooKeeper操作和維護一個小型的數據節點,這些節點被稱爲znode,採用類似於文件系統的層級樹狀結構進行管理。

圖2-1描述了一個znode樹的結構,根節點包含4個子節點,其中三個子節點擁有下一 級節點,葉子節點存儲了數據信息。

在這裏插入圖片描述
針對一個znode,沒有數據常常表達了重要的信息。比如,在主-從模式的例子中,主節點的znode沒有數據,表示當前還沒有選舉出主節點。而圖2-1中涉及的一些其他znode節點在主-從模式的配置中非常有用:

  • ·/workers節點作爲父節點,其下每個znode子節點保存了系統中一個可用從節點信息。如圖2-1所示,有一個從節點(foot.com:2181)。
  • ·/tasks節點作爲父節點,其下每個znode子節點保存了所有已經創建並等待從節點執行的任務的信息,主-從模式的應用的客戶端在/tasks下添加一個znode子節點,用來表示一個新任務,並等待任務狀態的znode節點。
  • ·/assign節點作爲父節點,其下每個znode子節點保存了分配到某個從節點的一個任務信息,當主節點爲某個從節點分配了一個任務,就會在/assign下增加一個子節點。

1、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節點的內容會被整個替 換或全部讀取進來。

2、znode的不同類型

當新建znode時,還需要指定該節點的類型(mode),不同的類型決定了znode節點的行爲方式。

持久節點和臨時節點(不可擁有子節點)

znode節點可以是持久(persistent)節點,還可以是臨時(ephemeral) 節點。持久的znode,如/path,只能通過調用delete來進行刪除。臨時的 znode與之相反,當創建該節點的客戶端崩潰或關閉了與ZooKeeper的連接時,這個節點就會被刪除。

  • 持久znode是一種非常有用的znode,可以通過持久類型的znode爲應用保存一些數據,即使znode的創建者不再屬於應用系統時,數據也可以保存下來而不丟失。例如,在主-從模式例子中,需要保存從節點的任務分配情 況,即使分配任務的主節點已經崩潰了。
  • 臨時znode傳達了應用某些方面的信息,僅當創建者的會話有效時這些信息必須有效保存。例如,在主從模式的例子中,當主節點創建的znode爲臨時節點時,該節點的存在意味着現在有一個主節點,且主節點狀態處於正常運行中。如果主znode消失後,該znode節點仍然存在,那麼系統將無法監測到主節點崩潰。這樣就可以阻止系統繼續進行,因此這個znode需要和主節點一起消失。我們也在從節點中使用臨時的znode,如果一個從節點 失效,那麼會話將會過期,之後znode/workers也將自動消失。
臨時節點的兩種被刪除情況
  • 1.當創建該znode的客戶端的會話因超時或主動關閉而中止時。
  • 2.當某個客戶端(不一定是創建者)主動刪除該節點時。

因爲臨時的znode在其創建者的會話過期時被刪除,所以我們現在不允許臨時節點擁有子節點。在社區討論中,已經討論過關於允許臨時znode擁 有子節點的問題,其想法是使其子節點也均爲臨時節點。這個功能也許會 出現在未來的發佈版本中,但現在還是不可用的。

有序節點

一個znode還可以設置爲有序(sequential)節點。一個有序znode節點被分配唯一個單調遞增的整數。當創建有序節點時,一個序號會被追加到路徑之後。例如,如果一個客戶端創建了一個有序znode節點,其路徑 爲/tasks/task-,那麼ZooKeeper將會分配一個序號,如1,並將這個數字追加到路徑之後,最後該znode節點爲/tasks/task-1。有序znode通過提供了創建具有唯一名稱的znode的簡單方式。同時也通過這種方式可以直觀地查看 znode的創建順序。總之,znode一共有4種類型:

  • 持久的(persistent)
  • 臨時的 (ephemeral)
  • 持久有序的(persistent_sequential)
  • 臨時有序的 (ephemeral_sequential)。

3、監視和通知

ZooKeeper通常以遠程服務的方式被訪問,如果每次訪問znode時,客戶端都需要獲得節點中的內容,這樣的代價就非常大。因爲這樣會導致更高的延遲,而且ZooKeeper需要做更多的操作。

考慮圖2-2中的例子,第二次調用getChildren/tasks返回了相同的值,一個空的集合,其實是沒有必要的。
在這裏插入圖片描述
這是一個常見的輪詢問題。爲了替換客戶端的輪詢,我們選擇了基於通知(notification)的機制:

  • 客戶端向ZooKeeper註冊需要接收通知的 znode,通過對znode設置監視點(watch)來接收通知。
  • 監視點是一個單次觸發的操作,意即監視點會觸發一個通知。爲了接收多個通知,客戶端必須在每次通知後設置一個新的監視點。在圖2-3闡述的情況下,當節點/tasks發生變化時,客戶端會收到一個通知,並從ZooKeeper讀取一個新值。

監視點是一個單次觸發的操作,意即監視點會觸發一個通知。爲了接收多個通知,客戶端必須在每次通知後設置一個新的監視點。

在這裏插入圖片描述
當使用通知機制時,還有一些需要知道的事情。因爲通知機制是單次觸發的操作,所以在客戶端接收一個znode變更通知並設置新的監視點時, znode節點也許發生了新的變化(不要擔心,你不會錯過狀態的變化)。讓我們看一個例子來說明它到底是怎麼工作的。假設事件按以下順序發生:

  • 1.客戶端c1設置監視點來監控/tasks數據的變化。
  • 2.客戶端c1連接後,向/tasks中添加了一個新的任務。
  • 3.客戶端c1接收通知。
  • 4.客戶端c1設置新的監視點,在設置完成前,第三個客戶端c3連接後, 向/tasks中添加了一個新的任務。

客戶端c1最終設置了新的監視點,但由c3添加數據的變更並沒有觸發一個通知。爲了觀察這個變更,在設置新的監視點前,c1實際上需要讀取節點/tasks的狀態,通過在設置監視點前讀取ZooKeeper的狀態,最終,c1 就不會錯過任何變更。

通知機制(先通知再更新,否則可能讀到第二次更新數據?)

通知機制的一個重要保障是,對同一個znode的操作,先向客戶端傳送通知,然後再對該節點進行變更。

如果客戶端對一個znode設置了監視點, 而該znode發生了兩個連續更新。第一次更新後,客戶端在觀察第二次變化 前就接收到了通知,然後讀取znode中的數據。我們認爲主要特性在於通知 機制阻止了客戶端所觀察的更新順序。雖然ZooKeeper的狀態變化傳播給某 些客戶端時更慢,但我們保障客戶端以全局的順序來觀察ZooKeeper的狀 態。

通知類型(使得客戶端可以監視不同的變化)

客戶端可以設置多種監視點,如

  • 監控znode的數據變化
  • 監控znode 子節點的變化
  • 監控znode的創建或刪除。

爲了設置監視點,可以使用任何 API中的調用來讀取ZooKeeper的狀態,在調用這些API時,傳入一個 watcher對象或使用默認的watcher。本章後續(主從模式的實現)及第4章 會以主從模式的例子來展開討論,我們將深入研究如何使用該機制。

緩存管理

如果不讓客戶端來管理其擁有的ZooKeeper數據的緩存,我們不得不 讓ZooKeeper來管理這些應用程序的緩存。但是,這樣會導致ZooKeeper的 設計更加複雜。事實上,如果讓ZooKeeper管理緩存失效,可能會導致 ZooKeeper在運行時,停滯在等待客戶端確認一個緩存失效的請求上,因爲在進行所有的寫操作前,需要確認所有的緩存數據是否已經失效。

4、版本

每一個znode都有一個版本號,它隨着每次數據變化而自增。兩個API 操作可以有條件地執行:setData和delete。這兩個調用以版本號作爲轉入參 數,只有當轉入參數的版本號與服務器上的版本號一致時調用纔會成功。 當多個ZooKeeper客戶端對同一個znode進行操作時,版本的使用就會顯得 尤爲重要。

例如,假設客戶端c1對znode/config寫入了一些配置信息,如果 另一個客戶端c2同時更新了這個znode,此時c1的版本號已經過期,c1調用 setData一定不會成功。使用版本機制有效避免了以上情況。在這個例子 中,c1在寫入數據時使用的版本無法匹配,使得操作失敗,圖2-4描述了這 個情況。

在這裏插入圖片描述

二、ZooKeeper架構

應用通過客戶端庫來對ZooKeeper實現了調 用。客戶端庫負責與ZooKeeper服務器端進行交互。

圖2-5展示了客戶端與服務器端之間的關係。每一個客戶端導入客戶端 庫,之後便可以與任何ZooKeeper的節點進行通信。

在這裏插入圖片描述

ZooKeeper服務器端運行於兩種模式下:獨立模式(standalone)和仲裁模式(quorum)。獨立模式幾乎與其術語所描述的一樣:有一個單獨的 服務器,ZooKeeper狀態無法複製。在仲裁模式下,具有一組ZooKeeper服 務器,我們稱爲ZooKeeper集合(ZooKeeper ensemble),它們之前可以進行狀態的複製,並同時爲服務於客戶端的請求。從這個角度出發,我們使 用術語“ZooKeeper集合”來表示一個服務器設施,這一設施可以由獨立模式 的一個服務器組成,也可以仲裁模式下的多個服務器組成。

Zookeeper仲裁

在仲裁模式下,ZooKeeper複製集羣中的所有服務器的數據樹。但如果讓一個客戶端等待每個服務器完成數據保存後再繼續,延遲問題將無法接 受

在公共管理領域,法定人數是指進行一項投票所需的立法者的最小數量。而在ZooKeeper中,則是指爲了使ZooKeeper工作必須有效運行的服務器的最小數量。這個數字也是服務器告知客戶端安全保存數據前,需要保存客戶端數據的服務器的最小個數。例如,我們一共有5個ZooKeeper服務 器,但法定人數爲3個,這樣,只要任何3個服務器保存了數據,客戶端就 可以繼續,而其他兩個服務器最終也將捕獲到數據,並保存數據。

選擇法定人數準確的大小是一個非常重要的事。法定人數的數量需要保證不管系統發生延遲或崩潰,服務主動確認的任何更新請求需要保持下 去,直到另一個請求代替它。

法定人數的大小必須至少爲3,即集合中5個服務器的多數原則。通過使用多數方案,我們就可以容許f個服務器的崩潰,在這裏,f爲小於集合中服務器數量的一半。例如,如果有5個服務器,可以容許最多 f=2個崩潰。

會話

會話的概念非常重要,對ZooKeeper的運行也非常關鍵。客戶端提交給 ZooKeeper的所有操作均關聯在一個會話上。當一個會話因某種原因而中止 時,在這個會話期間創建的臨時節點將會消失。

客戶端初始連接到集合中某一個服務器或一個 獨立的服務器。客戶端通過TCP協議與服務器進行連接並通信,但當會話 無法與當前連接的服務器繼續通信時,會話就可能轉移到另一個服務器 上。ZooKeeper客戶端庫透明地轉移一個會話到不同的服務器。

會話提供了順序保障,這就意味着同一個會話中的請求會以FIFO(先 進先出)順序執行。

三、開始使用ZooKeeper

下載鏈接

  • bin目錄中有啓動 ZooKeeper的腳本。以.sh結尾的腳本運行於UNIX平臺
  • 在conf目錄中保存配置文件
  • lib目錄包括Java的JAR文件,它們是運行ZooKeeper所需要的第三方文件。

1、第一個ZooKeeper會話

使用ZooKeeper發行包中bin/目錄下的zkServer和zkCli工具。

  • 變更目錄即cd到項目根目錄下,重命名配置文件:
    mv conf/zoo_sample.cfg conf/zoo.cfg
  • 雖然是可選的,最好還是把data目錄移出/tmp目錄,以防止ZooKeeper 填滿了根分區(root partition)。可以在zoo.cfg文件中修改這個目錄的位 置。
    dataDir=/users/me/zookeeper
  • 啓動服務端
    bin/zkServer.sh start
    這個服務器命令使得ZooKeeper服務器在後臺中運行。如果在前臺中運 行以便查看服務器的輸出,可以通過以下命令運行:
    bin/zkServer.sh start-foreground
  • 啓動客戶端
    bin/zkCli.sh

客戶端建立會話過程

下面1-5對應於圖片中的1-5

  • 1、客戶端啓動程序來建立一個會話。
  • 2、客戶端嘗試連接到localhost/127.0.0.1:2181。
  • 3、客戶端連接成功,服務器開始初始化這個新會話。
  • 4、會話初始化成功完成。
  • 5、服務器向客戶端發送一個SyncConnected事件。
    在這裏插入圖片描述
    在輸出的結尾,我們看到會話建立的日誌消息。第一處提到“Initiating client connection.”。消息本身說明到底發生了什麼,而額外的重要細節說 明瞭客戶端嘗試連接到客戶端發送的連接串localhost/127.0.0.1:2181中的 一個服務器。這個例子中,字符串只包含了localhost,因此指明瞭具體連 接的地址。之後我們看到關於SASL的消息,我們暫時忽略這個消息,隨後 一個確認信息說明客戶端與本地的ZooKeeper服務器建立了TCP連接。後面 的日誌信息確認了會話的建立,並告訴我們會話ID爲: 0x13b6fe376cd0000。最後客戶端庫通過SyncConncted事件通知了應用。應 用需要實現Watcher對象來處理這個事件。下一節將詳細說明事件。

例子

  • ls /:列出根(root)下的所有的znode節點
  • 創建一個名爲/workers的znode
    create /workers “”
  • 刪除名爲/workers的znode
    delete /workers
  • 關閉會話
    quit
  • ZooKeeper服務端關閉
    bin/zkServer.sh stop

2、會話的狀態即生命週期

會話的生命週期(lifetime)是指會話從創建到結束的時期,無論會話正常關閉還是因超時而導致過期。

一個會話的主要可能狀態大多是簡單明瞭的:

  • CONNECTING
  • CONNECTED
  • CLOSED
  • NOT_CONNECTED。

狀態的轉換依賴於發生 在客戶端與服務之間的各種事件(見圖2-6)。

在這裏插入圖片描述

  • 一個會話從NOT_CONNECTED狀態開始,當ZooKeeper客戶端初始化後轉換到CONNECTING狀態(圖2-6中的箭頭1)。
  • 正常情況下,成功與 ZooKeeper服務器建立連接後,會話轉換到CONNECTED狀態(箭頭2)。
  • 當客戶端與ZooKeeper服務器斷開連接或者無法收到服務器的響應時,它就會轉換回CONNECTING狀態(箭頭3)並嘗試發現其他ZooKeeper服務器。 如果可以發現另一個服務器或重連到原來的服務器,當服務器確認會話有效後,狀態又會轉換回CONNECTED狀態。
  • 否則,它將會聲明會話過期,然後轉換到CLOSED狀態(箭頭4)。應用也可以顯式地關閉會話(箭頭4 和箭頭5)。

發生網絡分區時等待CONNECTING

如果一個客戶端與服務器因超時而斷開連接,客戶端仍然保持 CONNECTING狀態。如果因網絡分區問題導致客戶端與ZooKeeper集合被 隔離而發生連接斷開,那麼其狀態將會一直保持,直到顯式地關閉這個會 話,或者分區問題修復後,客戶端能夠獲悉ZooKeeper服務器發送的會話 已經過期。發生這種行爲是因爲ZooKeeper集合對聲明會話超時負責,而 不是客戶端負責。直到客戶端獲悉ZooKeeper會話過期,否則客戶端不能 聲明自己的會話過期。然而,客戶端可以選擇關閉會話。

創建一個會話時,你需要設置會話超時這個重要的參數,這個參數設 置了ZooKeeper服務允許會話被聲明爲超時之前存在的時間。如果經過時間 t之後服務接收不到這個會話的任何消息,服務就會聲明會話過期。而在客戶端側,如果經過t/3的時間未收到任何消息,客戶端將向服務器發送心跳 消息。在經過2t/3時間後,ZooKeeper客戶端開始尋找其他的服務器,而此 時它還有t/3時間去尋找。

客戶端會嘗試連接哪一個服務器?

在仲裁模式下,客戶端有多個服務器可以連接,而在獨立模式下,客 戶端只能嘗試重新連接單個服務器。在仲裁模式中,應用需要傳遞可用的 服務器列表給客戶端,告知客戶端可以連接的服務器信息並選擇一個進行連接。

當嘗試連接到一個不同的服務器時,非常重要的是,這個服務器的 ZooKeeper狀態要與最後連接的服務器的ZooKeeper狀態保持最新。客戶端不能連接到這樣的服務器:它未發現更新而客戶端卻已經發現的更新。 ZooKeeper通過在服務中排序更新操作來決定狀態是否最新。ZooKeeper確 保每一個變化相對於所有其他已執行的更新是完全有序的。因此,如果一 個客戶端在位置i觀察到一個更新,它就不能連接到只觀察到i’<i的服務器 上。在ZooKeeper實現中,系統根據每一個更新建立的順序來分配給事務標識符。

圖2-7描述了在重連情況下事務標識符(zkid)的使用。當客戶端因超 時與s1斷開連接後,客戶端開始嘗試連接s2,但s2延遲於客戶端所知的變 化。然而,s3對這個變化的情況與客戶端保持一致,所以s3可以安全連 接。

在這裏插入圖片描述

3、ZooKeeper與仲裁模式

到目前爲止,我們一直基於獨立模式配置的服務器端。如果服務器啓動,服務就啓動了,但如果服務器故障,整個服務也因此而關閉。這非常不符合可靠的協作服務的承諾。出於可靠性,我們需要運行多個服務器。

幸運的是,我們可以在一臺機器上運行多個服務器。我們僅僅需要做 的便是配置一個更復雜的配置文件。

爲了讓服務器之間可以通信,服務器間需要一些聯繫信息。理論上, 服務器可以使用多播來發現彼此,但我們想讓ZooKeeper集合支持跨多個網絡而不是單個網絡,這樣就可以支持多個集合的情況。

爲了完成這些,我們將要使用以下配置文件:
在這裏插入圖片描述
我們主要討論最後三行對於server.n項的配置信息。其餘配置參數將會 在第10章中進行說明。
每一個server.n項指定了編號爲n的ZooKeeper服務器使用的地址和端口號。每個server.n項通過冒號分隔爲三部分,第一部分爲服務器n的IP地址或主機名(hostname),第二部分和第三部分爲TCP端口號,分別用於仲裁通信和羣首選舉。因爲我們在同一個機器上運行三個服務器進程,所以 我們需要在每一項中使用不同的端口號。通常,我們在不同的服務器上運 行每個服務器進程,因此每個服務器項的配置可以使用相同的端口號。

我們還需要分別設置data目錄,我們可以在命令行中通過以下命令來操作:
在這裏插入圖片描述
當啓動一個服務器時,我們需要知道啓動的是哪個服務器。一個服務 器通過讀取data目錄下一個名爲myid的文件來獲取服務器ID信息。
可以通 過以下命令來創建這些文件:
在這裏插入圖片描述
當服務器啓動時,服務器通過配置文件中的dataDir參數來查找data目錄的配置。它通過mydata獲得服務器ID,之後使用配置文件中server.n對應的項來設置端口並監聽。當在不同的機器上運行ZooKeeper服務器進程時, 它們可以使用相同的客戶端端口和相同的配置文件。但對於這個例子,在 一臺服務器上運行,我們需要自定義每個服務器的客戶端端口。

因此,首先使用本章之前討論的配置文件,創建z1/z1.cfg。之後通過分別改變客戶端端口號爲2182和2183,創建配置文件z2/z2.cfg和z3/z3.cfg。

現在可以啓動服務器,讓我們從z1開始:

在這裏插入圖片描述
服務器的日誌記錄爲zookeeper.out。因爲我們只啓動了三個ZooKeeper 服務器中的一個,所以整個服務還無法運行。在日誌中我們將會看到以下 形式的記錄:
在這裏插入圖片描述
這個服務器瘋狂地嘗試連接到其他服務器,然後失敗,如果我們啓動 另一個服務器,我們可以構成仲裁的法定人數:
在這裏插入圖片描述
如果我們觀察第二個服務器的日誌記錄zookeeper.out,我們將會看 到:
在這裏插入圖片描述

該日誌指出服務器2已經被選舉爲羣首。如果我們現在看看服務器1的 日誌,我們會看到:
在這裏插入圖片描述
服務器1作爲服務器2的追隨者被激活。我們現在具有了符合法定仲裁 (三分之二)的可用服務器。

在此刻服務開始可用。我們現在需要配置客戶端來連接到服務上。連 接字符串需要列出所有組成服務的服務器host:port對。對於這個例子,連 接串爲"127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"(我們包含第 三個服務器的信息,即使我們永遠不啓動它,因爲這可以說明ZooKeeper一 些有用的屬性)。

我們使用zkCli.sh來訪問集羣:

在這裏插入圖片描述
當連接到服務器後,我們會看到以下形式的消息:
在這裏插入圖片描述
注意日誌消息中的端口號,在本例中的2182。如果通過Ctrl-C來停止 客戶端並重啓多次它,我們將會看到端口號在218102182之間來回變化。我 們也許還會注意到嘗試2183端口後連接失敗的消息,之後爲成功連接到某 一個服務器端口的消息。

簡單的負載均衡

客戶端以隨機順序連接到連接串中的服務器。這樣可以用ZooKeeper 來實現一個簡單的負載均衡。不過,客戶端無法指定優先選擇的服務器來 進行連接。例如,如果我們有5個ZooKeeper服務器的一個集合,其中3個 在美國西海岸,另外兩個在美國東海岸,爲了確保客戶端只連接到本地服務器上,我們可以使在東海岸客戶端的連接串中只出現東海岸的服務器, 在西海岸客戶端的連接串中只有西海岸的服務器。

這個連接嘗試說明如何通過運行多個服務器來達到可靠性(當然,在 生產環境中,你需要在不同的主機上進行這些操作)。對於本書大部分, 包括後續幾章,我們一直以獨立模式的服務器進行開發,因爲啓動和管理 多個服務器非常簡單,實現這個例子也非常簡單。除了連接串外,客戶端 不用關心ZooKeeper服務由多少個服務器組成,這也是ZooKeeper的優點之 一。

4、實現一個原語:通過ZooKeeper實現鎖

關於ZooKeeper的功能,一個簡單的例子就是通過鎖來實現臨界區域。我們知道有很多形式的鎖(如:讀/寫鎖、全局鎖),通過ZooKeeper來實 現鎖也有多種方式。

假設有一個應用由n個進程組成,這些進程嘗試獲取一個鎖。再次強調,ZooKeeper並未直接暴露原語,因此我們使用ZooKeeper的接口來管理 znode,以此來實現鎖。爲了獲得一個鎖,每個進程p嘗試創建znode,名 爲/lock。如果進程p成功創建了znode,就表示它獲得了鎖並可以繼續執行 其臨界區域的代碼。不過一個潛在的問題是進程p可能崩潰,導致這個鎖永 遠無法釋放。在這種情況下,沒有任何其他進程可以再次獲得這個鎖,整個系統可能因死鎖而失靈。爲了避免這種情況,我們不得不在創建這個節 點時指定/lock爲臨時節點。

其他進程因znode存在而創建/lock失敗。因此,進程監聽/lock的變化, 並在檢測到/lock刪除時再次嘗試創建節點來獲得鎖。
當收到/lock刪除的通 知時,如果進程p還需要繼續獲取鎖,它就繼續嘗試創建/lock的步驟,如果 其他進程已經創建了,就繼續監聽節點。

四、一個主-從模式例子的實現

本節中我們通過zkCli工具來實現主-從示例的一些功能。這個例子僅用於教學目的,我們不推薦使用zkCli工具來搭建系統。使用zkCli的目的僅僅 是爲了說明如何通過ZooKeeper來實現協作菜譜,從而撇開在實際實現中所需的大量細節。我們將在下一章中進入實現的細節。

主-從模式的模型中包括三個角色:

  • 主節點
    主節點負責監視新的從節點和任務,分配任務給可用的從節點。
  • 從節點
    從節點會通過系統註冊自己,以確保主節點看到它們可以執行任務, 然後開始監視新任務。
  • 客戶端
    客戶端創建新任務並等待系統的響應。 現在探討這些不同的角色以及每個角色需要執行的確切步驟。

下面這張圖片是根據主從模式畫出來的,每個節點可能還有數據。

在這裏插入圖片描述

1、主節點角色

因爲只有一個進程會成爲主節點,所以一個進程成爲ZooKeeper的主節點後必須鎖定管理權。爲此,進程需要創建一個臨時znode,名爲/master:

創建臨時節點create -e /master “”

在這裏插入圖片描述
上面圖片中的1、2、3的意思分別爲:

  • 創建主節點的znode,以便獲得管理權。使用-e標誌來表示創建的 znode爲臨時性的。
  • 列出ZooKeeper樹的根。
  • 獲取/master znode的元數據和數據。

剛剛發生了什麼?首先創建一個臨時znode/master。我們在znode中添加了主機信息,以便ZooKeeper外部的其他進程需要與它通信。添加主機信息並不是必需的,但這樣做僅僅是爲了說明我們可以在需要時添加數據。 爲了設置znode爲臨時性的,需要添加-e標誌。記得,一個臨時節點會在會 話過期或關閉時自動被刪除。

現在讓我們看下我們使用兩個進程來獲得主節點角色的情況,儘管在任何時刻最多只能有一個活動的主節點,其他進程將成爲備份主節點。假如其他進程不知道已經有一個主節點被選舉出來,並嘗試創建一個/master 節點。讓我們看看會發生什麼:
在這裏插入圖片描述

監視master節點stat /master true

ZooKeeper告訴我們一個/master節點已經存在。這樣,第二個進程就知道已經存在一個主節點。然而,一個活動的主節點可能會崩潰,備份主節點需要接替活動主節點的角色。爲了檢測到這些,需要在/master節點上設置一個監視點,操作如下:
在這裏插入圖片描述
stat命令可以得到一個znode節點的屬性,並允許我們在已經存在的 znode節點上設置監視點。通過在路徑後面設置參數true來添加監視點。當 活動的主節點崩潰時,我們會觀察到以下情況:

如果監視了/master之後,如果創建臨時節點的會話關閉了,就會得到通知matser節點被刪除。
在這裏插入圖片描述
因爲備份主節點成功創建了/master節點,所以現在客戶端開始成爲活 動主節點。

2、從節點、任務和分配

在我們討論從節點和客戶端所採取的步驟之前,讓我們先創建三個重要的父znode,/workers、/tasks和/assign:

在這裏插入圖片描述
這三個新的znode爲持久性節點,且不包含任何數據。本例中,通過使用這些znode可以告訴我們哪個從節點當前有效,還告訴我們當前有任務需要分配,並向從節點分配任務。

ls /workers true也會監視/workers節點

在真實的應用中,這些znode可能由主進程在分配任務前創建,也可能由一個引導程序創建,不管這些節點是如何創建的,一旦這些節點存在了,主節點就需要監視/workers和/tasks的子節點的變化情況:

在這裏插入圖片描述
請注意,在主節點上調用stat命令前,我們使用可選的true參數調用ls命令。通過true這個參數,可以設置對應znode的子節點變化的監視點。

3、從節點角色

從節點首先要通知主節點,告知從節點可以執行任務。從節點通過 在/workers子節點下創建臨時性的znode來進行通知,並在子節點中使用主 機名來標識自己:
在這裏插入圖片描述
注意,輸出中,ZooKeeper確認znode已經創建。之前主節點已經監視了/workers的子節點變化情況。一旦從節點在/workers下創建了一個znode, 主節點就會觀察到以下通知信息:
在這裏插入圖片描述
下一步,從節點需要創建一個父znode/assign/worker1.example.com來接收任務分配,並通過第二個參數爲true的ls命令來監視這個節點的變化,以 便等待新的任務。
在這裏插入圖片描述
在這裏插入圖片描述
從節點現在已經準備就緒,可以接收任務分配。之後,我們通過討論 客戶端角色來看一下任務分配的問題。

4、客戶端角色

客戶端向系統中添加任務。在本示例中具體任務是什麼並不重要,我 們假設客戶端請求主從系統來運行cmd命令。爲了向系統添加一個任務, 客戶端執行以下操作:
在這裏插入圖片描述
我們需要按照任務添加的順序來添加znode,其本質上爲一個隊列。客 戶端現在必須等待任務執行完畢。執行任務的從節點將任務執行完畢後, 會創建一個znode來表示任務狀態。客戶端通過查看任務狀態的znode是否 創建來確定任務是否執行完畢,因此客戶端需要監視狀態znode的創建事 件:
在這裏插入圖片描述
執行任務的從節點會在/tasks/task-0000000000節點下創建狀態znode節點,所以我們需要用ls命令來監視/tasks/task-0000000000的子節點。

一旦創建任務的znode,主節點會觀察到以下事件:
在這裏插入圖片描述
主節點之後會檢查這個新的任務,獲取可用的從節點列表,之後分配 這個任務給worker1.example.com:
在這裏插入圖片描述
從節點接收到新任務分配的通知:
在這裏插入圖片描述
從節點之後便開始檢查新任務,並確認該任務是否分配給自己:
在這裏插入圖片描述
一旦從節點完成任務的執行,它就會在/tasks中添加一個狀態znode:
在這裏插入圖片描述
之後,客戶端接收到通知,並檢查執行結果:
在這裏插入圖片描述
客戶端檢查狀態znode的信息,並確認任務的執行結果。本例中,我們 看到任務成功執行,其狀態爲“done”。當然任務也可能非常複雜,甚至涉 及另一個分佈式系統。最終不管是什麼樣的任務,執行任務的機制與通過 ZooKeeper來傳遞結果,本質上都是一樣的。

五、小結

我們看到了 ZooKeeper通過其API提供的基本功能,還探討了其架構中的一些重要概念,如通過仲裁理論進行復制。此時此刻,最重要的並不是瞭解ZooKeeper 的複製協議是如何工作的,最重要的是明白仲裁理論的概念,因爲你在部 署ZooKeeper時需要指定服務器的數量。討論的另一個重要概念便是會話。 會話的語義對ZooKeeper的保障非常關鍵,因爲它們常常會涉及會話。

爲了對如何使用ZooKeeper提供初步的瞭解,我們使用zkCli工具來訪 問ZooKeeper服務器,並執行請求。我們展示了使用該工具在主-從模式例 子中的主要操作。當實現一個真實的ZooKeeper應用時,你不應該使用這個 工具;這個工具更多地用於調試和監控目的。你需要使用ZooKeeper提供的 某一語言套件。在下一章中,我們將使用Java來實現我們的例子。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章