搞懂分散式技術4:ZAB協議概述與選主流程詳解
- 2019 年 12 月 2 日
- 筆記
本文內容參考網路,侵刪
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫
https://github.com/h2pl/Java-Tutorial
文章將同步到我的個人部落格:
www.how2playlife.com
該系列博文會告訴你什麼是分散式系統,這對後端工程師來說是很重要的一門學問,我們會逐步了解常見的分散式技術、以及一些較為常見的分散式系統概念,同時也需要進一步了解zookeeper、分散式事務、分散式鎖、負載均衡等技術,以便讓你更完整地了解分散式技術的具體實戰方法,為真正應用分散式技術做好準備。
如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。
ZAB協議
- ZAB協議是專門為zookeeper實現分散式協調功能而設計。zookeeper主要是根據ZAB協議是實現分散式系統數據一致性。
- zookeeper根據ZAB協議建立了主備模型完成zookeeper集群中數據的同步。這裡所說的主備系統架構模型是指,在zookeeper集群中,只有一台leader負責處理外部客戶端的事物請求(或寫操作),然後leader伺服器將客戶端的寫操作數據同步到所有的follower節點中。

- ZAB的協議核心是在整個zookeeper集群中只有一個節點即Leader將客戶端的寫操作轉化為事物(或提議proposal)。Leader節點再數據寫完之後,將向所有的follower節點發送數據廣播請求(或數據複製),等待所有的follower節點回饋。在ZAB協議中,只要超過半數follower節點回饋OK,Leader節點就會向所有的follower伺服器發送commit消息。即將leader節點上的數據同步到follower節點之上。

- ZAB協議中主要有兩種模式,第一是消息廣播模式;第二是崩潰恢復模式
消息廣播模式
- 在zookeeper集群中數據副本的傳遞策略就是採用消息廣播模式。zookeeper中數據副本的同步方式與二階段提交相似但是卻又不同。二階段提交的要求協調者必須等到所有的參與者全部回饋ACK確認消息後,再發送commit消息。要求所有的參與者要麼全部成功要麼全部失敗。二階段提交會產生嚴重阻塞問題。
- ZAB協議中Leader等待follower的ACK回饋是指」只要半數以上的follower成功回饋即可,不需要收到全部follower回饋」
- 圖中展示了消息廣播的具體流程圖

- zookeeper中消息廣播的具體步驟如下: 4.1. 客戶端發起一個寫操作請求 4.2. Leader伺服器將客戶端的request請求轉化為事物proposql提案,同時為每個proposal分配一個全局唯一的ID,即ZXID。 4.3. leader伺服器與每個follower之間都有一個隊列,leader將消息發送到該隊列 4.4. follower機器從隊列中取出消息處理完(寫入本地事物日誌中)畢後,向leader伺服器發送ACK確認。 4.5. leader伺服器收到半數以上的follower的ACK後,即認為可以發送commit 4.6. leader向所有的follower伺服器發送commit消息。
- zookeeper採用ZAB協議的核心就是只要有一台伺服器提交了proposal,就要確保所有的伺服器最終都能正確提交proposal。這也是CAP/BASE最終實現一致性的一個體現。
- leader伺服器與每個follower之間都有一個單獨的隊列進行收發消息,使用隊列消息可以做到非同步解耦。leader和follower之間只要往隊列中發送了消息即可。如果使用同步方式容易引起阻塞。性能上要下降很多。
崩潰恢復
- zookeeper集群中為保證任何所有進程能夠有序的順序執行,只能是leader伺服器接受寫請求,即使是follower伺服器接受到客戶端的請求,也會轉發到leader伺服器進行處理。
- 如果leader伺服器發生崩潰,則zab協議要求zookeeper集群進行崩潰恢復和leader伺服器選舉。
- ZAB協議崩潰恢復要求滿足如下2個要求: 3.1. 確保已經被leader提交的proposal必須最終被所有的follower伺服器提交。 3.2. 確保丟棄已經被leader出的但是沒有被提交的proposal。
- 根據上述要求,新選舉出來的leader不能包含未提交的proposal,即新選舉的leader必須都是已經提交了的proposal的follower伺服器節點。同時,新選舉的leader節點中含有最高的ZXID。這樣做的好處就是可以避免了leader伺服器檢查proposal的提交和丟棄工作。
- leader伺服器發生崩潰時分為如下場景: 5.1. leader在提出proposal時未提交之前崩潰,則經過崩潰恢復之後,新選舉的leader一定不能是剛才的leader。因為這個leader存在未提交的proposal。 5.2 leader在發送commit消息之後,崩潰。即消息已經發送到隊列中。經過崩潰恢復之後,參與選舉的follower伺服器(剛才崩潰的leader有可能已經恢復運行,也屬於follower節點範疇)中有的節點已經是消費了隊列中所有的commit消息。即該follower節點將會被選舉為最新的leader。剩下動作就是數據同步過程。
數據同步
- 在zookeeper集群中新的leader選舉成功之後,leader會將自身的提交的最大proposal的事物ZXID發送給其他的follower節點。follower節點會根據leader的消息進行回退或者是數據同步操作。最終目的要保證集群中所有節點的數據副本保持一致。
- 數據同步完之後,zookeeper集群如何保證新選舉的leader分配的ZXID是全局唯一呢?這個就要從ZXID的設計談起。 2.1 ZXID是一個長度64位的數字,其中低32位是按照數字遞增,即每次客戶端發起一個proposal,低32位的數字簡單加1。高32位是leader周期的epoch編號,至於這個編號如何產生(我也沒有搞明白),每當選舉出一個新的leader時,新的leader就從本地事物日誌中取出ZXID,然後解析出高32位的epoch編號,進行加1,再將低32位的全部設置為0。這樣就保證了每次新選舉的leader後,保證了ZXID的唯一性而且是保證遞增的。

