ZooKeeper官方文檔—第三章 4.常見問題和解決方案

ZooKeeper常見問題和解決方案

構建Zookeeper高級設計指南

在本文中,你可以發現使用Zookeeper實現高級功能的解決方案. 這些都只需要在客戶端支持實現,不需要Zookeeper做額外的支持. 希望社區開發者遵循這些客戶端約定來提高易用性和標準化.

Zookeeper有趣事情之一是它使用異步消息通知,你可以使用這個功能區構建同步一致性元件,例如隊列和鎖. 正如你說看到的那樣,它能夠做到這一點,因爲Zookeeper會使Updates操作保持整體的順序性,並有機制來公開這個順序.

注意下面的內容,這是嘗試的最佳實戰. 通常,我們要避免輪訓和定時查詢或者其他任何可能引起羊羣效應的操作,這會導致流量爆發和限制可伸縮性.

有許多有用的功能不包含在這裏,例如— 可撤銷的讀寫優先鎖,僅舉這一個例子. 這裏提到一些結構,在這裏 - 鎖,尤其是 -這裏只是說明一些問題, 當日你也可能發現一些其他的接口,例如:事件處理或者隊列, 執行相同操作但更優秀的設計. 總之,這個例子主要是爲了給你開腦洞的.

開箱即用的應用:命名服務、配置服務、組成成員(Out of the Box Applications: Name Service, Configuration, Group Membership)

命名服務和配置是Zookeeper的兩個重要應用. 這兩個功能由Zookeeper API提供直接的支持.

同時Zookeeper也直接支持另一個功能group membership. 這個組由一個節點來表示. 組成員在組節點下創建臨時節點. 當Zookeeper發生錯誤時,成員節點會自動刪除.

阻塞

分佈式系統使用阻塞去阻塞一系列節點的進程,當一個條件被觸發,所有節點會同時進行數據處理. 阻塞的實現是基於Zookeeper中設置一個阻塞節點. 如果這個阻塞節點已經存在,說明可以實現阻塞. 這是僞代碼:

  1. 客戶單調用Zookeeper API的exists()方法來驗證阻塞節點是否存在,並設置監聽者爲true.

  2. 如果exists() 返回false, 則阻塞消失,客戶端繼續.

  3. 否則,如果exists() 返回true, 客戶端會等待關於該阻塞節點的Zookeeper監聽者事件.

  4. 當客戶單事件觸發,客戶端會重新調用 exists( ), 如果阻塞節點仍然存在會一直阻塞值該節點刪除.

雙重阻塞

雙重阻塞可以同步計算的開始和結束. 當足夠的進程進入到阻塞中, 進程開始他們的計算,當完成計算後會調用同時離開阻塞. 這個例子顯示瞭如何使用Zookeeper節點做阻塞.

這段僞代碼中,阻塞節點表示爲b. 每個客戶端p當進入阻塞時註冊,離開時會註銷該阻塞節點. 註冊阻塞節點需要通過下面Enter , 它會在開始運算之前一直等待x個客戶端進程註冊. (這裏的x由你的系統來決定)

Enter Leave
  1. 創建一個名爲n的字符串: n = b+“/”+p

  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中處理節點,則刪除(n)並退出

  4. 如果p是L中最小處理節點,則等待L中的最高節點.

  5. 如果存在則delete(n) 繼續等待中最小的節點

  6. 跳轉至 1

進入時, 所有進程都會設置ready節點的觀察者,創建一個臨時節點作爲阻塞節點的孩子節點. 除了最後一個節點,其餘的進程會進入阻塞等待ready節點(在第五行). 進程創建低x個節點,最後的進程會檢查孩子節點(這時會大於x),並創建ready節點, 喚醒其他進程. 注意等待節點喚醒,僅當它被喚醒的那刻,所以線程等待是高效的.

在退出時,你不能使用使用flag例如ready,因爲你正在檢測進程節點離開. 使用臨時節點,在阻塞過後失敗的節點已經進入不阻塞的正確進程的完成. 當進程進程準備好離開,他們需要刪除他們的進程節點,等待其他進程做同樣的事情.

當b的進程節點不存在時,進程退出. 然而,爲了高效,你可以使用最小進程節點最爲ready flag. 所有其他準備離開的節點檢查最低節點的消失,並且最低節點的擁有者檢測其他進程節點(爲了簡單而選擇最高). 這就意味着,除了最後一個節點,每個節點刪除僅僅有一個單獨的進程被喚醒. 當最後一個刪除時喚醒每一個節點.

隊列

