前言
ZAB 協議是為分散式協調服務 ZooKeeper 專門設計的一種支援崩潰恢復的原子廣播協議。在 ZooKeeper 中,主要依賴 ZAB 協議來實現分散式資料一致性,基於該協議,ZooKeeper 實現了一種主備模式的系統架構來保持叢集中各個副本之間的資料一致性。
Atomic broadcast protocol
ZAB 是 Zookeeper 原子廣播協議的簡稱,下面我們來討論協議的內容,注意:理論與實現是有區別的,如果你對協議的理論不感興趣,可以直接跳過看實現。
問題的提出
Zookeeper 客戶端會隨機連線到 Zookeeper 叢集的一個節點,如果是讀請求,就直接從當前節點中讀取資料;如果是寫請求,那麼節點就會向 leader 提交事務,leader 會廣播事務,只要有超過半數節點寫入成功,該寫請求就會被提交(類 2PC 協議)。
那麼問題來了:
- 主從架構下,leader 崩潰,資料一致性怎麼保證?
- 選舉 leader 的時候,整個叢集無法處理寫請求的,如何快速進行 leader 選舉?
帶著這兩個問題,我們來看看 ZAB 協議是如何解決的。
ZAB 的四個階段
術語解釋
- quorum:叢集中超過半數的節點集合
ZAB 中的節點有三種狀態
- following:當前節點是跟隨者,服從 leader 節點的命令
- leading:當前節點是 leader,負責協調事務
- election/looking:節點處於選舉狀態
節點的持久狀態
- history:當前節點接收到事務提議的 log
- acceptedEpoch:follower 已經接受的 leader 更改年號的 NEWEPOCH 提議
- currentEpoch:當前所處的年代
- lastZxid:history 中最近接收到的提議的 zxid (最大的)
在 ZAB 協議的事務編號 Zxid 設計中,Zxid 是一個 64 位的數字,其中低 32 位是一個簡單的單調遞增的計數器,針對客戶端每一個事務請求,計數器加 1;而高 32 位則代表 Leader 週期 epoch 的編號,每個當選產生一個新的 Leader 伺服器,就會從這個 Leader 伺服器上取出其本地日誌中最大事務的ZXID,並從中讀取 epoch 值,然後加 1,以此作為新的 epoch,並將低 32 位從 0 開始計數。
epoch:可以理解為當前叢集所處的年代或者週期,每個 leader 就像皇帝,都有自己的年號,所以每次改朝換代,leader 變更之後,都會在前一個年代的基礎上加 1。這樣就算舊的 leader 崩潰恢復之後,也沒有人聽他的了,因為 follower 只聽從當前年代的 leader 的命令。*
Phase 0: Leader election(選舉階段)
節點在一開始都處於選舉階段,只要有一個節點得到超半數節點的票數,它就可以當選準 leader。只有到達 Phase 3 準 leader 才會成為真正的 leader。這一階段的目的是就是為了選出一個準 leader,然後進入下一個階段。
協議並沒有規定詳細的選舉演算法,後面我們會提到實現中使用的 Fast Leader Election。
Phase 1: Discovery(發現階段)
在這個階段,followers 跟準 leader 進行通訊,同步 followers 最近接收的事務提議。這個一階段的主要目的是發現當前大多數節點接收的最新提議,並且準 leader 生成新的 epoch,讓 followers 接受,更新它們的 acceptedEpoch
一個 follower 只會連線一個 leader,如果有一個節點 f 認為另一個 follower p 是 leader,f 在嘗試連線 p 時會被拒絕,f 被拒絕之後,就會進入 Phase 0。
Phase 2: Synchronization(同步階段)
同步階段主要是利用 leader 前一階段獲得的最新提議歷史,同步叢集中所有的副本。只有當 quorum 都同步完成,準 leader 才會成為真正的 leader。follower 只會接收 zxid 比自己的 lastZxid 大的提議。
Phase 3: Broadcast(廣播階段)
到了這個階段,Zookeeper 叢集才能正式對外提供事務服務,並且 leader 可以進行訊息廣播。同時如果有新的節點加入,還需要對新節點進行同步。
值得注意的是,ZAB 提交事務並不像 2PC 一樣需要全部 follower 都 ACK,只需要得到 quorum (超過半數的節點)的 ACK 就可以了。
協議實現
協議的 Java 版本實現跟上面的定義有些不同,選舉階段使用的是 Fast Leader Election(FLE),它包含了 Phase 1 的發現職責。因為 FLE 會選舉擁有最新提議歷史的節點作為 leader,這樣就省去了發現最新提議的步驟。實際的實現將 Phase 1 和 Phase 2 合併為 Recovery Phase(恢復階段)。所以,ZAB 的實現只有三個階段:
- Fast Leader Election
- Recovery Phase
- Broadcast Phase
Fast Leader Election
前面提到 FLE 會選舉擁有最新提議歷史(lastZixd最大)的節點作為 leader,這樣就省去了發現最新提議的步驟。這是基於擁有最新提議的節點也有最新提交記錄的前提。
成為 leader 的條件
- 選
epoch
最大的 epoch
相等,選 zxid 最大的epoch
和zxid
都相等,選擇server id
最大的(就是我們配置zoo.cfg
中的myid
)
節點在選舉開始都預設投票給自己,當接收其他節點的選票時,會根據上面的條件更改自己的選票並重新傳送選票給其他節點,當有一個節點的得票超過半數,該節點會設定自己的狀態為 leading,其他節點會設定自己的狀態為 following。
選舉過程
Recovery Phase (恢復階段)
這一階段 follower 傳送它們的 lastZixd 給 leader,leader 根據 lastZixd 決定如何同步資料。這裡的實現跟前面 Phase 2 有所不同:Follower 收到 TRUNC 指令會中止 L.lastCommittedZxid 之後的提議,收到 DIFF 指令會接收新的提議。
history.lastCommittedZxid:最近被提交的提議的 zxid
history:oldThreshold:被認為已經太舊的已提交提議的 zxid
總結
經過上面的分析,我們可以來回答開始提到的兩個問題
主從架構下,leader 崩潰,資料一致性怎麼保證?
leader 崩潰之後,叢集會選出新的 leader,然後就會進入恢復階段,新的 leader 具有所有已經提交的提議,因此它會保證讓 followers 同步已提交的提議,丟棄未提交的提議(以 leader 的記錄為準),這就保證了整個叢集的資料一致性。
選舉 leader 的時候,整個叢集無法處理寫請求的,如何快速進行 leader 選舉?
這是通過 Fast Leader Election 實現的,leader 的選舉只需要超過半數的節點投票即可,這樣不需要等待所有節點的選票,能夠儘早選出 leader。
這篇文章是根據我對 ZAB 協議的理解寫成的,如果覺得有些細節沒有講清楚,可以看後面的參考資料,我主要是參考這篇論文的。
喜歡的點點關注點點贊,如有不同意見歡迎評論留言。
歡迎工作一到五年的Java工程師朋友們加入Java架構開發:878249276,群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
ZooKeeper’s atomic broadcast protocol:Theory and practice