Zookeeper 進階之——典型應用場景(二)

本文轉載自:https://www.cnblogs.com/haippy/archive/2012/07/23/2604556.html 作者:haippy 轉載請註明該聲明。

本文是前一篇博文《Zookeeper 進階之——典型應用場景(一)》的姊妹篇

閱讀指南——如何利用 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),如果屏障節點存在,屏障就會生效,下面是僞代碼:

  1. 客戶端在屏障節點上調用 ZooKeeper API  exists(),watch 設置爲 true.

  2. 如果 exists() 返回 false,屏障消失,客戶端可以推進的自己的工作。

  3. 否則, exists() 返回 true,客戶端等待屏障節點上監聽事件的到來。

  4. 如果監聽事件被觸發,客戶端重新執行 exists( ), 再一次重複上述 1-3 步,直到屏障節點被移除。

 

(雙屏障)Double Barriers

雙屏障(Double barriers)使得所有客戶端在進入和結束某一計算任務時都會得到同步。當足夠的進程processes(注:此處指節點)加入到屏障時,才啓動任務,然後當任務完成時,離開屏障區,下面的代碼段示意如何使用 Zookeeper 創建屏障節點。

僞代碼中屏障節點用 b 表示,每個客戶端進程(節點)  p 在進入屏障節點時註冊事件,然後在離開時取消註冊事件。進入屏障節點註冊事件的代碼如下表的 Enter 程序段所示, 在繼續處理任務之前,它將等待客戶端 x 進程的註冊。(此處的 x 由你針對自己的系統決定)

Enter Leave
  1. 創建名稱爲 b+“/”+p 的 Znode 節點

  2. 設置監視:exists(b + ‘‘/ready’’, true)

  3. 創建子節點:create( n, EPHEMERAL)

  4. L = getChildren(b, false)

  5. 如果L的孩子數目小於 x 的, 則等待監視事件

  6. 否則 create(b + ‘‘/ready’’, REGULAR)

  1. L = getChildren(b, false)

  2. 如果沒有任何子節點,則退出。

  3. 如果 p 是 L 中唯一節點,則 delete(n) 並退出。

  4. 如果 p 是 L 中的序號最低的節點,則等待 P 中的序號最高節點。

  5. 否則 依然存在,則 delete(n) 並繼續等待L中的最低節點。

  6. 跳轉 1

在進入屏障時,所有的進程(節點)監視一個準備好的節點(屏障節點),並創建一個臨時節點作爲屏障節點的孩子。除了最後進入屏障的節點外,每個進程(節點)都等待屏障節點,直到第 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)。

需要獲得鎖的客戶端按照以下步驟來獲取鎖:

  1. 調用 create( ),參數 pathname 爲 "_locknode_/lock-",並設置 sequence ephemeral 標誌。

  2. 在所節點(lock node)上調用 getChildren( ) ,不需要設置監視標誌。 (爲了避免“羊羣效應”).

  3. 如果在第 1 步中創建的節點的路徑具有最小的序號後綴,那麼該客戶端就獲得了鎖。

  4. 客戶端調用 exists( ) ,並在鎖目錄路徑中下一個最小序號的節點上設置監視標誌。

  5. 如果 exists( ) 返回 false,跳轉至第 2 步,否則,在跳轉至第 2 步之前等待前一部路徑上節點的通知消息。

解鎖協議非常簡單:需要釋放鎖的客戶端只需要刪除在第 1 步中創建的節點即可。

注意事項:

  • 一個節點的刪除只會導致一個客戶端被喚醒,因爲每個節點只被一個客戶端監視,這避免了羊羣效應。

  • 沒有輪詢和超時。

  • 根據你實現鎖的方式不同,不同的實現可能會帶來大量的鎖競爭,鎖中斷,調試鎖等等。

Shared Locks

在基本的鎖協議之上,你只需要做一些小的改變就可以實現共享鎖(shared locks):

獲取讀鎖: 獲取寫鎖:
  1. Call create( ) to create a node with pathname "_locknode_/read-". This is the lock node use later in the protocol. Make sure to set both the sequence and ephemeral flags.

  2. Call getChildren( ) on the lock node without setting the watch flag - this is important, as it avoids the herd effect.

  3. If there are no children with a pathname starting with "write-" and having a lower sequence number than the node created in step 1, the client has the lock and can exit the protocol.

  4. Otherwise, call exists( ), with watch flag, set on the node in lock directory with pathname staring with "write-" having the next lowest sequence number.

  5. If exists( ) returns false, goto step 2.

  6. Otherwise, wait for a notification for the pathname from the previous step before going to step 2

  1. Call create( ) to create a node with pathname "_locknode_/write-". This is the lock node spoken of later in the protocol. Make sure to set both sequence and ephemeral flags.

  2. Call getChildren( ) on the lock node without setting the watch flag - this is important, as it avoids the herd effect.

  3. If there are no children with a lower sequence number than the node created in step 1, the client has the lock and the client exits the protocol.

  4. Call exists( ), with watch flag set, on the node with the pathname that has the next lowest sequence number.

  5. If exists( ) returns false, goto step 2. Otherwise, wait for a notification for the pathname from the previous step before going to step 2.

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(n²),另外一個是僅通過臨時節點不能判斷某些參與方是否失效,爲了利用臨時節點檢測參與方是否失效,必須參與方創建該節點。

爲了解決第一個問題,你可以將系統設置成只有一個協調者可以收到事務節點狀態的變化,一旦協調者達成意見後通知其他參與方, 該方法可擴展性較強,但是速度很慢,因爲所有的通信都指向協調者。

爲了解決第二個問題,你可以讓參與方把事務傳播到參與方,並讓每個參與方創建自己的臨時節點。

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而言:

  1. 創建 Znode 節點 z,路徑名稱爲"ELECTION/n_"並設置 SEQUENCE 和 EPHEMERAL 標誌。

  2. 假設 C 是"ELECTION"的子節點集合,  i 是 z 節點的序號。

  3. 監視節點 "ELECTION/n_j" 的改變,j 是滿足 j < i 最小的序號,n_j 是 C 節點集合中的某個節點。

當收到 Znode 節點刪除的通知時:

  1. 假設 C 是 “ELECTION” 新的子節點集合。

  2. 如果 z 是 C 中的最小節點,則執行 Leader 選舉流程。

  3. 否則,監視節點 "ELECTION/n_j" 的改變,j 是滿足 j < i 最小的序號,n_j 是 C 節點集合中的某個節點。

注意,在子節點列表中沒有先遣節點的 Znode 並不意味着該節點的創建者知道它就是當前的Leader,應用程序可能需要考慮創建一個單獨的 Znode 來確認該 Leader 已經執行了選舉流程。

發佈了0 篇原創文章 · 獲贊 170 · 訪問量 155萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章