一文徹底搞懂ZAB演算法,看這篇就夠了!!!

碼老思發表於2023-04-29

最近需要設計一個分散式系統,需要一箇中介軟體來儲存共享的資訊,來保證多個系統之間的資料一致性,調研了兩個主流框架Zookeeper和ETCD,發現都能滿足我們的系統需求。其中ETCD是K8s中採用的分散式儲存,而其底層採用了RAFT演算法來保證一致性,之前已經詳細分析了Raft演算法的原理,今天主要仔細分析下Zookeeper的底層演算法-ZAB演算法。

什麼是ZAB 演算法?

ZAB的全稱是 Zookeeper Atomic Broadcast (Zookeeper原子廣播)。Zookeeper 是透過 Zab 演算法來保證分散式事務的最終一致性。

  1. Zab協議是為分散式協調服務Zookeeper專門設計的一種 支援崩潰恢復 的 原子廣播協議 ,是Zookeeper保證資料一致性的核心演算法。Zab借鑑了Paxos演算法,但又不像Paxos那樣,是一種通用的分散式一致性演算法。它是特別為Zookeeper設計的支援崩潰恢復的原子廣播協議

  2. 在Zookeeper中主要依賴Zab協議來實現資料一致性,基於該協議,zk實現了一種主備模型(即Leader和Follower模型)的系統架構來保證叢集中各個副本之間資料的一致性。 這裡的主備系統架構模型,就是指只有一臺客戶端(Leader)負責處理外部的寫事務請求,然後Leader客戶端將資料同步到其他Follower節點。

客戶端的讀取流程:客戶端會隨機的連結到 zookeeper 叢集中的一個節點,如果是讀請求,就直接從當前節點中讀取資料;如果是寫請求,那麼節點就會向 Leader 提交事務,Leader 接收到事務提交,會廣播該事務,只要超過半數節點寫入成功,該事務就會被提交。

深入ZAB演算法

ZAB演算法分為兩大塊內容,訊息廣播崩潰恢復

  • 訊息廣播(boardcast):Zab 協議中,所有的寫請求都由 leader 來處理。正常工作狀態下,leader 接收請求並透過廣播協議來處理。

  • 崩潰恢復(recovery):當服務初次啟動,或者 leader 節點掛了,系統就會進入恢復模式,直到選出了有合法數量 follower 的新 leader,然後新 leader 負責將整個系統同步到最新狀態。

1. 訊息廣播

訊息廣播的過程實際上是一個簡化的兩階段提交過程,這裡對兩階段提交做一個簡單的介紹。

兩階段提交

兩階段提交演算法本身是一致強一致性演算法,適合用作資料庫的分散式事務,其實資料庫的經常用到的TCC本身就是一種2PC。

下面以MySQL中對資料庫的修改過程,來介紹下兩階段提交的具體流程,在MySQL中對一條資料的修改操作首先寫undo日誌,記錄的資料原來的樣子,接下來執行事務修改操作,把資料寫到redo日誌裡面,萬一捅婁子,事務失敗了,可從undo裡面恢復資料。資料庫透過undo與redo能保證資料的強一致性。

  • 首先第一階段叫準備節點,事務的請求都傳送給一個個的資源,這裡的資源可以是資料庫,也可以是其他支援事務的框架,他們會分別執行自己的事務,寫日誌到undo與redo,但是不提交事務。
  • 當事務管理器收到了所以資源的反饋,事務都執行沒報錯後,事務管理器再傳送commit指令讓資源把事務提交,一旦發現任何一個資源在準備階段沒有執行成功,事務管理器會傳送rollback,讓所有的資源都回滾。這就是2pc,非常簡單。


說他是強一致性的是他需要保證任何一個資源都成功,整個分散式事務才成功。

