Zookeeper ZAB協議原理淺析

v-code發表於2020-12-19


前言

DTCC 要在下週一到週三要在北京舉辦,身邊有不少人都去參加了,領略中國最為領先的一些公司的自研儲存技術。
阿里自研polardb,polardb-x(x-engine)相關,華為自研Gaussdb,開源TiDB 等,從SQL,到NoSQL,到NewSQL 都會將一些核心技術設計在大會上分享討論,而且今年也是中國資料儲存技術進入時間領先領域的一年(之前都是google,微軟等巨頭),那這場大會將是深入瞭解近年來中國儲存最為前沿的技術盛會。

然後週五的一場技術分享卻發現自己知識體系的嚴重漏洞, 分散式領域從上到下(分散式協調系統,分散式資料庫(kv,table,graph,document),分散式儲存(塊,檔案系統,物件),單機儲存),除了最底層的單機儲存引擎(現在做的)之外沒有一個能夠深入理解掌握並靈活應用的。僅僅為了準備一個分散式協調系統的分享,就發現了無數的知識漏洞(從基礎的編碼到上層的系統認知),那這樣的基礎去參加更高層次的技術集會豈不是被吊打。就像平時聽大佬們分享一樣,總是在感嘆自己的無知,沒有足夠的知識基礎,沒有辦法在大腦中快速打造屬於自己的知識架構,最後反而適得其反。


回到我們要討論的ZAB協議上,這是周內分享的一部分,當然僅僅是一些原理上的描述,並沒有涉及zookeeper內部的程式碼實現。分享過程中將ZAB協議的演進也做了一個整體的分享,從paxos,multi paxos, raft/zab 都做了整體的描述,本篇文章主要討論zab的協議原理,畢竟zookeeper的實現核心,當然要上乾貨。
之前有兩篇相關的zookeeper 基礎入門和運維相關的文章,能夠有效節省大家的入門時間,先對 zookeeper有一個整體的瞭解。

1. 一文入門zookeeper
2. 一文運維zookeeper

關於zab協議內容的介紹能夠回答如下幾個問題:

  1. zookeeper 客戶端介面為什麼是wait-free的?即介面之間不會相互影響,所以不需要相互等待返回,可以並行呼叫介面。(並行更新資料)
  2. zookeeper 如何保證不出現雙主?
  3. zookeeper如何保證請求順序執行?

1. 基本角色和概念

ZAB 協議(zookeeper atomic broadcast) zookeeper原子廣播協議,作為zookeeper實現分散式協調服務的核心,提供從leader 選舉,到日誌複製,到資料同步 以及 最後的資料廣播 ,提供了一整套的實現演算法。是一個值得學習研究的分散式系統,能夠極大得幫助一些感興趣的同學提升對分散式系統的理解和認知。

zookeeper 內部有三種角色,每一種角色可以用一個zookeeper server程式來表示:

  • leader: 整個叢集只能有一個,主要處理寫請求,zookeeper中所有的寫請求都需要由leader負責處理。當然也能提供讀服務。
  • follower: 整個叢集可以有多個,只能提供讀服務。在leader發生異常之後通過ZAB的leader election 以及後續的Discovery完成leader的重新選舉,將一個follower 標記為leader,對外提供讀寫服務。
  • observer: 不參與leader選舉和投票,僅僅提供讀服務和接受leader變更的通知。可以作為zookeeper同城雙機房,中的從機房的角色。既能夠提供讀服務,又能夠有效得減少兩個機房之間的rpc通訊,從而提升整體的叢集效能(當然,存在的問題也很明顯,主從機房發生網路分割槽,從機房就不可用了)。

ZAB協議的主要是四個階段:

  • Leader Election: 主要是節點之間進行資訊同步,選擇出一個leader
  • Discovery: leader 獲取最新的history資訊。(這裡的history資訊是整個叢集最新的<v,zxid> 事務版本zxid以及其對應的資料)
  • Synchronization: leader將獲取到的最新的資料同步到其他的從節點,並補全老資料,刪除新資料
  • BroadCast: 之前的三個階段都是叢集不可用的狀態。到了這個階段,整個叢集就可以對外提供讀寫服務,且zookeeper叢集正常狀態下處於該階段。

