ZooKeeper的一致性算法賞析

1 ZAB介紹

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

先來解釋下部分名詞服務器

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

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

zxid:事務請求的惟一標記,由leader服務器負責進行分配。由2部分構成,高32位是上述的peerEpoch,低32位是請求的計數,從0開始。因此由zxid咱們就能夠知道該請求是哪一個輪次的,而且是該輪次的第幾個請求。線程

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

  • Leader electionleader選舉過程,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源碼中是具體怎麼來實現的cdn

2 ZAB協議源碼實現

先看下ZooKeeper總體的實現狀況,以下圖所示server

31160806_blp5

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

2.1 重要的數據介紹

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

  • long lastProcessedZxid:最後一次commit的事務請求的zxid
  • LinkedList<Proposal> committedLog、long maxCommittedLog、long minCommittedLog:ZooKeeper會保存最近一段時間內執行的事務請求議案,個數限制默認爲500個議案。上述committedLog就是用來保存議案的列表,上述maxCommittedLog表示最大議案的zxid,minCommittedLog表示committedLog中最小議案的zxid。
  • ConcurrentMap<Long, Proposal> outstandingProposalsLeader擁有的屬性,每當提出一個議案,都會將該議案存放至outstandingProposals,一旦議案被過半認同了,就要提交該議案,則從outstandingProposals中刪除該議案
  • ConcurrentLinkedQueue<Proposal> toBeAppliedLeader擁有的屬性,每當準備提交一個議案,就會將該議案存放至該列表中,一旦議案應用到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<Long, Vote> recvset用於收集LOOKING、FOLLOWING、LEADING狀態下的server的投票
  • HashMap<Long, Vote> 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)))));

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    /*

     

    * 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狀態。這個解釋以下:

    由於目前leader和follower都是各自檢測是否進入leader選舉過程。leader檢測到未過半的server的ping回覆,則leader會進入LOOKING狀態,可是follower有本身的檢測,感知這一事件,還須要必定時間,在此期間,若是其餘server加入到該集羣,可能會收到其餘follower的過半的對以前leader的投票,可是此時該leader已經不處於LEADING狀態了,因此須要這麼一個檢查來排除這種狀況。

2.3 Recovery Phase

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

  • 1 通訊初始化leader會建立一個ServerSocket,接收follower的鏈接,leader會爲每個鏈接會用一個LearnerHandler線程來進行服務
  • 2 從新爲peerEpoch選舉出一個新的peerEpochfollower會向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<Long, Proposal> outstandingProposals
  • ConcurrentLinkedQueue<Proposal> 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的效果了嗎?它的做用是幹什麼呢?

相關文章
相關標籤/搜索