Apache > ZooKeeper
 

ZooKeeper 範本和解決方案

使用 ZooKeeper 建立較高層級建構的指南

在本文中,您將找到使用 ZooKeeper 來實作較高階函數的指南。所有這些都是由客戶端實作的慣例,不需要 ZooKeeper 的特別支援。希望社群會在客戶端程式庫中擷取這些慣例,以簡化其使用並鼓勵標準化。

關於 ZooKeeper 最有趣的事情之一是,即使 ZooKeeper 使用非同步通知,您也可以使用它來建構同步一致性原語,例如佇列和鎖定。正如您所看到的,這是可能的,因為 ZooKeeper 對更新施加了整體順序,並有公開此順序的機制。

請注意,以下範本嘗試採用最佳實務。特別是,它們避免輪詢、計時器或任何其他可能導致「從眾效應」的事物,從而導致流量激增並限制可擴充性。

可以想像許多有用的功能未包含在此處,例如:可撤銷的讀寫優先權鎖定。而這裡提到的部分結構(特別是鎖定)說明了某些重點,即使您可能會發現其他結構(例如事件處理或佇列)是執行相同功能更實用的方式。一般來說,本節中的範例旨在激發思考。

關於錯誤處理的重要注意事項

實作食譜時,您必須處理可復原的例外狀況(請參閱 常見問題)。特別是,有數個食譜採用順序性暫時節點。建立順序性暫時節點時,會發生一個錯誤案例,即在伺服器上建立 () 成功,但在傳回節點名稱給用戶端之前,伺服器發生故障。當用戶端重新連線時,其工作階段仍然有效,因此不會移除節點。這表示用戶端難以得知其節點是否已建立。以下食譜包含處理此問題的措施。

現成的應用程式:名稱服務、組態、群組成員資格

名稱服務和組態是 ZooKeeper 的兩個主要應用程式。這兩個功能由 ZooKeeper API 直接提供。

ZooKeeper 直接提供的另一個功能是群組成員資格。群組由節點表示。群組成員在群組節點下建立暫時節點。當成員節點異常失敗時,ZooKeeper 會在偵測到失敗時自動移除這些節點。

屏障

分散式系統使用障礙來封鎖一組節點的處理,直到符合某個條件,屆時允許所有節點繼續進行。障礙的實作方式是透過指定一個障礙節點。如果障礙節點存在,則表示有障礙。以下是偽程式碼

  1. 用戶端在障礙節點上呼叫 ZooKeeper API 的 exists() 函式,並將監視設定為 true。
  2. 如果 exists() 傳回 false,表示障礙已解除,用戶端即可繼續進行
  3. 否則,如果 exists() 傳回 true,用戶端會等待 ZooKeeper 的障礙節點監視事件。
  4. 當觸發監視事件時,用戶端會重新發出 exists( ) 呼叫,並再次等待,直到移除障礙節點。

雙重屏障

雙重障礙讓用戶端能夠同步運算的開始和結束。當足夠的程序加入障礙時,程序會開始其運算,並在完成後離開障礙。此食譜顯示如何使用 ZooKeeper 節點作為障礙。

此食譜中的偽程式碼將障礙節點表示為 b。每個用戶端程序 p 在進入時會向障礙節點註冊,並在準備離開時取消註冊。節點會透過以下 Enter 程序向障礙節點註冊,它會等待 x 個用戶端程序註冊後才繼續進行運算。(此處的 x 由您為您的系統決定。)

進入 離開
1. 建立名稱 n = b+“/”+p 1. L = getChildren(b, false)
2. 設定監控:exists(b + ‘‘/ready’’, true) 2. 如果沒有子節點,則離開
3. 建立子節點:create(n, EPHEMERAL) 3. 如果 p 是 L 中唯一的處理程序節點,則刪除 (n) 並離開
4. L = getChildren(b, false) 4. 如果 p 是 L 中最低的處理程序節點,則在 L 中等待最高的處理程序節點
5. 如果 L 中的子節點少於_x_,則等待監控事件 5. 否則,如果仍存在,則 **刪除 (n)** 並在 L 中等待最低的處理程序節點
6. 否則,create(b + ‘‘/ready’’, REGULAR) 6. 前往 1

在進入時,所有處理程序都會監控一個就緒節點,並建立一個臨時節點作為障礙節點的子節點。除了最後一個處理程序之外,每個處理程序都會進入障礙,並在第 5 行等待就緒節點出現。建立第 x 個節點(最後一個處理程序)的處理程序會在子節點清單中看到 x 個節點,並建立就緒節點,喚醒其他處理程序。請注意,等待中的處理程序只會在需要離開時喚醒,因此等待很有效率。

在離開時,您不能使用 就緒 等標誌,因為您正在監控處理程序節點的消失。透過使用臨時節點,在進入障礙後失敗的處理程序不會阻止正確的處理程序完成。當處理程序準備離開時,他們需要刪除其處理程序節點,並等待所有其他處理程序執行相同的動作。