ZAB 協議中有一些基本概念需要提前同步一下,如果感覺理解的還不很深刻,建議先看看前言推薦的兩篇文章。

  • zxid: 唯一標識一個trasaction, 全域性唯一遞增的64位整數。zxid由 <epoch, count>
  • Epoch: 每個leader生命週期的一個標識。newEpoch = lastEpoch + 1
  • Count :表示每個Epoch期間發生的transaction id, 每個count 都是從0開始加一遞增
  • zxid的比較; 我們稱zxid <e, c> 大於 zxid’<e’, c’>,當滿足 (e > e’ ) || (e = e’ & c > c’).

同時在ZAB中,我們前面說到的zookeeper內部的角色都會統稱為peer, 一個peer代表一個角色程式;peer中有如下幾個核心變數:

  • history: 被Peer提交的歷史proposal<v,zxid>,也就是資料和事務id
  • acceptedEpoch,接受最新的NEWEPOCH的Epoch,主要是用來leader選舉過程中follower判斷是否接受leader的NEWEPOCH資訊。
  • currentEpoch 接受最新的NEWLEADER的epoch, 當選舉出來新的 leader之後,會將新leader的epoch更新到這個檔案中。
  • lastZxid: 表示history 最近提交的proposal的zxid.

這麼多概念第一次看肯定記不下, 實際講解的過程中如果忘記了可以返回來檢視。

2. Leader Election

顧名思義,這個階段就是選舉leader的階段,叢集不可用。
核心目的:通過投票完成leader選舉,且叢集每一個成員都會知道leader的epoch和leader id(myid檔案)

時序圖如下:
在這裡插入圖片描述
投票的資訊主要是類似vote (zxid,id),當然實際更加詳細(vote,id,state,round);id表示 唯一標識一個peer的id,也就是myid檔案中的編號;state表示peer的所處的狀態( leader,follower,election),round表示當前peer是第幾輪投票。

上圖中我們使用node(id,zxid)來簡化選舉過程,其中id就是myid, zxid就是當前peer最新的版本號。
圖中有三條白色箭頭,分別代表三個節點的時間,每一個節點在某一個時間會有三條線。
比如T1時刻的node1的三條黃色線,表示分別向自己投票,將自己的投票資訊傳送給其他兩個節點,投票資訊的大小比較如下規則:node1(id,zxid) > node2(id’ , zxid’) ,當滿足 zxid > zxid’ || (zxid==zxid’ && id > id’)

T1 時刻 開始了leader選舉,三個節點都將各自的node資訊先傳送給自己,再傳送給其他兩個節點。
T2 時刻, 其他兩個節點都已經完成了投票資訊的比較:

  • 比如 node1 會收到其他兩個節點的投票資訊,依次和自己的zxid進行比較,版本號高的成為leader;node1最高,不需要再傳送訊息投票(它開始已經投自己一票了)。
  • node2 會收到來自node3和node1的投票,進行投票資訊的比較,發現node1 > node2,node1 > node3,投票給node1,並準備好 新的投票結果進行廣播。
  • node3 類似,投票給node1。

T3 時刻,整個叢集其實已經完成了選舉,node1成為新leader, 不過還是準leader,後續需要進行一些更進一步的版本資料同步。

這個過程存在一些問題,比如node1 T1時刻傳送給 node2節點的投票資訊出現rpc延遲,在node2完成投票決策之後才到達node2。
在這裡插入圖片描述

在T1 時候 , node2只收到了node2自己和node3的投票,進行投票資訊的比對,雖然zxid 相等,但是node3 id更大,且也滿足大多數,則node2 會選擇node3作為leader,而node3會選擇node1進行投票(node1傳送到node3的投票資訊並沒有延遲)也就是到T3時刻,node2認為node3是leader , 而node3和node1都認為自己是leader。當然實際情況node3並不會被標記為leader,因為node3只收到一個投票,不滿足大多數,只是叢集中會存在這樣的衝突。

當然這種問題在後續的Dicovery階段進行leader版本資訊比較時就能夠避免,發現leader的版本號比follower 版本號更低時會觸發重新選舉,這裡說一下Leader Election這個階段如何避免 某個peer出現 delay message的問題。
在這裡插入圖片描述

維護一個超時時間 Finalize Wait Time,當某一個peer收到投票資訊後傳送了一次投票結果,但是在這段時間內如果還收到其他的投票資訊且需要變更投票結果,那麼這個peer會重新傳送一個新的決策結果給其他的peer。

