閱讀本文前,請確保您已經(jīng)閱讀了我的文章:ZooKeeper基礎
為了保證ZooKeeper的可用性,在生產(chǎn)環(huán)境中我們使用ZooKeeper集群模式對外提供服務,并且集群規(guī)模至少由3個ZooKeeper節(jié)點組成。
但是,并非節(jié)點越多越好。節(jié)點越多,使用的資源越多,ZooKeeper節(jié)點間花費的通訊成本也越高。
3節(jié)點集群和4節(jié)點集群,我們選擇使用3節(jié)點集群;5節(jié)點集群和6節(jié)點集群,我們選擇使用5節(jié)點集群,以此類推。因為生產(chǎn)環(huán)境為了保證高可用,3節(jié)點集群最多只允許掛1臺,4節(jié)點集群最多也只允許掛1臺(過半原則)。同理5節(jié)點集群最多允許掛2臺,6節(jié)點集群最多也只允許掛2臺。因此出于對資源節(jié)省的考慮,我們應該使用奇數(shù)節(jié)點來滿足相同的高可用性。
如果3個節(jié)點組成集群,其中1個節(jié)點掛掉后,根據(jù)ZooKeeper的Leader選舉機制是可以從另外2個節(jié)點選出一個作為Leader的,集群可以繼續(xù)對外提供服務。
為了保證寫操作的一致性與可用性,Zookeeper專門設計了一種名為原子廣播(ZAB)的支持崩潰恢復的一致性協(xié)議?;谠搮f(xié)議,Zookeeper實現(xiàn)了一種主從模式的系統(tǒng)架構(gòu)來保持集群中各個副本之間的數(shù)據(jù)一致性。
根據(jù)ZAB協(xié)議,所有的寫操作都必須通過Leader完成,Leader寫入本地日志后再復制到所有的Follower節(jié)點。
一旦Leader節(jié)點無法工作,ZAB協(xié)議能夠自動從Follower節(jié)點中重新選出一個合適的替代者,即新的Leader,該過程即為領導選舉。領導選舉過程,是ZAB協(xié)議中最為重要和復雜的過程,ZAB協(xié)議選舉Leader算法被稱為基于TCP的FastLeaderElection算法。
1. 選舉基本原則
1. 選舉投票必須在同一輪次中進行
如果Follower服務選舉輪次不同,不會采納投票。使用選舉輪次的目的稍后再說。
2. 數(shù)據(jù)最新的節(jié)點優(yōu)先成為Leader
數(shù)據(jù)的新舊使用每個節(jié)點的最大zxid來判定,zxid越大認為節(jié)點數(shù)據(jù)約接近Leader的數(shù)據(jù),自然應該成為Leader。
3. 比較server.id,id值大的優(yōu)先成為Leader
如果每個參與競選節(jié)點zxid相同,再使用server.id做比較。server.id是在配置文件中指定的節(jié)點在集群中唯一的id。
最后,當超過半數(shù)的節(jié)點投票都指向的節(jié)點成為Leader,其他參與投票的節(jié)點則成為Follower。
zxid用于標識一次更新操作的Proposal ID。為了保證順序性,該zkid必須單調(diào)遞增。因此Zookeeper使用一個64位的數(shù)來表示,高32位是Leader的epoch,從1開始,每次選出新的Leader,epoch加一。低32位為該epoch內(nèi)的序號,每次epoch變化,都將低32位的序號重置。這樣保證了zkid的全局遞增性。
2. 節(jié)點狀態(tài)
集群內(nèi)的幾點狀態(tài)分為以下4種:
- LOOKING:不確定Leader狀態(tài)。該狀態(tài)下的服務器認為當前集群中沒有Leader,會發(fā)起Leader選舉
- FOLLOWING:跟隨者狀態(tài)。表明當前服務器角色是Follower,并且它知道Leader是誰
- LEADING:領導者狀態(tài)。表明當前服務器角色是Leader,它會維護與Follower間的心跳
- OBSERVING:觀察者狀態(tài)。表明當前服務器角色是Observer,與Folower唯一的不同在于不參與選舉,也不參與集群寫操作時的投票(Observer角色用于讀操作,增加集群吞吐量)。
3. 選舉流程
3.1 自增選舉輪次
Zookeeper規(guī)定所有有效的投票都必須在同一輪次中。每個服務器在開始新一輪投票時,會先對自己維護的logicClock進行自增操作。
每個服務器都會維護一個名為logicClock的變量,用于標識當前選舉的輪次。每開始一次Leader選舉,服務器都會將自己存儲的logicClock執(zhí)行加一操作,并且投票時會附帶上這個logicClock。如果其他服務器收到了一個帶有舊的logicClock的投票,則會直接忽略這個投票。
使用
logicClock記錄輪次的目的在于:考慮一臺機器宕機了,宕機時它的logicClock為2,此時它的投票桶內(nèi)的所有數(shù)據(jù)都是在第2輪次中收到的數(shù)據(jù)。當它恢復后,輪次已經(jīng)來到了4,此時如果讓它直接參與投票是不正確的,因為它的投票桶的數(shù)據(jù)并不是最新數(shù)據(jù),因此它無法投出正確的選票。
logicClock變量只在一次Leader選舉開始時執(zhí)行一次遞增操作,一次選舉中的多輪投票并不會改變logicClock變量的值。
3.2 初始化選票
每個服務器在廣播自己的選票前,會將自己的投票箱清空。
投票箱記錄了所收到的選票。例:服務器2投票給服務器3,服務器3投票給服務器1,服務器1投給了服務器1,則服務器1的投票箱為(2, 3), (3, 1), (1, 1)。票箱中只會記錄每一個投票者的最后一票,如投票者更新了自己的選票,則其它服務器收到該新選票后會在自己票箱中更新該服務器的選票。
每個服務器在進行領導選舉時,會發(fā)送如下關鍵信息:
- logicClock:每個服務器會維護一個自增的整數(shù),名為logicClock,它表示這是該服務器發(fā)起的第多少輪投票
- state:當前服務器的狀態(tài)
- self_id:當前服務器的myid
- self_zxid:當前服務器上所保存的數(shù)據(jù)的最大zxid
- vote_id:被推舉的服務器的myid
- vote_zxid:被推舉的服務器上所保存的數(shù)據(jù)的最大zxid
事實上,每一臺機器投票箱的數(shù)據(jù)結(jié)構(gòu)是一個Map,如下所示:
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
我們可以看到,一個投票包含了選舉輪次,目標Leader的server.id和zxid等信息。
3.2 發(fā)送選票
每臺服務器將給自己的投票放入投票箱,通過廣播把投給自己的票發(fā)送給集群中其他LOOKING狀態(tài)的服務器。
如圖所示,首先每一臺服務器內(nèi)的投票箱都被初始化為自己,(1,1)表示服務器1投給服務器1。另外,服務器1分別向服務器2和服務器3發(fā)送了兩張投票,投票內(nèi)容為(1,1,0)表示(logicClock,leaderServer.id, maxZxid),表示服務器1認為的leader的server.id是1,它的最大zxid為0。
每一臺服務器收到其他服務器的投票后,都會更新自己的投票箱,然后根據(jù)投票箱內(nèi)的投票情況選擇出下一票投給誰。
3.4 接收外部投票
服務器會嘗試從其它服務器獲取投票,并記入自己的投票箱內(nèi)。如果無法獲取任何外部投票,則會確認自己是否與集群中其它服務器保持著有效連接。如果是,則根據(jù)投票箱內(nèi)的內(nèi)容發(fā)送自己的投票;如果否,則馬上與之建立連接。
服務器在收到其他機器的投票后,會根據(jù)投票箱中的所有投票來判斷下次投票投給誰。例如某一時刻服務器1內(nèi)的投票箱內(nèi)的投票是:(1,1),(2,2),(3,3)(服務器1投給1,2投給2,3投給3)。那么服務器1會根據(jù)各個投票內(nèi)包含的zxid和server.id來判斷下一次投票投給誰,并發(fā)送給集群內(nèi)的其他機器。
3.5 判斷選舉輪次
收到外部投票后,首先會根據(jù)投票信息中所包含的logicClock來進行不同處理。
- 如果外部投票的logicClock大于自己的logicClock。說明該服務器的選舉輪次落后于其它服務器的選舉輪次,立即清空自己的投票箱并將自己的logicClock更新為收到的logicClock,然后再對比自己之前的投票與收到的投票以確定是否需要變更自己的投票,最終再次將自己的投票廣播出去。
- 外部投票的logicClock小于自己的logicClock。當前服務器直接忽略該投票,繼續(xù)處理下一個投票。
- 外部投票的logickClock與自己的相等。當時進行選票PK。
3.6 選票PK
選票PK是基于(vote_id, vote_zxid)的對比
- 外部投票的logicClock大于自己的logicClock,則將自己的logicClock及自己的選票的logicClock變更為收到的logicClock
- 若logicClock一致,則對比二者的vote_zxid,若外部投票的vote_zxid比較大,則將自己的票中的vote_zxid與vote_myid更新為收到的票中的vote_zxid與vote_myid并廣播出去,另外將收到的票及自己更新后的票放入自己的票箱。
- 若二者vote_zxid一致,則比較二者的vote_myid,若外部投票的vote_myid比較大,則將自己的票中的vote_myid更新為收到的票中的vote_myid并廣播出去,另外將收到的票及自己更新后的票放入自己的票箱
3.7 統(tǒng)計選票
如果已經(jīng)確定有過半服務器認可了自己的投票(可能是更新后的投票),則終止投票。否則繼續(xù)接收其它服務器的投票,直到過半服務器的投票都與自己的投票相同。
3.8 更新服務器狀態(tài)
投票終止后,服務器開始更新自身狀態(tài)。若過半的票投給了自己,則將自己的服務器狀態(tài)更新為LEADING,否則將自己的狀態(tài)更新為FOLLOWING