優點:原理簡單,實現方便
缺點:同步阻塞,單點問題,資料不一致,容錯性不好

  • 同步阻塞:在二階段提交的過程中,所有的節點都在等待其他節點的響應,無法進行其他操作。這種同步阻塞極大的限制了分散式系統的效能。
  • 單點問題:協調者在整個二階段提交過程中很重要,如果協調者在提交階段出現問題,那麼整個流程將無法運轉。更重要的是,其他參與者將會處於一直鎖定事務資源的狀態中,而無法繼續完成事務操作。
  • 資料不一致:假設當協調者向所有的參與者傳送commit請求之後,發生了區域性網路異常,或者是協調者在尚未傳送完所有 commit請求之前自身發生了崩潰,導致最終只有部分參與者收到了commit請求。這將導致嚴重的資料不一致問題。
  • 容錯性不好:二階段提交協議沒有設計較為完善的容錯機制,任意一個節點是失敗都會導致整個事務的失敗。

ZAB訊息廣播過程

Zookeeper叢集中,存在以下三種角色的節點:

  • *Leader:Zookeeper叢集的核心角色,在叢集啟動或崩潰恢復中透過Follower參與選舉產生,為客戶端提供讀寫服務,並對事務請求進行處理
    Follower:Zookeeper叢集的核心角色,在叢集啟動或崩潰恢復中參加選舉,沒有被選上就是這個角色,
    為客戶端提供讀取服務,也就是處理非事務請求,Follower不能處理事務請求,對於收到的事務請求會轉發給Leader。
    Observer:觀察者角色,
    不參加選舉,為客戶端提供讀取服務,處理非事務請求**,對於收到的事務請求會轉發給Leader。使用Observer的目的是為了擴充套件系統,提高讀取效能。
  1. Leader 接收到訊息請求後,將訊息賦予一個全域性唯一的 64 位自增 id,叫做:zxid,透過 zxid 的大小比較即可實現因果有序這一特性。
  2. Leader 透過先進先出佇列(透過 TCP 協議來實現,以此實現了全域性有序這一特性)將帶有 zxid 的訊息作為一個提案(proposal)分發給所有 follower。
  3. 當 follower 接收到 proposal,先將 proposal 寫到硬碟,寫硬碟成功後再向 leader 回一個 ACK。
  4. 當 leader 接收到合法數量的 ACKs 後,leader 就向所有 follower 傳送 COMMIT 命令,同時會在本地執行該訊息。
  5. 當 follower 收到訊息的 COMMIT 命令時,就會執行該訊息。

相比於完整的二階段提交,Zab 協議最大的區別就是不能終止事務,follower 要麼回 ACK 給 leader,要麼拋棄 leader,在某一時刻,leader 的狀態與 follower 的狀態很可能不一致,因此它不能處理 leader 掛掉的情況,所以 Zab 協議引入了恢復模式來處理這一問題。

從另一角度看,正因為 Zab 的廣播過程不需要終止事務,也就是說不需要所有 follower 都返回 ACK 才能進行 COMMIT,而是隻需要合法數量(2n+1 臺伺服器中的 n+1 臺) 的follower,也提升了整體的效能。

Leader 伺服器與每一個 Follower 伺服器之間都維護了一個單獨的 FIFO 訊息佇列進行收發訊息,使用佇列訊息可以做到非同步解耦。 Leader 和 Follower 之間只需要往佇列中發訊息即可。如果使用同步的方式會引起阻塞,效能要下降很多。

2. 崩潰恢復

崩潰恢復的主要任務就是選舉Leader(Leader Election),Leader選舉分兩個場景:

  • Zookeeper伺服器啟動時Leader選舉。
  • Zookeeper叢集執行過程中Leader崩潰後的Leader選舉。

選舉引數

在介紹選舉流程之前,需要介紹幾個引數,

  • myid: 伺服器ID,這個是在安裝Zookeeper時配置的,myid越大,該伺服器在選舉中被選為Leader的優先順序會越大。ZAB演算法中透過myid來規避了多個節點可能有相同zxid問題,注意可以對比之前的Raft演算法,Raft演算法中透過隨機的timeout來規避多個節點可能同時成為Leader的問題。
  • zxid: 事務ID,這個是由Zookeeper叢集中的Leader節點進行Proposal時生成的全域性唯一的事務ID,由於只有Leader才能進行Proposal,所以這個zxid很容易做到全域性唯一且自增。因為Follower沒有生成zxid的許可權。zxid越大,表示當前節點上提交成功了最新的事務,這也是為什麼在崩潰恢復的時候,需要優先考慮zxid的原因。
  • epoch: 投票輪次,每完成一次Leader選舉的投票,當前Leader節點的epoch會增加一次。在沒有Leader時,本輪此的epoch會保持不變。