也就是到這個Finalize Wait Time 結束後的叢集leader才會是新的Leader。
T2時刻也處在FWT的時間段內,這個時候延遲的node1 的投票資訊傳送到了node2,發現之前的投票結果需要變更,則會重新發起一次投票,投票為node1作為leader。

當然這個 Finalize Wait Time 肯定也不能完全保證解決這個問題,它的數值設定多大也只是概率性的降低delay message 導致的投票資訊延遲達到的問題。所以,還需要更加嚴謹的機制來保證不出現leader衝突的問題,我們繼續來看後續階段。

3. Discovery

Leader Election之後整個叢集已經完成了選主,當然這個leader並不是真正的leader。之前它可能擁有最新的版本號,但現在已經改朝換代了,需要有自己的年號來向天下宣告自己的登基。所以它需要重新發布年號(版本號),同時需要掌控整個王朝最新的資源(最新的資料),只有完成這一些事情自己才能穩坐寶座,成為正王。

現在的叢集擁有這個幾個角色,其中有一個準leader(按照之前leader選舉過程,也有可能出現雙leader,然後進入這個階段)
在這裡插入圖片描述

ZAB協議的角度先總體說一下這個階段的核心目的:

  1. 確認/生成 一個新leader的Epoch
  2. 新的Epoch同步到所有的Follower
  3. Leader獲取最新的history, 準備進行後續的Synchronization 階段。history: <value, zxid>

具體過程如下:
在這裡插入圖片描述

  1. Follower節點知道準Leader節點之後,會傳送一個FOLLOWERINFO的資訊攜帶自己的f.acceptedEpoch內容

  2. 準leader節點收到超過半數的FOLLOWERINFO之後,會從中選擇一個最大的,並在最大的基礎上+1,即max{f.acceptedEpoch} + 1

  3. 準Leader將準備好的NEWEPOCH傳送到follower, 表示自己的年號已經更新。等待quorum中的成員回覆ACK

  4. follower 收到NEWEPOCH之後和自己本地epoch進行比對:

    a. leader 傳送過來的epoch > acceptedEpoch,更新自己的acceptedEpoch 為新的epoch,並回復一個ACKEPOCH訊息,這個訊息中攜帶上個currentEpoch, history 和 lastZxid(history 最近提交的proposal的zxid)

    b. leader傳送過來的epoch < acceptedEpoch ,則 回退到階段0,重新進行leader選舉(叢集中存在節點異常)。
    c. leader傳送過來的epoch 和 本地acceptedEpoch相等的場景論文並沒有提到,感覺應該會需要重新選舉,畢竟leader已經在收到大多數的FOLLOWERINFO 中最大的+1了。

  5. Leader收到所有quorum中follower的ACKEPOCH, 從所有的訊息中找出currentEpoch最大的或者lastZxid最大的follower,然後把該follower的history 作為自己的history(pull history的過程)。當然,如果本地自己的currentEpoch 或者 lastZxid最大,那就用本地的history即可。

到現在,Leader 已經獲取到最新的history, 並開始準備進行後續的Synchronization 階段。

4. Synchronization

這個階段的核心目的是:

  1. 同步history proposal。即將Leader獲取到的最新的history 資料同步到follower節點,讓整個叢集資料對齊。
  2. 處理上個階段遺留下來的proposal,follower節點中的資料 需要清理的可以清理,需要刪除的可以刪除。

大體過程如下:
在這裡插入圖片描述

Leader這個階段剛開始的時候已經有了整個叢集最新的history資料。

  1. Leader 想所有的follower傳送NEWLEADER資訊,其中包括leader自己最新的epoch 和 最新的history資料。

  2. follower 收到leader的訊息之後判斷當前輪次自己的acceptedEpoch和leader傳送過來的epoch是否一樣(discovery階段已經對follower自己的acceptedEpoch進行了更新)

    1). follower的acceptedEpoch和新epoch相同,表示自己已經跟上了新的epoch, 那麼做如下幾個事情
    a. 更新自己的currentEpoch為新的epoch,表示進入新的朝代了
    b. 按照zxid的大小逐一進行本地proposed,此時這些transaction還未commit。
    c. 更新自己的history為最新的history
    d. 返回一個ACKNEWLEADER 給leader, 表示這個follower已經完成資料同步

    2). follower收到的epoch和本地的acceptedEpoch不同,那麼回退到階段0,重新選主(存在節點異常,當前主節點並不能包含所有的資料,不能隨意更新,否則會丟資料)。

  3. Leader 收到follower節點的ACKNEWLEADER訊息之後,對proposal的資料進行提交commit,所有的follower節點也會收到commit請求(落盤)

  4. follower節點收到leader的COMMIT請求,會對自己本地已經proposed但還未commit的事務,按照zxid進行從小到大的排序,優先commit zxid較小的節點。

  5. Leader 和 Follower都完成同步之後進入第四階段。