分佈式隊列是一個常用的數據結構. 在Zookeeper中實現一個分佈式隊列,首先定義一個節點去表示隊列,也就是隊列節點. 分佈式客戶端通過調用create()以"queue-"+序列的形式放入隊列中,並且在調用create時,設置序列和臨時標記爲true.因爲序列標記爲ture, 新的路徑是這樣_path-to-queue-node_/queue-X, X是單調遞增序列. 一個客戶端想要刪除隊列,需要調用Zookeeper的getChildren( ) 方法, 隊列節點的監聽者設置爲true, 開始處理最小序列的節點. 這個節點不需要調用另外一個 getChildren( ) 直到它耗盡來自第一個getChildren( ) 的整個集合. 如果這個隊列節點沒有子節點,處理進程會等待一個監聽者消息後再次會再次檢查隊列.

注意

現在在Zookeeper目錄中存在隊列的實現. 這是發行版發佈的 -- 在src/recipes/queue目錄.

優先隊列

實現一個優先隊列,  你僅需要對 queue recipe做兩個簡單的改變. 首先,添加一個隊列,"queue-YY"是路徑名稱的結尾,YY是指元素的優先級,數字越低表示優先級越高(僅指UNIX). 第二點,當從隊列移除時,如果出發了額監聽者關於隊列節點的通知,客戶端會放棄之前獲取的孩子列表,而使用最新的孩子列表.

完整的分佈式鎖是全局同步的,意味着在同一時間兩個客戶端不會持有同一個鎖. 這些可以使用Zookeeper實現. 就像優先隊列,首先定義一個鎖節點.

在Zookeeper目錄中鎖的實現. 在發行發佈版中 -- src/recipes/lock 發佈的目錄.

客戶端希望獲得一個鎖要做以下這些事情:

  1. 調用 create( ) 方法,參數是"_locknode_/lock-"的路徑名和自增序列和臨時節點標記.

  2. 在Lock節點上調用 getChildren( ) 而不設置監聽者標記(這最終要的是避開羊羣效應).

  3. 如果第一步創建的路徑名有最小序列數後綴,客戶端獲得鎖並離開協議.

  4. 客戶端在鎖目錄的下一個最小序列值得路徑上使用 exists( ) 設置監聽者標記.

  5. 如果 exists( ) 返回false, 就進入第二部 2. 否則, 在進入第二部之前等待該路徑的通知.

解鎖協議十分簡單: 客戶端希望釋放鎖只需要簡單的刪除第一步創建的節點.

這裏有些事情需要注意:

  • 刪除這個節點只會導致你喚醒精確的每個客戶端的監聽者. 通過這種方式,你避免了羊羣效應.

  • 沒有輪序和超時.

  • 因爲你實現鎖的方式,非常容易發生鎖競爭的成員、打破鎖、調試鎖問題等.

共享鎖

你在鎖定協議上做一點改變就可以實現共享鎖:

獲得一個讀取鎖: 獲得一個寫鎖:
  1. 調用 create( ) 來創建一個以“_locknode_/read-”命名的節點. 這是稍後再協議中使用的鎖節點. 確保設置序列和臨時節點標記.

  2. 在鎖節點調用 getChildren( ) 而不設置監聽標記—這點很重要,它可以避免羊羣效應.

  3. 如果沒有“write-”開頭的節點,並且有一個比第一步創建節點更小的序列號,客戶端獲得鎖並且推出協議.

  4. 否則,調用exists( ), 設置監聽者標記,  設置在鎖目錄下以"write-"開頭的路徑的一下一個最小序列.

  5. 如果 exists( ) 返回 false, 跳轉至步驟 2.

  6. 否則, 在跳轉到第二部之前,等待上一步路徑名的通知.

  1. 調用create()創建一個"_locknode_/write-"路徑的節點. 這是協議後面提到的鎖節點. 確保設置序列和臨時節點標記.

  2. 在鎖節點調用 getChildren( ) 但不設置監聽者標記 - 這很重要,可以避免羊羣效應.

  3. 如果不存在比第一步創建節點小的節點,獲得客戶端鎖和客戶端退出協議.

  4. 調用 exists( ), 設置監聽者,設置下一個在該路徑下一個更小序列的節點上.

  5. 如果存在 exists( ) 返回false, 跳轉至步驟 2. 否則,在跳轉到第二步之前,等待該節點前一步的消息通知.

注意

這個操作可能造成羊羣效應:當大量客戶端在等待讀鎖時,當最小序列的"write-"節點被刪除客戶端都會或多或少的獲得消息. 實際上,這個是有效行爲: 所有等待讀的客戶端都應該釋放,因爲他們存在鎖. 羊羣效應實際上是指發佈一個"herd",只有一個或少數客戶端可以進行下去.

可恢復的共享鎖

對共享鎖進行小的修改,通過修改你可以使你的共享鎖可以恢復:

