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

最近需要設計一個分佈式系統,需要一箇中間件來存儲共享的信息,來保證多個系統之間的數據一致性,調研了兩個主流框架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 協議設計的優秀之處有兩點,一是簡化二階段提交,提升了在正常工作情況下的性能;二是巧妙地利用率自增序列,簡化了異常恢復的邏輯,也很好地保證了順序處理這一特性


參考:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章