ZAB協議原理
- ZAB協議要求每個leader都要經歷三個階段,即發現,同步,廣播。
- 發現:即要求zookeeper集群必須選擇出一個leader進程,同時leader會維護一個follower可用列表。將來客戶端可以這follower中的節點進行通訊。
- 同步:leader要負責將本身的數據與follower完成同步,做到多副本存儲。這樣也是體現了CAP中高可用和分區容錯。follower將隊列中未處理完的請求消費完成後,寫入本地事物日誌中。
- 廣播:leader可以接受客戶端新的proposal請求,將新的proposal請求廣播給所有的follower。
Zookeeper設計目標
- zookeeper作為當今最流行的分散式系統應用協調框架,採用zab協議的最大目標就是建立一個高可用可擴展的分散式數據主備系統。即在任何時刻只要leader發生宕機,都能保證分散式系統數據的可靠性和最終一致性。
- 深刻理解ZAB協議,才能更好的理解zookeeper對於分散式系統建設的重要性。以及為什麼採用zookeeper就能保證分散式系統中數據最終一致性,服務的高可用性。
Zab與Paxos Zab的作者認為Zab與paxos並不相同,只所以沒有採用Paxos是因為Paxos保證不了全序順序:Because multiple leaders can propose a value for a given instance two problems arise. First, proposals can conflict. Paxos uses ballots to detect and resolve conflicting proposals. Second, it is not enough to know that a given instance number has been committed, processes must also be able to fi gure out which value has been committed. Paxos演算法的確是不關心請求之間的邏輯順序,而只考慮數據之間的全序,但很少有人直接使用paxos演算法,都會經過一定的簡化、優化。
Paxos演算法優化 Paxos演算法在出現競爭的情況下,其收斂速度很慢,甚至可能出現活鎖的情況,例如當有三個及三個以上的proposer在發送prepare請求後,很難有一個proposer收到半數以上的回復而不斷地執行第一階段的協議。因此,為了避免競爭,加快收斂的速度,在演算法中引入了一個Leader這個角色,在正常情況下同時應該最多只能有一個參與者扮演Leader角色,而其它的參與者則扮演Acceptor的角色。在這種優化演算法中,只有Leader可以提出議案,從而避免了競爭使得演算法能夠快速地收斂而趨於一致;而為了保證Leader的健壯性,又引入了Leader選舉,再考慮到同步的階段,漸漸的你會發現對Paxos演算法的簡化和優化已經和上面介紹的ZAB協議很相似了。
總結 Google的粗粒度鎖服務Chubby的設計開發者Burrows曾經說過:「所有一致性協議本質上要麼是Paxos要麼是其變體」。這句話還是有一定道理的,ZAB本質上就是Paxos的一種簡化形式。
ZAB與FastLeaderElection選主演算法流程詳解
這篇主要分析leader的選主機制,zookeeper提供了三種方式:
- LeaderElection
- AuthFastLeaderElection
- FastLeaderElection
默認的演算法是FastLeaderElection,所以這篇主要分析它的選舉機制。
選擇機制中的概念
伺服器ID
比如有三台伺服器,編號分別是1,2,3。
編號越大在選擇演算法中的權重越大。
數據ID
伺服器中存放的最大數據ID.
值越大說明數據越新,在選舉演算法中數據越新權重越大。
邏輯時鐘
或者叫投票的次數,同一輪投票過程中的邏輯時鐘值是相同的。每投完一次票這個數據就會增加,然後與接收到的其它伺服器返回的投票資訊中的數值相比,根據不同的值做出不同的判斷。
選舉狀態
- LOOKING,競選狀態。
- FOLLOWING,隨從狀態,同步leader狀態,參與投票。
- OBSERVING,觀察狀態,同步leader狀態,不參與投票。
- LEADING,領導者狀態。
選舉消息內容
在投票完成後,需要將投票資訊發送給集群中的所有伺服器,它包含如下內容。
- 伺服器ID
- 數據ID
- 邏輯時鐘
- 選舉狀態
選舉流程圖
因為每個伺服器都是獨立的,在啟動時均從初始狀態開始參與選舉,下面是簡易流程圖。