在獲取讀和寫鎖協議的第一步,調用 getData( ) 來設置監聽,之後立刻調用create( ). 如果客戶端隨後接收到第一步節點創建的消息通知,會調用在該節點另一個getData( ), 設置監聽者並檢查字符串"unlock", 發送給客戶端信息,客戶端必須釋放鎖. 這是因爲,根據這個共享鎖協議,可以在這個及節點通過客戶端調用setData()要求其放棄鎖, 只需要往節點中寫入"unlock".

注意這個協議需要鎖的持有者同意釋放鎖. 這個同意是十分重要的,特別是這個鎖的持有者需要在釋放鎖之前做一些操作.  當然你可以一直實現可恢復的共享鎖通過在協議裏規定允許撤銷者刪除lock節點如果lock持有者在一定的時間之後還沒有刪除lock。

兩階段提交

兩階段提交協議是一個在分佈式系統中讓所有客戶端系統可以提交和回滾事務的算法.

在Zookeeper中,你可以實現兩階段提交,通過一個協調者創建一個事務節點,例如 “/app/Tx”,每個參與者叫做"/app/Tx/s_i". 當協調者創建孩子節點時,它讓內容定義. 一旦與事務有關的每個點從協調者哪裏接收到事務,參與者讀取每個孩子節點並設置監聽者. 然後每個參與者處理查詢和投票選擇是"commit"或者"abort"然後寫入各自對應的節點. 一旦寫入完成,其他的節點接收到通知,並且不久後所有參與者都會擁有所有投票,他們可以決定是執行"aboort"或者"coomit". 注意如果有些節點投票了“abort”,那麼一個節點可以提早決定"abort".

有趣的是,這個實現中,協調者的角色僅僅是決定參與者,然後創建Zookeeper節點,並將事務傳播到相應的參與者. 事實上,可以可以通過吸入事務節點來傳播事務.

上面的描述有兩個重要的缺陷. 一個是消息的複雜性是O(n²). 第二個是通過臨時節點檢測參與者的錯誤是不可能的. 使用臨時節點的參與者發現錯誤是必要,它需要能夠創建這個節點.

解決第一個問題,你可以僅接受改變的事務節點的通知,然後通知參與者一旦參與者下決定. 注意這個方法是可擴展的,但是它很慢, 因爲它要求所有通知通告協調者.

解決第二個問題, 你可以讓協調者傳播事務到參與者,並且每個參與者創建自己的臨時節點.

領導者選舉

使用Zookeeper做領導人選舉的簡單方法是創建代表客戶端的"proposals"節點時設置SEQUENCE|EPHEMERAL 標記. 方法是有一個叫做"/election"的節點,然後每個每個節點創建一個叫"/election/n"的子節點,並加上標籤SEQUENCE|EPHEMERAL. 關於sequence標記, ZooKeeper自動在"/election"的孩子節點後追加一個更大的數字.  創建的節點中最小序列的作爲領導者.

不過,這不是全部. 重要的是檢測領導者異常,然後當領導者異常時選舉一個新的領導者. 一個簡單的方案是讓所有應用進程監控最小的節點,當最小節點離線時檢測它們是否是最小的節點.(注意如果領導者發生異常最小節點將會消失,因爲這個節點是臨時節點). 但是可能會導致羊羣效應:當領導者異常後,所有進程會收到一個通知,在"/election"節點調用getChildren獲得"/election"節點當前的孩子節點列表,它會導致Zookeeper必須處理的操作數量激增.  爲了避開羊羣效應, 在znodes序列中監視下一個節點就足夠了. 如果客戶端接收到一個通知,它正在監聽的節點已經消失,接着在此刻沒有更小的節點時,它會稱爲新的領導者.注意這通過所有節點檢測同一個znode節點而避免了羊羣效應.

這裏是它的僞代碼:

讓ELECTION稱爲應用車需的的選擇路徑. 自願者成爲領導者:

  1. 創建一個爲z的節點,路徑爲"ELECTION/n_",設置SEQUENCE and EPHEMERAL 標記;

  2. 讓C作爲"ELECTION"的孩子, i作爲z節點的序列號.

  3. 檢測"ELECTION/n_j"的改變, 在這裏j是最大的序列號,這裏j小於i,並且n_j是C中的一個節點.

在接收到znode節點刪除通知後:

  1. 讓C成爲ElECION的新的孩子.

  2. 如果Z是C紅最小的節點,然後執行領導者過程;

  3. 否則,檢測"ELECTION/n_j"的改變, 這裏的j是最大序列號,這裏j小於i並且n_j是C裏的節點.

注意子節點列表裏沒有之前的節點並不意味着節點的創建者知道他是當前的領導者。應用可能考慮創建一個單獨的節點去通知領導者已經執行了領導者選舉過程。

沒有符合的翻譯結果!

請確認選中的文本是完整的單詞或句子。

目前僅谷歌翻譯支持漢譯英。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章