b 的子節點中沒有處理程序節點時,處理程序會離開。但是,為了效率,您可以使用最低的處理程序節點作為就緒標誌。所有其他準備離開的處理程序都會監控最低現有處理程序節點的消失,而最低處理程序的所有者則會監控任何其他處理程序節點(為簡化起見,選擇最高的)的消失。這表示除了最後一個節點會在移除時喚醒所有人之外,只會有一個處理程序在每個節點刪除時喚醒。

佇列

分散式佇列是一種常見的資料結構。要在 ZooKeeper 中實作分散式佇列,請先指定一個 znode 來存放佇列,也就是佇列節點。分散式客戶端會呼叫 create() 將某些內容放入佇列,其路徑名稱以「queue-」結尾,並在 create() 呼叫中將 順序臨時 標誌設定為 true。由於設定了 順序 標誌,新的路徑名稱將具有 path-to-queue-node/queue-X 的格式,其中 X 是單調遞增的數字。想要從佇列中移除的客戶端會呼叫 ZooKeeper 的 getChildren( ) 函式,並在佇列節點上將 監控 設定為 true,並開始處理具有最低數字的節點。客戶端不需要發出另一個 getChildren( ),直到它耗盡從第一個 getChildren( ) 呼叫取得的清單。如果佇列節點中沒有子節點,讀取器會等待監控通知以再次檢查佇列。

注意

ZooKeeper 食譜目錄中現在有一個佇列實作。這會與發行版一起發行,發行成品的 zookeeper-recipes/zookeeper-recipes-queue 目錄。

優先佇列

若要實作優先佇列,您只需要對一般 佇列食譜 進行兩個簡單的變更。首先,若要加入佇列,路徑名稱會以「queue-YY」結尾,其中 YY 是元素的優先順序,數字越小表示優先順序越高(就像 UNIX 一樣)。其次,當從佇列中移除時,客戶端會使用最新的子項清單,表示如果佇列節點觸發監控通知,客戶端會使先前取得的子項清單失效。

鎖定

完全分散的鎖定在全球同步,表示在任何時間快照中,沒有兩個客戶端認為它們持有相同的鎖定。這些可以使用 ZooKeeper 實作。與優先佇列一樣,首先定義鎖定節點。

注意

ZooKeeper 食譜目錄中現在有一個鎖定實作。這會與發行版一起發行,發行成品的 zookeeper-recipes/zookeeper-recipes-lock 目錄。

想要取得鎖定的客戶端會執行下列動作

  1. 使用「locknode/guid-lock-」的路徑名稱和設定的 順序短暫 旗標呼叫 create( )。如果錯過 create() 結果,則需要 guid。請參閱下列註解。
  2. 在鎖定節點上呼叫 getChildren( ) 而不 設定監控旗標(這很重要,可以避免羊群效應)。
  3. 如果步驟 1 中建立的路徑名稱具有最低順序號碼字尾,則客戶端具有鎖定,且客戶端會離開通訊協定。
  4. 客戶端會在順序號碼最低的鎖定目錄路徑上使用設定的監控旗標呼叫 exists( )
  5. 如果 exists( ) 傳回 null,則前往步驟 2。否則,請在前往步驟 2 之前等待前一步驟路徑名稱的通知。

解鎖通訊協定非常簡單:想要釋放鎖定的客戶端只需刪除它們在步驟 1 中建立的節點。

以下是幾個注意事項

可復原錯誤和 GUID

共用鎖定

您可以透過對鎖定通訊協定進行一些變更來實作共用鎖定

取得讀取鎖定 取得寫鎖
1. 呼叫 create( ) 以建立路徑名稱為「guid-/read-」的節點。這是稍後在通訊協定中使用的鎖定節點。請務必設定 sequenceephemeral 旗標。 1. 呼叫 create( ) 以建立路徑名稱為「guid-/write-」的節點。這是稍後在通訊協定中提到的鎖定節點。請務必設定 sequenceephemeral 旗標。
2. 在鎖定節點上呼叫 getChildren( )設定 watch 旗標 - 這很重要,因為它可以避免從眾效應。 2. 在鎖定節點上呼叫 getChildren( )設定 watch 旗標 - 這很重要,因為它可以避免從眾效應。
3. 如果沒有任何子節點的路徑名稱以「write-」開頭,且順序號碼低於步驟 1 中建立的節點,則用戶端已取得鎖定,可以退出通訊協定。 3. 如果沒有任何子節點的順序號碼低於步驟 1 中建立的節點,則用戶端已取得鎖定,且用戶端退出通訊協定。
4. 否則,在鎖定目錄中的節點上呼叫 exists( ),設定 watch 旗標,路徑名稱以「write-」開頭,且具有下一個最低順序號碼。 4. 在具有下一個最低順序號碼的路徑名稱的節點上呼叫 exists( ), 設定 watch 旗標。
5. 如果 exists( ) 傳回 false,則跳至步驟 2 5. 如果 exists( ) 傳回 false,則跳至步驟 2。否則,在前往步驟 2 之前,請等待上一步驟路徑名稱的通知。
6. 否則,在前往步驟 2 之前,請等待上一步驟路徑名稱的通知。

備註

