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
discovery:
synchronization:
broadcast
上面簡單的描述了上述4個過程,這4個過程的較長的描述在zab的paper中可以找到,但是我看了之後基本和zab的源碼實作上相差有點大,這裡就不再提zab paper對上述4個過程的描述了,下面會詳細的說明zookeeper源碼中是具體怎麼來實作的
先看下zookeeper整體的實作情況,如下圖所示
上述實作中recovery phase包含了zab協定中的discovery和synchronization。
加上前面已經介紹的幾個名詞
long lastprocessedzxid:最後一次commit的事務請求的zxid
linkedlist
concurrentmap outstandingproposals
concurrentlinkedqueue 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中删除
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,規則如下:
就是優先比較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上的資料資訊
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
如果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.1中的内容
先來看看持久化過程:
broadcast過程的持久化
leader shutdown過程的持久化
再來說說恢複:
事務快照的恢複
事務日志的恢複
由此我們可以看到,在初始化恢複的時候,是會将所有最新的事務日志作為已經commit的事務來處理的
也就是說這裡面可能會有部分事務日志還沒真實送出,而這裡全部當做已送出來處理。這個處理簡單粗暴了一些,而raft對老資料的恢複則控制的更加嚴謹一些。
一旦leader挂了,上述leader的2個集合
就無效了。他們并不在leader恢複的時候起作用,而是在系統正常執行,而某個follower挂了又恢複的時候起作用。
我們可以看到在上述2.3的恢複過程中,會首先進行快照日志和事務日志的恢複,然後再補充leader的上述2個資料中的内容。
目前leader和follower之間的同步是通過bio方式來進行的,一旦該鍊路出現異常則會關閉該鍊路,重新與leader建立連接配接,重新同步最新的資料
用戶端收到ok回複,會不會丢失資料?
用戶端沒有收到ok回複,會不會多存儲資料?
用戶端如果收到ok回複,說明已經過半複制了,則在leader選舉中肯定會包含該請求對應的事務日志,則不會丢失該資料
用戶端連接配接的leader或者follower挂了,用戶端沒有收到ok回複,目前是可能丢失也可能沒丢失,因為伺服器端的處理也很簡單粗暴,對于未來leader上的事務日志都會當做送出來處理的,即都會被應用到記憶體樹中。
同時目前zookeeper的原生用戶端也沒有進行重試,伺服器端也沒有對重試進行檢查。這一部分到下一篇再詳細探讨與raft的差別
本文有很多細節,難免可能疏漏,還請指正。
這裡留個問題供大家思考下:
raft每次執行appendentries rpc的時候,都會帶上目前leader的新term,來防止舊的leader的舊term來執行相關操作,而zookeeper的peerepoch呢?達到防止舊leader的效果了嗎?它的作用是幹什麼呢?
下一篇就來對比一下raft和zab之間的細微的差別
歡迎關注微信公衆号:乒乓狂魔