ZooKeeper的一致性演算法賞析

發表於2016-11-08

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 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原始碼中是具體怎麼來實現的

2 ZAB協議原始碼實現

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

31160806_blp5

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

2.1 重要的資料介紹

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

  • 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,規則如下:

    就是優先比較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的效果了嗎?它的作用是幹什麼呢?

相關文章