另外在選舉的過程中,每個節點的當前狀態會在以下幾種狀態之中進行轉變。

LOOKING: 競選狀態。
FOLLOWING: 隨從狀態,同步Leader 狀態,參與Leader選舉的投票過程。
OBSERVING: 觀察狀態,同步Leader 狀態,不參與Leader選舉的投票過程。
LEADING: 領導者狀態。

選舉流程

選舉的流程如下:

  • 每個Server會發出一個投票,第一次都是投自己。投票資訊:(myid,ZXID)
  • 收集來自各個伺服器的投票
  • 處理投票並重新投票,處理邏輯:優先比較ZXID,然後比較myid
  • 統計投票,只要超過半數的機器接收到同樣的投票資訊,就可以確定leader
  • 改變伺服器狀態,進入正常的訊息廣播流程。

ZAB演算法需要解決的兩大問題

1. 已經被處理的訊息不能丟

這一情況會出現在以下場景:當 leader 收到合法數量 follower 的 ACKs 後,就向各個 follower 廣播 COMMIT 命令,同時也會在本地執行 COMMIT 並向連線的客戶端返回「成功」。但是如果在各個 follower 在收到 COMMIT 命令前 leader 就掛了,導致剩下的伺服器並沒有執行都這條訊息。

為了實現已經被處理的訊息不能丟這個目的,Zab 的恢復模式使用了以下的策略:

  1. 選舉擁有 proposal 最大值(即 zxid 最大) 的節點作為新的 leader:由於所有提案被 COMMIT 之前必須有合法數量的 follower ACK,即必須有合法數量的伺服器的事務日誌上有該提案的 proposal,因此,只要有合法數量的節點正常工作,就必然有一個節點儲存了所有被 COMMIT 的 proposal。 而在選舉Leader的過程中,會比較zxid,因此選舉出來的Leader必然會包含所有被COMMIT的proposal。
  2. 新的 leader 將自己事務日誌中 proposal 但未 COMMIT 的訊息處理。
  3. 新的 leader 與 follower 建立先進先出的佇列, 先將自身有而 follower 沒有的 proposal 傳送給 follower,再將這些 proposal 的 COMMIT 命令傳送給 follower,以保證所有的 follower 都儲存了所有的 proposal、所有的 follower 都處理了所有的訊息。

2. 被丟棄的訊息不能再次出現

這一情況會出現在以下場景:當 leader 接收到訊息請求生成 proposal 後就掛了,其他 follower 並沒有收到此 proposal,因此經過恢復模式重新選了 leader 後,這條訊息是被跳過的。 此時,之前掛了的 leader 重新啟動並註冊成了 follower,他保留了被跳過訊息的 proposal 狀態,與整個系統的狀態是不一致的,需要將其刪除。

5f677u

Zab 透過巧妙的設計 zxid 來實現這一目的。一個 zxid 是64位,高 32 是紀元(epoch)編號,每經過一次 leader 選舉產生一個新的 leader,新 leader 會將 epoch 號 +1。低 32 位是訊息計數器,每接收到一條訊息這個值 +1,新 leader 選舉後這個值重置為 0。這樣設計的好處是舊的 leader 掛了後重啟,它不會被選舉為 leader,因為此時它的 zxid 肯定小於當前的新 leader。當舊的 leader 作為 follower 接入新的 leader 後,新的 leader 會讓它將所有的擁有舊的 epoch 號的未被 COMMIT 的 proposal 清除。

Zab 協議設計的優秀之處有兩點,一是簡化二階段提交,提升了在正常工作情況下的效能;二是巧妙地利用率自增序列,簡化了異常恢復的邏輯,也很好地保證了順序處理這一特性


參考:

歡迎關注公眾號【碼老思】,這裡有最通俗易懂的原創技術乾貨。

相關文章