ZooKeeper的一致性算法賞析

1 ZAB介紹

ZAB協議全稱就是ZooKeeper Atomic Broadcast protocol,是ZooKeeper用來實現一致性的算法,分成如下4個階段。

先來解釋下部分名詞

electionEpoch:每執行一次leader選舉,electionEpoch就會自增,用來標記leader選舉的輪次

peerEpoch:每次leader選舉完成之後,都會選舉出一個新的peerEpoch,用來標記事務請求所屬的輪次

zxid:事務請求的唯一標記,由leader服務器負責進行分配。由2部分構成,高32位是上述的peerEpoch,低32位是請求的計數,從0開始。所以由zxid我們就可以知道該請求是哪個輪次的,並且是該輪次的第幾個請求。

lastProcessedZxid:最後一次commit的事務請求的zxid

  • Leader election

    leader選舉過程,electionEpoch自增,在選舉的時候lastProcessedZxid越大,越有可能成爲leader

  • Discovery:

    第一:leader收集follower的lastProcessedZxid,這個主要用來通過和leader的lastProcessedZxid對比來確認follower需要同步的數據範圍

    第二:選舉出一個新的peerEpoch,主要用於防止舊的leader來進行提交操作(舊leader向follower發送命令的時候,follower發現zxid所在的peerEpoch比現在的小,則直接拒絕,防止出現不一致性)

  • Synchronization:

    follower中的事務日誌和leader保持一致的過程,就是依據follower和leader之間的lastProcessedZxid進行,follower多的話則刪除掉多餘部分,follower少的話則補充,一旦對應不上則follower刪除掉對不上的zxid及其之後的部分然後再從leader同步該部分之後的數據

  • Broadcast

    正常處理客戶端請求的過程。leader針對客戶端的事務請求,然後提出一個議案,發給所有的follower,一旦過半的follower回覆OK的話,leader就可以將該議案進行提交了,向所有follower發送提交該議案的請求,leader同時返回OK響應給客戶端

上面簡單的描述了上述4個過程,這4個過程的詳細描述在zab的paper中可以找到,但是我看了之後基本和zab的源碼實現上相差有點大,這裏就不再提zab paper對上述4個過程的描述了,下面會詳細的說明ZooKeeper源碼中是具體怎麼來實現的

2 ZAB協議源碼實現

先看下ZooKeeper整體的實現情況,如下圖所示

zab實現

上述實現中Recovery Phase包含了ZAB協議中的Discovery和Synchronization。

2.1 重要的數據介紹

加上前面已經介紹的幾個名詞

  • long lastProcessedZxid:最後一次commit的事務請求的zxid
  • LinkedList committedLog、long maxCommittedLog、long minCommittedLog:

    ZooKeeper會保存最近一段時間內執行的事務請求議案,個數限制默認爲500個議案。上述committedLog就是用來保存議案的列表,上述maxCommittedLog表示最大議案的zxid,minCommittedLog表示committedLog中最小議案的zxid。

  • ConcurrentMap outstandingProposals

    Leader擁有的屬性,每當提出一個議案,都會將該議案存放至outstandingProposals,一旦議案被過半認同了,就要提交該議案,則從outstandingProposals中刪除該議案

  • ConcurrentLinkedQueue toBeApplied

    Leader擁有的屬性,每當準備提交一個議案,就會將該議案存放至該列表中,一旦議案應用到ZooKeeper的內存樹中了,然後就可以將該議案從toBeApplied中刪除

對於上述幾個參數,整個Broadcast的處理過程可以描述爲:

  • leader針對客戶端的事務請求(leader爲該請求分配了zxid),創建出一個議案,並將zxid和該議案存放至leader的outstandingProposals中
  • leader開始向所有的follower發送該議案,如果過半的follower回覆OK的話,則leader認爲可以提交該議案,則將該議案從outstandingProposals中刪除,然後存放到toBeApplied中
  • leader對該議案進行提交,會向所有的follower發送提交該議案的命令,leader自己也開始執行提交過程,會將該請求的內容應用到ZooKeeper的內存樹中,然後更新lastProcessedZxid爲該請求的zxid,同時將該請求的議案存放到上述committedLog,同時更新maxCommittedLog和minCommittedLog
  • leader就開始向客戶端進行回覆,然後就會將該議案從toBeApplied中刪除

