深入ZooKeeper——獨立模式實踐、仲裁模式解析
開始使用Zookeeper
開始之前,需要下載zookeeper發行包。
我通過重新配置網路,然後通過yum install wget
獲取wget命令,使用wget http://....zookeeper-3.4.10.tar.gz
下載發行包,再用tar -xvzf zookeeper-3.4.10.tar.gz
解壓。由於我對linux忘記了大半,所以花了2小時。
在發行包(distribution)的目錄中,你會發現在bin目錄中有啟動ZooKeeper的指令碼。以.sh結尾的指令碼運行於UNIX平臺(Linux、Mac OSX等),以.cmd結尾的指令碼則用於Windows。
lib目錄包括Java的JAR檔案,它們是執行ZooKeeper所需要的第三方檔案。稍後我們需要引用ZooKeeper解壓縮的目錄。我們以{PATH_TO_ZK}方式來引用該目錄。
第一個ZooKeeper會話
首先我們以獨立模式執行ZooKeeper並建立一個會話。(使用ZooKeeper發行包中bin/目錄下的zkServer和zkCli工具)
假設你已經下載並解壓了ZooKeeper發行包,進入shell,變更目錄(cd)到專案根目錄下,重新命名配置檔案:
雖然是可選的,最好還是把data目錄移出/tmp目錄,以防止ZooKeeper填滿了根分割槽(root partition)。可以在zoo.cfg檔案中修改這個目錄的位置。
通過vi zoo.cfg
:修改為dataDir=/users/me/zookeeper
最後,為了啟動伺服器,執行以下命令:
當然,這個伺服器命令使得ZooKeeper伺服器在後臺中執行。如果在前臺中執行以便檢視伺服器的輸出,可以通過以下命令執行:
./zkServer.sh start-foreground
這個選項提供了大量詳細資訊的輸出,以便允許檢視伺服器發生了什麼。
現在我們準備啟動客戶端。在另一個shell中進入專案根目錄,執行以下命令:
./zkCli.sh
為了更加了解ZooKeeper,讓我們列出根(root)下的所有znode,然後建立一個znode。首先我們要確認此刻znode樹為空,除了節點/zookeeper之外,該節點內標記了ZooKeeper服務所需的元資料樹。
現在發生了什麼?我們執行ls/後看到這裡只有/zookeeper節點。現在我們建立一個名為/workers的znode,確保如下所示:
當建立/workers節點後,我們指定了一個空字串(""),說明我
們此刻不希望在這個znode中儲存資料。然而,該介面中的這個引數可
以使我們儲存任何字串到ZooKeeper的節點中。比如,可以替
換"“為"workers”。
為了完成這個練習,刪除znode,然後退出:
觀察到znode/workers已經被刪除,並且會話現在也關閉。為了完成最後的清理,退出ZooKeeper伺服器:
會話的狀態和宣告週期
會話的生命週期(lifetime)是指會話從建立到結束的時期,無論會話正常關閉還是因超時而導致過期。
為了討論在會話中發生了什麼,我們需要考慮會話可能的狀態,以及可能導致會話狀態改變的事件。
一個會話的主要可能狀態大多是簡單明瞭的:CONNECTING、CONNECTED、CLOSED和NOT_CONNECTED。
一個會話從NOT_CONNECTED狀態開始,當ZooKeeper客戶端初始化後轉換到CONNECTING狀態(圖2-6中的箭頭1)。正常情況下,成功與ZooKeeper伺服器建立連線後,會話轉換到CONNECTED狀態(箭頭2)。當客戶端與ZooKeeper伺服器斷開連線或者無法收到伺服器的響應時,它就會轉換回CONNECTING狀態(箭頭3)並嘗試發現其他ZooKeeper伺服器。
如果可以發現另一個伺服器或重連到原來的伺服器,當伺服器確認會話有效後,狀態又會轉換回CONNECTED狀態。否則,它將會宣告會話過期,然後轉換到CLOSED狀態(箭頭4)。應用也可以顯式地關閉會話(箭頭4和箭頭5)。
建立一個會話時,你需要設定會話超時這個重要的引數,這個引數設定了ZooKeeper服務允許會話被宣告為超時之前存在的時間。如果經過時間t之後服務接收不到這個會話的任何訊息,服務就會宣告會話過期。而在客戶端側,如果經過t/3的時間未收到任何訊息,客戶端將向伺服器傳送心跳訊息。在經過2t/3時間後,ZooKeeper客戶端開始尋找其他的伺服器,而此時它還有t/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的功能,一個簡單的例子就是通過鎖來實現臨界區域。我們知道有很多形式的鎖(如:讀/寫鎖、全域性鎖),通過ZooKeeper來實現鎖也有多種方式。
假設有一個應用由n個程序組成,這些程序嘗試獲取一個鎖。再次強調,ZooKeeper並未直接暴露原語,因此我們使用ZooKeeper的介面來管理znode,以此來實現鎖。
為了獲得一個鎖,每個程序p嘗試建立znode,名為/lock。如果程序p成功建立了znode,就表示它獲得了鎖並可以繼續執行其臨界區域的程式碼。不過一個潛在的問題是程序p可能崩潰,導致這個鎖永遠無法釋放。為了避免這種情況,我們不得不在建立這個節點時指定/lock為臨時節點。
一個主-從模式例子的實現
本節中我們通過zkCli工具來實現主-從示例的一些功能。這個例子僅用於教學目的,我們不推薦使用zkCli工具來搭建系統。
主-從模式的模型中包括三個角色:
- 主節點
- 從節點
- 客戶端
主節點角色
因為只有一個程序會成為主節點,所以一個程序成為ZooKeeper的主節點後必須鎖定管理權。為此,程序需要建立一個臨時znode,名為/master:
①建立主節點的znode,以便獲得管理權。使用-e
標誌來表示建立的znode為臨時性的。
②列出ZooKeeper樹的根。
③獲取/master znode的元資料和資料。
現在讓我們看下我們使用兩個程序來獲得主節點角色的情況,儘管在任何時刻最多隻能有一個活動的主節點,其他程序將成為備份主節點。
假如其他程序不知道已經有一個主節點被選舉出來,並嘗試建立一個/master節點。讓我們看看會發生什麼:
ZooKeeper告訴我們一個/master節點已經存在。這樣,第二個程序就知道已經存在一個主節點。然而,一個活動的主節點可能會崩潰,備份主節點需要接替活動主節點的角色。
為了檢測到這些,需要在/master節點上設定一個監視點,操作如下:
stat命令可以得到一個znode節點的屬性,並允許我們在已經存在的znode節點上設定監視點。通過在路徑後面設定引數true來新增監視點。當活動的主節點崩潰時,我們會觀察到以下情況:
在輸出的最後,我們注意到NodeDeleted事件。這個事件指出活動主節點的會話已經關閉或過期。同時注意,/master節點已經不存在了。現在備份主節點通過再次建立/master節點來成為活動主節點。
從節點、任務和分配
在我們討論從節點和客戶端所採取的步驟之前,讓我們先建立三個重要的父znode,/workers、/tasks和/assign:
這三個新的znode為永續性節點,且不包含任何資料。通過使用這些znode可以告訴我們哪個從節點當前有效,還告訴我們當前有任務需要分配,並向從節點分配任務。
在真實的應用中,這些znode可能由主程序在分配任務前建立,也可能由一個載入程式建立,不管這些節點是如何建立的,一旦這些節點存在了,主節點就需要監視/workers和/tasks的子節點的變化情況:
從節點角色
從節點首先要通知主節點,告知從節點可以執行任務。從節點通過在/workers子節點下建立臨時性的znode來進行通知,並在子節點中使用主機名來標識自己:
注意,輸出中,ZooKeeper確認znode已經建立。之前主節點已經監視了/workers的子節點變化情況。一旦從節點在/workers下建立了一個znode,主節點就會觀察到以下通知資訊:
下一步,從節點需要建立一個znode/assign/worker1.example.com來接收任務分配,並通過第二個引數為true的ls命令來監視這個節點的變化,以便等待新的任務。
從節點現在已經準備就緒,可以接收任務分配。之後,我們通過討論客戶端角色來看一下任務分配的問題。
客戶端角色
客戶端向系統中新增任務。
我們假設客戶端請求主從系統來執行cmd命令。為了向系統新增一個任務,客戶端執行以下操作:
我們需要按照任務新增的順序來新增znode,其本質上為一個佇列。客戶端現在必須等待任務執行完畢。
執行任務的從節點將任務執行完畢後,會建立一個znode來表示任務狀態。客戶端通過檢視任務狀態的znode是否建立來確定任務是否執行完畢,因此客戶端需要監視狀態znode的建立事件:
執行任務的從節點會在/tasks/task-0000000000節點下建立狀態znode節點,所以我們需要用ls命令來監視/tasks/task-0000000000的子節點。
一旦建立任務的znode,主節點會觀察到以下事件:
主節點之後會檢查這個新的任務,獲取可用的從節點列表,之後分配這個任務給worker1.example.com:
從節點接收到新任務分配的通知:
從節點之後便開始檢查新任務,並確認該任務是否分配給自己:
一旦從節點完成任務的執行,它就會在/tasks中新增一個狀態znode:
之後,客戶端接收到通知,並檢查執行結果:
客戶端檢查狀態znode的資訊,並確認任務的執行結果。本例中,我們看到任務成功執行,其狀態為“done”。當然任務也可能非常複雜,甚至涉及另一個分散式系統。最終不管是什麼樣的任務,執行任務的機制與通過ZooKeeper來傳遞結果,本質上都是一樣的。