可撤銷共用鎖定

透過對共用鎖定通訊協定進行輕微修改,您可以透過修改共用鎖定通訊協定,讓共用鎖定可撤銷。

在取得讀取和寫入鎖定通訊協定的步驟 1 中,在呼叫 create( ) 之後,立即呼叫設定 watchgetData( )。如果用戶端隨後收到它在步驟 1 中建立的節點的通知,它會在該節點上執行另一個設定 watchgetData( ),並尋找字串「unlock」,這會向用戶端發出信號,表示它必須釋放鎖定。這是因為根據此共用鎖定通訊協定,您可以透過在鎖定節點上呼叫 setData(),將「unlock」寫入該節點,來要求具有鎖定的用戶端放棄鎖定。

請注意,此協定要求鎖定持有者同意釋放鎖定。此同意很重要,特別是在鎖定持有者需要在釋放鎖定前進行一些處理的情況下。當然,您隨時可以透過在協定中規定,如果鎖定持有者在一段時間後未刪除鎖定,則撤銷者允許刪除鎖定節點,來實作「帶有驚人雷射光束的可撤銷共用鎖定」。

兩階段提交

兩階段提交協定是一種演算法,可讓分散式系統中的所有用戶端同意提交或中止交易。

在 ZooKeeper 中,您可以透過讓協調器建立交易節點(例如「/app/Tx」)和每個參與站台一個子節點(例如「/app/Tx/s_i」)來實作兩階段提交。當協調器建立子節點時,它會將內容保留為未定義。一旦交易中涉及的每個站台從協調器收到交易,該站台就會讀取每個子節點並設定監控。然後,每個站台會處理查詢並透過寫入其各自的節點來投票「提交」或「中止」。一旦寫入完成,其他站台就會收到通知,並且只要所有站台都收集到所有投票,它們就可以決定「中止」或「提交」。請注意,如果某些站台投票「中止」,則節點可以提早決定「中止」。

此實作的一個有趣面向是,協調器的唯一角色是決定站台群組、建立 ZooKeeper 節點,以及將交易傳播到對應的站台。事實上,甚至可以透過在交易節點中撰寫交易,來透過 ZooKeeper 傳播交易。

上述方法有兩個重要的缺點。一是訊息複雜度,為 O(n²)。二是無法透過暫時節點來偵測站台故障。若要使用暫時節點偵測站台故障,站台必須建立節點。

若要解決第一個問題,您可以只讓協調器收到交易節點變更的通知,然後在協調器做出決定後通知站台。請注意,此方法具有可擴充性,但速度也較慢,因為它要求所有通訊都透過協調器進行。

若要解決第二個問題,您可以讓協調器將交易傳播到站台,並讓每個站台建立自己的暫時節點。

領導人選舉

使用 ZooKeeper 進行領導者選舉的簡單方法是在建立代表用戶端「建議」的 znode 時使用 SEQUENCE|EPHEMERAL 旗標。這個概念是有一個 znode,例如「/election」,讓每個 znode 建立一個子 znode「/election/guid-n_」並具有 SEQUENCE|EPHEMERAL 旗標。透過序列旗標,ZooKeeper 會自動附加一個序列號碼,這個號碼會大於先前附加在「/election」子項目的任何號碼。建立具有最小附加序列號碼的 znode 的程序就是領導者。

不過,這還不夠。重要的是要注意領導者的故障,以便在當前領導者故障時,新的用戶端會成為新的領導者。一個簡單的解決方案是讓所有應用程式程序監控當前最小的 znode,並在最小的 znode 消失時檢查它們是否為新的領導者(請注意,如果領導者故障,最小的 znode 會消失,因為該節點是臨時的)。但這會造成羊群效應:當前領導者故障時,所有其他程序都會收到通知,並在「/election」上執行 getChildren 以取得「/election」的當前子項目清單。如果用戶端數量龐大,這會導致 ZooKeeper 伺服器必須處理的作業數量激增。若要避免羊群效應,只要監控 znode 序列中的下一個 znode 即可。如果用戶端收到它正在監控的 znode 已消失的通知,那麼在沒有更小的 znode 的情況下,它就會成為新的領導者。請注意,這透過不讓所有用戶端監控同一個 znode 來避免羊群效應。

以下是偽程式碼

讓 ELECTION 成為應用程式的選擇路徑。若要自願成為領導者

  1. 建立 znode z,路徑為「ELECTION/guid-n_」,並具有 SEQUENCE 和 EPHEMERAL 旗標;
  2. 讓 C 成為「ELECTION」的子項目,而我是 z 的序列號碼;
  3. 監控「ELECTION/guid-n_j」的變更,其中 j 是最大的序列號碼,且 j < i,而 n_j 是 C 中的 znode;

在收到 znode 刪除通知時

  1. 讓 C 成為 ELECTION 的新子項目集合;
  2. 如果 z 是 C 中最小的節點,則執行領導者程序;
  3. 否則,監控「ELECTION/guid-n_j」的變更,其中 j 是最大的序列號碼,且 j < i,而 n_j 是 C 中的 znode;

備註