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整體的實現情況,如下圖所示
上述實現中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的效果了嗎?它的作用是幹什麼呢?