下面詳細解釋一下這個流程:
首先給出幾個名詞定義:
(1)Serverid:在配置server時,給定的伺服器的標示id。
(2)Zxid:伺服器在運行時產生的數據id,zxid越大,表示數據越新。
(3)Epoch:選舉的輪數,即邏輯時鐘。隨著選舉的輪數++
(4)Server狀態:LOOKING,FOLLOWING,OBSERVING,LEADING
步驟:
一、 Server剛啟動(宕機恢復或者剛啟動)準備加入集群,此時讀取自身的zxid等資訊。
二、 所有Server加入集群時都會推薦自己為leader,然後將(leader id 、 zixd 、 epoch)作為廣播資訊,廣播到集群中所有的伺服器(Server)。然後等待集群中的伺服器返回資訊。
三、 收到集群中其他伺服器返回的資訊,此時要分為兩類:該伺服器處於looking狀態,或者其他狀態。
(1) 伺服器處於looking狀態
首先判斷邏輯時鐘 Epoch:
a) 如果接收到Epoch大於自己目前的邏輯時鐘(說明自己所保存的邏輯時鐘落伍了)。更新本機邏輯時鐘Epoch,同時 Clear其他服務發送來的選舉數據(這些數據已經OUT了)。然後判斷是否需要更新當前自己的選舉情況(一開始選擇的leader id 是自己)
判斷規則rules judging:保存的zxid最大值和leader Serverid來進行判斷的。先看數據zxid,數據zxid大者勝出;其次再判斷leaderServerid, leader Serverid大者勝出;然後再將自身最新的選舉結果(也就是上面提到的三種數據(leader Serverid,Zxid,Epoch)廣播給其他server)
b) 如果接收到的Epoch小於目前的邏輯時鐘。說明對方處於一個比較OUT的選舉輪數,這時只需要將自己的 (leader Serverid,Zxid,Epoch)發送給他即可。
c) 如果接收到的Epoch等於目前的邏輯時鐘。再根據a)中的判斷規則,將自身的最新選舉結果廣播給其他 server。
同時Server還要處理2種情況:
a) 如果Server接收到了其他所有伺服器的選舉資訊,那麼則根據這些選舉資訊確定自己的狀態(Following,Leading),結束Looking,退出選舉。
b) 即使沒有收到所有伺服器的選舉資訊,也可以判斷一下根據以上過程之後最新的選舉leader是不是得到了超過半數以上伺服器的支援,如果是則嘗試接受最新數據,倘若沒有最新的數據到來,說明大家都已經默認了這個結果,同樣也設置角色退出選舉過程。
(2) 伺服器處於其他狀態(Following, Leading)
a) 如果邏輯時鐘Epoch相同,將該數據保存到recvset,如果所接收伺服器宣稱自己是leader,那麼將判斷是不是有半數以上的伺服器選舉它,如果是則設置選舉狀態退出選舉過程
b) 否則這是一條與當前邏輯時鐘不符合的消息,那麼說明在另一個選舉過程中已經有了選舉結果,於是將該選舉結果加入到outofelection集合中,再根據outofelection來判斷是否可以結束選舉,如果可以也是保存邏輯時鐘,設置選舉狀態,退出選舉過程。
以上就是FAST選舉過程。