從朝代更替來看,前面的幾個階段整個國家處於亂世:無君,君臣各有所思,各有所謀,可能的多君。。。。
到這個階段,歷經千難萬險完成了國家統一,君強臣明,整個國家開始一致對外,共向繁榮的場景。
正如我們的春秋戰國到秦,五代十國到宋,南宋到元,每一個帝國的崛起都歷經無數次的嘗試和磨難,但大一統的目標從秦遍成了唯一,只有叢集大一統,才能夠更好得施展每一個角色的才華。

5. BroadCast

這是一個穩定的時代, 之前三個階段,zookeeper無法對外提供服務。到了這個階段,整個叢集即能夠對外提供讀寫服務。

這個階段如果發生叢集成員變更,即加入了follower和observer。
整體過程如下:
·

  1. Leader收到一個寫請求,會生成一個Proposal: <value, zxid>, zxid = lastZxid + 1,對quorum中的follower節點發起propose請求,並攜帶生成的Proposal。
  2. follower節點收到propose的proposal,將其加入到history佇列,並向leader回覆ACK,表示已經收到propsal。
  3. leader收到過半節點的ACK之後,認為可以進行commit,則向quorum傳送COMMIT請求
  4. follower收到propose的commit 之後開始進行提交
    1). 為了滿足zxid的全域性一致性,這裡會檢查follower本地是否有未提交的proposal<v,z>,保證比當前zxid小的propose先提交
    2). 當所有小於zxid的propose都完成commit之後再提交當前的zxid。
  5. BroadCast階段 也能夠接受新的Follower或者Observer的加入,步驟如下:
    1). 新加入的節點會給Leader傳送一個FOLLOWERINFO資訊
    2). Leader收到後會回覆給他一個NEWEPOCH 和 NEWLEADER, 告訴這個節點叢集最新的epoch和history資料
    3). 新節點收到NEWLEADER後,如果正常邏輯處理完成後(將history中的資料併發propose),回一個ACKNEWLEADER給Leader
    4). Leader收到ACK回覆之後告訴他可以進行本地proposal的提交了,會傳送一個COMMIT 請求
    5). 新節點收到這個請求,對本地完成proposed的資料按照zxid從小到達進行commit(落盤)。
    6). 新節點完成commit之後,leader會將新的節點加入到自己的quorum列表中。

ps :
a. 以上過程不論是leader還是follower 節點在進行propose的過程都是可以併發進行的。對於leader來說,一個proposal的發起不會等待上一個commit完成之後才會發起,當前proposal和上一個proposal是可以並行處理,保證了zookeeper的更新介面可以提供wait-free 的能力。

b. commit 時需要保證本地比當前zxid更小的事務優先提交,從而保證zookeeper的Linearizable 特性

以上基本就是ZAB協議的每個階段的細節,在我們實際zookeeper的實現中,會對以上四個階段做優化。

我們能夠看到從開始選主到能夠提供服務,這個過程還是會有大量的rpc 和資料互動,zookeeper實際將leader election和discovery變更為 FLE(Fast Leader Election)階段,在完成Leader 選舉之後 leader 就已經擁有了最新的history資料。

將Synchronization 階段變更為了Recovery 過程,整體上就是讓單次rpc攜帶的資料量更大,能夠在完成相互的交流通訊之後進行更多更快的本地計算,而不是將較多的時間消耗在rpc和等待rpc資料的過程中。

後記

這是一個亂世的結束,但從歷史的角度看,也會是一個亂世的開始。

居安思危很難,領導層 只能夠在大多數場景做出正確的決策,但p9999和max之間差異還是太大。我們的系統 同樣是一個複雜體系,大量的靜默錯誤(硬體損耗,磁碟的bit位反轉,還有大量的底層系統軟體到上層應用軟體的bug)無法保證一個分散式叢集 每時每刻都正常執行。只能在有限的人力,有限的資源下最大化我們系統的可用性,創造足夠的價值。正如那一些歷史上的偉大帝國 在他們所在的時代創造了讓後人敬仰的文明。

相關文章