?
閱讀指南——如何利用 Zookeeper 構建上層應用?
本文將帶你如何利用 Zookeeper 實現某些分布式應用所必需的高級功能。所有功能均可以在客戶端按固定的模式實現,不需要 Zookeeper 的特殊支持,也希望 Zookeeper 社區能將這些具有固定實現模式的功能集成到 Zookeeper 客戶端的程序庫中,可以簡化 Zookeeper 的使用并且還能使某些功能的實現標準化。
即便 Zookeeper 本身使用異步通知( asynchronous ?notifications),但卻可以基于此構建同步的( synchronous )一致性原語,如隊列和鎖。你將看到 Zookeeper 實現這些功能是完全可能的,因為 Zookeeper 提供了強制的全序更新,并對外提供了保序接口。?
注意下面的程序段試圖采取最佳的編程實踐,尤其是避免使用輪詢(polling),定時器(timers)和其他任何可能造成“羊群效應(herd effect)”機制(“羊群效應”一般會帶來網絡流量的突增,限制系統的可擴展性)。
除了本文所列舉的功能,我們還可以想象出其他很多實用的功能,比如可撤銷的讀寫優先鎖。本文提到的某些構建方式——比如鎖,比較詳細的闡述了使用 Zookepper 的關鍵點,其實你可以找到其他的例子,如事件處理和隊列,但是,本節中的例子只是模擬相關的功能,在具體實踐中需要考慮其他方面的因素。
開箱即用的應用示例:命名服務,配置管理,組關系管理
命名服務和配置管理是 Zookeeper 提供的最基本的應用,這兩個功能可以直接用 Zookeeper 提供的 API 實現。
另外一個可以直接使用的功能是組關系管理,組在 Zookeeper 由一個 Znode 表示,組中的某個成員可以用組節點下的臨時節點(Ephemeral Nodes)表示,當 Zookeeper 檢測到節點故障時,節點成員中不正常的節點將會被自動地移除。
屏障(Barriers)
分布式系統使用屏障( barriers )來阻塞某一節點集的任務,直到滿足特定的條件該節點集的所有節點才能正常向前推進,就像屏障一樣,在當條件不滿足時,屏障阻塞了任務的執行,只有當條件滿足后屏障才會被拆除,各節點才能推進自己正在執行的任務。Zookeeper 中實現屏障時指定一個屏障節點(barrier node),如果屏障節點存在,屏障就會生效,下面是偽代碼:
-
客戶端在屏障節點上調用 ZooKeeper API?? exists(), watch 設置為 true .
-
如果? exists() ?返回 false,屏障消失,客戶端可以推進的自己的工作。
-
否則,? exists() ?返回 true,客戶端等待屏障節點上監聽事件的到來。
-
如果監聽事件被觸發,客戶端重新執行? exists( ), ?再一次重復上述 1-3 步,直到屏障節點被移除。
?
(雙屏障)Double Barriers
雙屏障(Double barriers)使得所有客戶端在進入和結束某一計算任務時都會得到同步。當足夠的進程processes(注:此處指節點)加入到屏障時,才啟動任務,然后當任務完成時,離開屏障區,下面的代碼段示意如何使用 Zookeeper 創建屏障節點。
偽代碼中屏障節點用? b ?表示,每個客戶端進程(節點)?? p ?在進入屏障節點時注冊事件,然后在離開時取消注冊事件。進入屏障節點注冊事件的代碼如下表的? Enter 程序段所示, ?在繼續處理任務之前,它將等待客戶端? x ?進程的注冊。(此處的? x ?由你針對自己的系統決定)
Enter | Leave |
|
|
在進入屏障時,所有的進程(節點)監視一個準備好的節點(屏障節點),并創建一個臨時節點作為屏障節點的孩子。除了最后進入屏障的節點外,每個進程(節點)都等待屏障節點,直到第 5 行的條件出現。該進程(節點)創建第 x 個節點——即最后的進程(節點),它將會看到 x 個節點,并喚醒其他進程(節點),注意,所有的等待進程(節點)只是在退出的時候被喚醒,所以等待還是很高效的。
在退出屏障時,你不能設置 諸如? ready ?的標志,因為你在等待進程節點退出,通過使用臨時節點,進入屏障后失效的進程節點并不會阻止其他運行正確的節點完成任務。當進程節點準備推出屏障區時,它必須刪除它的進程節點,并等待其他進程刪除各自的進程節點。
當 b 沒有的進程子節點時,進程(節點)就會退出屏障區。然而,為了效率起見,你可以使用序號最低的進程節點作為 ready 標志。所有其他準備退出屏障區的進程(節點)都監視序號最低的將要退出進程(節點)消失,序號最低的進程節點的擁有者則就等待其他任何一個節點的消失(選擇序號最高進程節點)。這意味著除了最后的一個進程節點外,其他的每個進程節點被刪除時只要喚醒一個進程節點即可,當它被刪除時就會喚醒其他的進程節點。
隊列(Queues)
分布式隊列是通用的數據結構,為了在 Zookeeper 中實現分布式隊列,首先需要指定一個 Znode 節點作為隊列節點(queue node), 各個分布式客戶端通過調用 create() 函數向隊列中放入數據,調用create()時節點路徑名帶"queue-"結尾,并設置順序和臨時( sequence ?and ephemeral )節點標志。 由于設置了節點的順序標志,新的路徑名具有以下字符串模式:"_path-to-queue-node_/queue-X",X 是唯一自增號。需要從隊列中移除數據的客戶端首先調用? getChildren( ) ?函數,同時在隊列節點(queue node)上將? watch ?設置為 true,并處理最小序號的節點(即從序號最小的節點中取數據)??蛻舳瞬恍枰僖淮握{用? getChildren( ), 隊列中的數據獲取完。如果隊列節點中沒有任何子節點,讀取隊列的客戶端需要等待隊列的監視事件通知。
Priority Queues
為了實現優先隊列,你在普通隊列上只需要簡單的改變兩處地方,首先,在某一元素被加入隊列時,路徑名以 "queue-YY" 結尾,YY 表示優先級,YY越小優先級越高,其次,從隊列中移除一個元素時,客戶端需要使用最新的孩子節點列表,這意味著如果隊列節點上監視通知被觸發,客戶端需要讓先前獲取的孩子節點列表無效。
鎖(Locks)
完全分布式鎖是全局同步的,這意味著在任何時刻沒有兩個客戶端會同時認為它們都擁有相同的鎖,使用 Zookeeper 可以實現分布式鎖,和優先隊列一樣,我們需要首先定義一個鎖節點(lock node)。
需要獲得鎖的客戶端按照以下步驟來獲取鎖:
-
調用? create( ), 參數 pathname 為 "_locknode_/lock-",并設置? sequence? 和? ephemeral ?標志。
-
在所節點(lock node)上調用? getChildren( ) ?,不需要設置監視標志。 (為了避免“羊群效應”).
-
如果在第 1 步中創建的節點的路徑具有最小的序號后綴,那么該客戶端就獲得了鎖。
-
客戶端調用? exists( ) ?,并在鎖目錄路徑中下一個最小序號的節點上設置監視標志。
-
如果? exists( ) ?返回 false,跳轉至第 2 步,否則,在跳轉至第 2 步之前等待前一部路徑上節點的通知消息。
解鎖協議非常簡單:需要釋放鎖的客戶端只需要刪除在第 1 步中創建的節點即可。
注意事項:
-
一個節點的刪除只會導致一個客戶端被喚醒,因為每個節點只被一個客戶端監視,這避免了羊群效應。
-
沒有輪詢和超時。
-
根據你實現鎖的方式不同,不同的實現可能會帶來大量的鎖競爭,鎖中斷,調試鎖等等。
Shared Locks
在基本的鎖協議之上,你只需要做一些小的改變就可以實現共享鎖(shared locks):
獲取讀鎖: | 獲取寫鎖: |
|
|
Recoverable Shared Locks
對共享鎖做一些細小的改變,我們就可以使共享鎖變成可撤銷的共享鎖:
在第 1 步,在獲取讀者和寫者的鎖協議中,在調用? create( ) 后, 立即調用 getData( ) ,并設置監視。如果客戶端稍后收到了它在第一步創建節點的通知,它會再一次在該節點上調用? getData( ) ,并設置監視,查找 “unlock” 串。該信號會通知客戶端必須釋放鎖。這是因為,依據共享鎖協議,你可以通過在鎖節點(lock node)上調用 setData()(將“unlock”寫入該節點) 請求擁有該鎖的客戶端放棄該鎖 。
注意該協議要求鎖的擁有者也同意釋放該鎖,該協定非常重要,尤其是鎖的擁有者需要在釋放該鎖前做一些處理。 當然,你也可以通過約定“撤銷者可以在鎖的擁有者一段時間沒有刪除該鎖的情況下刪除該鎖節點”來實現可撤銷的共享鎖。
兩階段提交(Two-phased Commit)
兩階段提交協議可以讓分布式系統的所有客戶端決定究竟提交某一事務或還是終止該事務。
在 Zookeeper 中,你可以讓協調者(coordinator)創建事務節點,比如,"/app/Tx",從而實現一個兩階段提交協議。 當協調者(coordinator)創建了子節點時,子節點內容是未定義的,由于每個事務參與方都會從協調者接收事務,參與方讀取每個子節點并設置監視。然后每個參與方通過向與自身相關的 Znode 節點寫入數據來投票“提交(commit)”或“中止(abort)”事務。一旦寫入完成,其他的參與方會被通知到,當所有的參與方都投完票后,協調者就可以決定究竟是“提交(commit)”或“中止(abort)”事務。注意,如果某些參與方投票“中止”,節點是可以決定提前“中止”事務的。
該實現方法有趣的地方在于協調者的唯一作用是決定參與方的組(the group of sites),創建 Zookeeper 節點, 將事務傳播到相應的參與方,實際上,Zookeeper 可以通過將消息寫入事務節點來傳播事務。
上述討論的方法存在兩個明顯的缺點,一是消息的復雜性,復雜度為 O(n2),另外一個是僅通過臨時節點不能判斷某些參與方是否失效,為了利用臨時節點檢測參與方是否失效,必須參與方創建該節點。
為了解決第一個問題,你可以將系統設置成只有一個協調者可以收到事務節點狀態的變化,一旦協調者達成意見后通知其他參與方, 該方法可擴展性較強,但是速度很慢,因為所有的通信都指向協調者。
為了解決第二個問題,你可以讓參與方把事務傳播到參與方,并讓每個參與方創建自己的臨時節點。
Leader 選舉(Leader Election)
Zookeeper 實現 Leader 選舉簡單做法是在創建代表 “proposals” 客戶端的 Znode 節點時設置? SEQUENCE|EPHEMERAL ?標志?;鞠敕ㄊ莿摻ㄒ粋€節點,比如 "/election",然后在創建子節點時"/election/n_"設置標志 SEQUENCE|EPHEMERAL. 當設置順序節點 SEQUENCE 標志時,Zookeeper 會在 "/election" 子節點的創建過程中自增子節點名稱后綴的序號,最小后綴序號的 Znode 節點表示Leader。
然而,還沒完,監視 Leader 失效也是非常重要的,當前的 Leader 失效后需要一個新的客戶端起來接替舊的 Leader 的位置。一個簡單的方式是讓所有的應用進程監視當前序號最小的 Znode 節點, 并在當前 序號最小的 Znode 節點失效是檢查他們是否為新的 Leader(注意當前序號最小的節點可能會隨著 Leader 的消失而消失,他們可能是該Leader 節點的臨時子節點). 但是這會導致'羊群效應(herd effect)":在當前 Leader 失效后,其他所有的進程(節點)將會收到通知,并在 "/election" 節點上執行 getChildren()來獲取"/election"節點的子節點列表,如果客戶端數目很大,它會使得Zookeeper服務器處理的操作次數急劇上升。為了避免羊群效應,客戶端只需要監視 Znode 節點中的下一個節點就足夠。如果某個客戶端收到了它正在監視的節點消失的通知,它將成為新的 Leader,因為此時沒有其它的 Znode 節點的序號比它小。所以這就避免了羊群效應,并且客戶端也沒有必要監視同一個最小的 Znode 節點。
以下是偽代碼:
假設 ELECTION 成為Leader 選舉應用的路徑,對于想要成為 Leader 的 Volunteer而言:
-
創建 Znode 節點 z,路徑名稱為"ELECTION/n_"并設置 SEQUENCE 和 EPHEMERAL 標志。
-
假設 C 是"ELECTION"的子節點集合,? i 是 z 節點的序號。
-
監視節點 "ELECTION/n_j" 的改變,j 是滿足 j < i 最小的序號,n_j 是 C 節點集合中的某個節點。
當收到 Znode 節點刪除的通知時:
-
假設 C 是 “ELECTION” 新的子節點集合。
-
如果 z 是 C 中的最小節點,則執行 Leader 選舉流程。
-
否則,監視節點 "ELECTION/n_j" 的改變,j 是滿足 j < i 最小的序號,n_j 是 C 節點集合中的某個節點。
注意,在子節點列表中沒有先遣節點的 Znode 并不意味著該節點的創建者知道它就是當前的Leader,應用程序可能需要考慮創建一個單獨的 Znode 來確認該 Leader 已經執行了選舉流程。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