以上就是我自己配置的Zookeeper選主日誌,從一開始LOOKING,然後new election, my id = 1, proposedzxid=0x0 也就是選自己為Leader,之後廣播選舉並重複之前Fast選主演算法,最終確定Leader。
源碼分析
QuorumPeer
主要看這個類,只有LOOKING狀態才會去執行選舉演算法。每個伺服器在啟動時都會選擇自己做為領導,然後將投票資訊發送出去,循環一直到選舉出領導為止。
public void run() { //....... try { while (running) { switch (getPeerState()) { case LOOKING: if (Boolean.getBoolean("readonlymode.enabled")) { //... try { //投票給自己... setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { //... } finally { //... } } else { try { //... setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { //... } } break; case OBSERVING: //... break; case FOLLOWING: //... break; case LEADING: //... break; } } } finally { //... } }
FastLeaderElection
它是zookeeper默認提供的選舉演算法,核心方法如下:具體的可以與本文上面的流程圖對照。
public Vote lookForLeader() throws InterruptedException { //... try { HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>(); int notTimeout = finalizeWait; synchronized(this){ //給自己投票 logicalclock.incrementAndGet(); updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } //將投票資訊發送給集群中的每個伺服器 sendNotifications(); //循環,如果是競選狀態一直到選舉出結果 while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS); //沒有收到投票資訊 if(n == null){ if(manager.haveDelivered()){ sendNotifications(); } else { manager.connectAll(); } //... } //收到投票資訊 else if (self.getCurrentAndNextConfigVoters().contains(n.sid)) { switch (n.state) { case LOOKING: // 判斷投票是否過時,如果過時就清除之前已經接收到的資訊 if (n.electionEpoch > logicalclock.get()) { logicalclock.set(n.electionEpoch); recvset.clear(); //更新投票資訊 if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { updateProposal(n.leader, n.zxid, n.peerEpoch); } else { updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } //發送投票資訊 sendNotifications(); } else if (n.electionEpoch < logicalclock.get()) { //忽略 break; } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { //更新投票資訊 updateProposal(n.leader, n.zxid, n.peerEpoch); sendNotifications(); } recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); //判斷是否投票結束 if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) { // Verify if there is any change in the proposed leader while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null){ if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){ recvqueue.put(n); break; } } if (n == null) { self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); Vote endVote = new Vote(proposedLeader, proposedZxid, proposedEpoch); leaveInstance(endVote); return endVote; } } break; case OBSERVING: //忽略 break; case FOLLOWING: case LEADING: //如果是同一輪投票 if(n.electionEpoch == logicalclock.get()){ recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); //判斷是否投票結束 if(termPredicate(recvset, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)) && checkLeader(outofelection, n.leader, n.electionEpoch)) { self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState()); Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch); leaveInstance(endVote); return endVote; } } //記錄投票已經完成 outofelection.put(n.sid, new Vote(n.leader, IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state)); if (termPredicate(outofelection, new Vote(n.leader, IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state)) && checkLeader(outofelection, n.leader, IGNOREVALUE)) { synchronized(this){ logicalclock.set(n.electionEpoch); self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState()); } Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch); leaveInstance(endVote); return endVote; } break; default: //忽略 break; } } else { LOG.warn("Ignoring notification from non-cluster member " + n.sid); } } return null; } finally { //... } }
判斷是否已經勝出
默認是採用投票數大於半數則勝出的邏輯。
選舉流程簡述
目前有5台伺服器,每台伺服器均沒有數據,它們的編號分別是1,2,3,4,5,按編號依次啟動,它們的選擇舉過程如下:
- 伺服器1啟動,給自己投票,然後發投票資訊,由於其它機器還沒有啟動所以它收不到回饋資訊,伺服器1的狀態一直屬於Looking。
- 伺服器2啟動,給自己投票,同時與之前啟動的伺服器1交換結果,由於伺服器2的編號大所以伺服器2勝出,但此時投票數沒有大於半數,所以兩個伺服器的狀態依然是LOOKING。
- 伺服器3啟動,給自己投票,同時與之前啟動的伺服器1,2交換資訊,由於伺服器3的編號最大所以伺服器3勝出,此時投票數正好大於半數,所以伺服器3成為領導者,伺服器1,2成為小弟。
- 伺服器4啟動,給自己投票,同時與之前啟動的伺服器1,2,3交換資訊,儘管伺服器4的編號大,但之前伺服器3已經勝出,所以伺服器4隻能成為小弟。
- 伺服器5啟動,後面的邏輯同伺服器4成為小弟。
幾種領導選舉場景
集群啟動領導選舉
初始投票給自己 集群剛啟動時,所有伺服器的logicClock都為1,zxid都為0。
各伺服器初始化後,都投票給自己,並將自己的一票存入自己的票箱,如下圖所示。

在上圖中,(1, 1, 0)第一位數代表投出該選票的伺服器的logicClock,第二位數代表被推薦的伺服器的myid,第三位代表被推薦的伺服器的最大的zxid。由於該步驟中所有選票都投給自己,所以第二位的myid即是自己的myid,第三位的zxid即是自己的zxid。
此時各自的票箱中只有自己投給自己的一票。
更新選票 伺服器收到外部投票後,進行選票PK,相應更新自己的選票並廣播出去,並將合適的選票存入自己的票箱,如下圖所示。

伺服器1收到伺服器2的選票(1, 2, 0)和伺服器3的選票(1, 3, 0)後,由於所有的logicClock都相等,所有的zxid都相等,因此根據myid判斷應該將自己的選票按照伺服器3的選票更新為(1, 3, 0),並將自己的票箱全部清空,再將伺服器3的選票與自己的選票存入自己的票箱,接著將自己更新後的選票廣播出去。此時伺服器1票箱內的選票為(1, 3),(3, 3)。
同理,伺服器2收到伺服器3的選票後也將自己的選票更新為(1, 3, 0)並存入票箱然後廣播。此時伺服器2票箱內的選票為(2, 3),(3, ,3)。
伺服器3根據上述規則,無須更新選票,自身的票箱內選票仍為(3, 3)。
伺服器1與伺服器2更新後的選票廣播出去後,由於三個伺服器最新選票都相同,最後三者的票箱內都包含三張投給伺服器3的選票。
根據選票確定角色 根據上述選票,三個伺服器一致認為此時伺服器3應該是Leader。因此伺服器1和2都進入FOLLOWING狀態,而伺服器3進入LEADING狀態。之後Leader發起並維護與Follower間的心跳。

Follower重啟
Follower重啟投票給自己 Follower重啟,或者發生網路分區後找不到Leader,會進入LOOKING狀態並發起新的一輪投票。

發現已有Leader後成為Follower 伺服器3收到伺服器1的投票後,將自己的狀態LEADING以及選票返回給伺服器1。伺服器2收到伺服器1的投票後,將自己的狀態FOLLOWING及選票返回給伺服器1。此時伺服器1知道伺服器3是Leader,並且通過伺服器2與伺服器3的選票可以確定伺服器3確實得到了超過半數的選票。因此伺服器1進入FOLLOWING狀態。

Leader重啟
Follower發起新投票 Leader(伺服器3)宕機後,Follower(伺服器1和2)發現Leader不工作了,因此進入LOOKING狀態並發起新的一輪投票,並且都將票投給自己。

廣播更新選票 伺服器1和2根據外部投票確定是否要更新自身的選票。這裡有兩種情況
- 伺服器1和2的zxid相同。例如在伺服器3宕機前伺服器1與2完全與之同步。此時選票的更新主要取決於myid的大小
- 伺服器1和2的zxid不同。在舊Leader宕機之前,其所主導的寫操作,只需過半伺服器確認即可,而不需所有伺服器確認。換句話說,伺服器1和2可能一個與舊Leader同步(即zxid與之相同)另一個不同步(即zxid比之小)。此時選票的更新主要取決於誰的zxid較大
在上圖中,伺服器1的zxid為11,而伺服器2的zxid為10,因此伺服器2將自身選票更新為(3, 1, 11),如下圖所示。

選出新Leader 經過上一步選票更新後,伺服器1與伺服器2均將選票投給伺服器1,因此伺服器2成為Follower,而伺服器1成為新的Leader並維護與伺服器2的心跳。

舊Leader恢復後發起選舉 舊的Leader恢復後,進入LOOKING狀態並發起新一輪領導選舉,並將選票投給自己。此時伺服器1會將自己的LEADING狀態及選票(3, 1, 11)返回給伺服器3,而伺服器2將自己的FOLLOWING狀態及選票(3, 1, 11)返回給伺服器3。如下圖所示。

舊Leader成為Follower 伺服器3了解到Leader為伺服器1,且根據選票了解到伺服器1確實得到過半伺服器的選票,因此自己進入FOLLOWING狀態。

一致性保證
ZAB協議保證了在Leader選舉的過程中,已經被Commit的數據不會丟失,未被Commit的數據對客戶端不可見。
Commit過的數據不丟失
Failover前狀態 為更好演示Leader Failover過程,本例中共使用5個Zookeeper伺服器。A作為Leader,共收到P1、P2、P3三條消息,並且Commit了1和2,且總體順序為P1、P2、C1、P3、C2。根據順序性原則,其它Follower收到的消息的順序肯定與之相同。其中B與A完全同步,C收到P1、P2、C1,D收到P1、P2,E收到P1,如下圖所示。

這裡要注意
- 由於A沒有C3,意味著收到P3的伺服器的總個數不會超過一半,也即包含A在內最多只有兩台伺服器收到P3。在這裡A和B收到P3,其它伺服器均未收到P3
- 由於A已寫入C1、C2,說明它已經Commit了P1、P2,因此整個集群有超過一半的伺服器,即最少三個伺服器收到P1、P2。在這裡所有伺服器都收到了P1,除E外其它伺服器也都收到了P2
選出新Leader 舊Leader也即A宕機後,其它伺服器根據上述FastLeaderElection演算法選出B作為新的Leader。C、D和E成為Follower且以B為Leader後,會主動將自己最大的zxid發送給B,B會將Follower的zxid與自身zxid間的所有被Commit過的消息同步給Follower,如下圖所示。

在上圖中
- P1和P2都被A Commit,因此B會通過同步保證P1、P2、C1與C2都存在於C、D和E中
- P3由於未被A Commit,同時倖存的所有伺服器中P3未存在於大多數據伺服器中,因此它不會被同步到其它Follower
通知Follower可對外服務 同步完數據後,B會向D、C和E發送NEWLEADER命令並等待大多數伺服器的ACK(下圖中D和E已返回ACK,加上B自身,已經占集群的大多數),然後向所有伺服器廣播UPTODATE命令。收到該命令後的伺服器即可對外提供服務。

未Commit過的消息對客戶端不可見
在上例中,P3未被A Commit過,同時因為沒有過半的伺服器收到P3,因此B也未Commit P3(如果有過半伺服器收到P3,即使A未Commit P3,B會主動Commit P3,即C3),所以它不會將P3廣播出去。
具體做法是,B在成為Leader後,先判斷自身未Commit的消息(本例中即P3)是否存在於大多數伺服器中從而決定是否要將其Commit。然後B可得出自身所包含的被Commit過的消息中的最小zxid(記為min_zxid)與最大zxid(記為max_zxid)。C、D和E向B發送自身Commit過的最大消息zxid(記為max_zxid)以及未被Commit過的所有消息(記為zxid_set)。B根據這些資訊作出如下操作
- 如果Follower的max_zxid與Leader的max_zxid相等,說明該Follower與Leader完全同步,無須同步任何數據
- 如果Follower的max_zxid在Leader的(min_zxid,max_zxid)範圍內,Leader會通過TRUNC命令通知Follower將其zxid_set中大於Follower的max_zxid(如果有)的所有消息全部刪除
上述操作保證了未被Commit過的消息不會被Commit從而對外不可見。
上述例子中Follower上並不存在未被Commit的消息。但可考慮這種情況,如果將上述例子中的伺服器數量從五增加到七,伺服器F包含P1、P2、C1、P3,伺服器G包含P1、P2。此時伺服器F、A和B都包含P3,但是因為票數未過半,因此B作為Leader不會Commit P3,而會通過TRUNC命令通知F刪除P3。如下圖所示。

總結
- 由於使用主從複製模式,所有的寫操作都要由Leader主導完成,而讀操作可通過任意節點完成,因此Zookeeper讀性能遠好於寫性能,更適合讀多寫少的場景
- 雖然使用主從複製模式,同一時間只有一個Leader,但是Failover機制保證了集群不存在單點失敗(SPOF)的問題
- ZAB協議保證了Failover過程中的數據一致性
- 伺服器收到數據後先寫本地文件再進行處理,保證了數據的持久性