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
上述實現中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 事務日誌和快照日誌的持久化和恢復
先來看看持久化過程:
再來講說恢復:
由此咱們能夠看到,在初始化恢復的時候,是會將全部最新的事務日誌做爲已經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的效果了嗎?它的做用是幹什麼呢?