2.2 Fast Leader Election

leader選舉過程要關注的要點:

  • 所有機器剛啓動時進行leader選舉過程
  • 如果leader選舉完成,剛啓動起來的server怎麼識別到leader選舉已完成

投票過程有3個重要的數據:

  • ServerState

    目前ZooKeeper機器所處的狀態有4種,分別是

    • LOOKING:進入leader選舉狀態
    • FOLLOWING:leader選舉結束,進入follower狀態
    • LEADING:leader選舉結束,進入leader狀態
    • OBSERVING:處於觀察者狀態
  • HashMap recvset

    用於收集LOOKING、FOLLOWING、LEADING狀態下的server的投票

  • HashMap outofelection

    用於收集FOLLOWING、LEADING狀態下的server的投票(能夠收集到這種狀態下的投票,說明leader選舉已經完成)

下面就來詳細說明這個過程:

  • 1 serverA首先將electionEpoch自增,然後爲自己投票

    serverA會首先從快照日誌和事務日誌中加載數據,就可以得到本機器的內存樹數據,以及lastProcessedZxid(這一部分後面再詳細說明)

    初始投票Vote的內容:

    • proposedLeader:ZooKeeper Server中的myid值,初始爲本機器的id
    • proposedZxid:最大事務zxid,初始爲本機器的lastProcessedZxid
    • proposedEpoch:peerEpoch值,由上述的lastProcessedZxid的高32得到

    然後該serverA向其他所有server發送通知,通知內容就是上述投票信息和electionEpoch信息

  • 2 serverB接收到上述通知,然後進行投票PK

    如果serverB收到的通知中的electionEpoch比自己的大,則serverB更新自己的electionEpoch爲serverA的electionEpoch

    如果該serverB收到的通知中的electionEpoch比自己的小,則serverB向serverA發送一個通知,將serverB自己的投票以及electionEpoch發送給serverA,serverA收到後就會更新自己的electionEpoch

    在electionEpoch達成一致後,就開始進行投票之間的pk,規則如下:

    /*
     * We return true if one of the following three cases hold:
     * 1- New epoch is higher
     * 2- New epoch is the same as current epoch, but new zxid is higher
     * 3- New epoch is the same as current epoch, new zxid is the same
     *  as current zxid, but server id is higher.
     */
    
    return ((newEpoch > curEpoch) || 
            ((newEpoch == curEpoch) &&
            ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
    

    就是優先比較proposedEpoch,然後優先比較proposedZxid,最後優先比較proposedLeader

    pk完畢後,如果本機器投票被pk掉,則更新投票信息爲對方投票信息,同時重新發送該投票信息給所有的server。

    如果本機器投票沒有被pk掉,則看下面的過半判斷過程

  • 3 根據server的狀態來判定leader

    如果當前發來的投票的server的狀態是LOOKING狀態,則只需要判斷本機器的投票是否在recvset中過半了,如果過半了則說明leader選舉就算成功了,如果當前server的id等於上述過半投票的proposedLeader,則說明自己將成爲了leader,否則自己將成爲了follower

    如果當前發來的投票的server的狀態是FOLLOWING、LEADING狀態,則說明leader選舉過程已經完成了,則發過來的投票就是leader的信息,這裏就需要判斷發過來的投票是否在recvset或者outofelection中過半了

    同時還要檢查leader是否給自己發送過投票信息,從投票信息中確認該leader是不是LEADING狀態(這一部分還需要仔細推敲下)

2.3 Recovery Phase

一旦leader選舉完成,就開始進入恢復階段,就是follower要同步leader上的數據信息

  • 1 通信初始化

    leader會創建一個ServerSocket,接收follower的連接,leader會爲每一個連接會用一個LearnerHandler線程來進行服務

  • 2 重新爲peerEpoch選舉出一個新的peerEpoch

    follower會向leader發送一個Leader.FOLLOWERINFO信息,包含自己的peerEpoch信息

    leader的LearnerHandler會獲取到上述peerEpoch信息,leader從中選出一個最大的peerEpoch,然後加1作爲新的peerEpoch。

    然後leader的所有LearnerHandler會向各自的follower發送一個Leader.LEADERINFO信息,包含上述新的peerEpoch

    follower會使用上述peerEpoch來更新自己的peerEpoch,同時將自己的lastProcessedZxid發給leader

    leader的所有LearnerHandler會記錄上述各自follower的lastProcessedZxid,然後根據這個lastProcessedZxid和leader的lastProcessedZxid之間的差異進行同步

  • 3 已經處理的事務議案的同步

    判斷LearnerHandler中的lastProcessedZxid是否在minCommittedLog和maxCommittedLog之間

    • LearnerHandler中的lastProcessedZxid和leader的lastProcessedZxid一致,則說明已經保持同步了
    • 如果lastProcessedZxid在minCommittedLog和maxCommittedLog之間

      從lastProcessedZxid開始到maxCommittedLog結束的這部分議案,重新發送給該LearnerHandler對應的follower,同時發送對應議案的commit命令

      上述可能存在一個問題:即lastProcessedZxid雖然在他們之間,但是並沒有找到lastProcessedZxid對應的議案,即這個zxid是leader所沒有的,此時的策略就是完全按照leader來同步,刪除該follower這一部分的事務日誌,然後重新發送這一部分的議案,並提交這些議案

    • 如果lastProcessedZxid大於maxCommittedLog

      則刪除該follower大於部分的事務日誌

    • 如果lastProcessedZxid小於minCommittedLog

      則直接採用快照的方式來恢復

  • 4 未處理的事務議案的同步

    LearnerHandler還會從leader的toBeApplied數據中將大於該LearnerHandler中的lastProcessedZxid的議案進行發送和提交(toBeApplied是已經被確認爲提交的)

    LearnerHandler還會從leader的outstandingProposals中大於該LearnerHandler中的lastProcessedZxid的議案進行發送,但是不提交(outstandingProposals是還沒被被確認爲提交的)

  • 5 將LearnerHandler加入到正式follower列表中

    意味着該LearnerHandler正式接受請求。即此時leader可能正在處理客戶端請求,leader針對該請求發出一個議案,然後對該正式follower列表纔會進行執行發送工作。這裏有一個地方就是:

    上述我們在比較lastProcessedZxid和minCommittedLog和maxCommittedLog差異的時候,必須要獲取leader內存數據的讀鎖,即在此期間不能執行修改操作,當欠缺的數據包已經補上之後(先放置在一個隊列中,異步發送),才能加入到正式的follower列表,否則就會出現順序錯亂的問題

    同時也說明了,一旦一個follower在和leader進行同步的過程(這個同步過程僅僅是確認要發送的議案,先放置到隊列中即可等待異步發送,並不是說必須要發送過去),該leader是暫時阻塞一切寫操作的。

    對於快照方式的同步,則是直接同步寫入的,寫入期間對數據的改動會放在上述隊列中的,然後當同步寫入完成之後,再啓動對該隊列的異步寫入。

    上述的要理解的關鍵點就是:既要不能漏掉,又要保證順序

  • 6 LearnerHandler發送Leader.NEWLEADER以及Leader.UPTODATE命令

    該命令是在同步結束之後發的,follower收到該命令之後會執行一次版本快照等初始化操作,如果收到該命令的ACK則說明follower都已經完成同步了並完成了初始化

    leader開始進入心跳檢測過程,不斷向follower發送心跳命令,不斷檢是否有過半機器進行了心跳回復,如果沒有過半,則執行關閉操作,開始進入leader選舉狀態

    LearnerHandler向對應的follower發送Leader.UPTODATE,follower接收到之後,開始和leader進入Broadcast處理過程

2.4 Broadcast Phase

前面其實已經說過了,參見2.1中的內容

3 特殊情況的注意點

3.1 事務日誌和快照日誌的持久化和恢復

先來看看持久化過程:

  • Broadcast過程的持久化

    leader針對每次事務請求都會生成一個議案,然後向所有的follower發送該議案

    follower接收到該議案後,所做的操作就是將該議案記錄到事務日誌中,每當記滿100000個(默認),則事務日誌執行flush操作,同時開啓一個新的文件來記錄事務日誌

    同時會執行內存樹的快照,snapshot.[lastProcessedZxid]作爲文件名創建一個新文件,快照內容保存到該文件中

  • leader shutdown過程的持久化

    一旦leader過半的心跳檢測失敗,則執行shutdown方法,在該shutdown中會對事務日誌進行flush操作

再來說說恢復:

  • 事務快照的恢復

    第一:會在事務快照文件目錄下找到最近的100個快照文件,並排序,最新的在前

    第二:對上述快照文件依次進行恢復和驗證,一旦驗證成功則退出,否則利用下一個快照文件進行恢復。恢復完成更新最新的lastProcessedZxid

  • 事務日誌的恢復

    第一:從事務日誌文件目錄下找到zxid大於等於上述lastProcessedZxid的事務日誌

    第二:然後對上述事務日誌進行遍歷,應用到ZooKeeper的內存樹中,同時更新lastProcessedZxid

    第三:同時將上述事務日誌存儲到committedLog中,並更新maxCommittedLog、minCommittedLog

由此我們可以看到,在初始化恢復的時候,是會將所有最新的事務日誌作爲已經commit的事務來處理的

也就是說這裏面可能會有部分事務日誌還沒真實提交,而這裏全部當做已提交來處理。這個處理簡單粗暴了一些,而raft對老數據的恢復則控制的更加嚴謹一些。

3.2 follower掛了之後又重啓的恢復過程

一旦leader掛了,上述leader的2個集合

  • ConcurrentMap outstandingProposals

  • ConcurrentLinkedQueue toBeApplied

就無效了。他們並不在leader恢復的時候起作用,而是在系統正常執行,而某個follower掛了又恢復的時候起作用。

我們可以看到在上述2.3的恢復過程中,會首先進行快照日誌和事務日誌的恢復,然後再補充leader的上述2個數據中的內容。

3.3 同步follower失敗的情況

目前leader和follower之間的同步是通過BIO方式來進行的,一旦該鏈路出現異常則會關閉該鏈路,重新與leader建立連接,重新同步最新的數據

3.5 對client端是否一致

  • 客戶端收到OK回覆,會不會丟失數據?
  • 客戶端沒有收到OK回覆,會不會多存儲數據?

客戶端如果收到OK回覆,說明已經過半複製了,則在leader選舉中肯定會包含該請求對應的事務日誌,則不會丟失該數據

客戶端連接的leader或者follower掛了,客戶端沒有收到OK回覆,目前是可能丟失也可能沒丟失,因爲服務器端的處理也很簡單粗暴,對於未來leader上的事務日誌都會當做提交來處理的,即都會被應用到內存樹中。

同時目前ZooKeeper的原生客戶端也沒有進行重試,服務器端也沒有對重試進行檢查。這一部分到下一篇再詳細探討與raft的區別

4 未完待續

本文有很多細節,難免可能疏漏,還請指正。

4.1 問題

這裏留個問題供大家思考下:

raft每次執行AppendEntries RPC的時候,都會帶上當前leader的新term,來防止舊的leader的舊term來執行相關操作,而ZooKeeper的peerEpoch呢?達到防止舊leader的效果了嗎?它的作用是幹什麼呢?

發佈了54 篇原創文章 · 獲贊 33 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章