搞懂分散式技術4:ZAB協議概述與選主流程詳解

  • 2019 年 12 月 2 日
  • 筆記

本文內容參考網路,侵刪

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫

https://github.com/h2pl/Java-Tutorial

文章將同步到我的個人部落格:

www.how2playlife.com

該系列博文會告訴你什麼是分散式系統,這對後端工程師來說是很重要的一門學問,我們會逐步了解常見的分散式技術、以及一些較為常見的分散式系統概念,同時也需要進一步了解zookeeper、分散式事務、分散式鎖、負載均衡等技術,以便讓你更完整地了解分散式技術的具體實戰方法,為真正應用分散式技術做好準備。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。

ZAB協議

  1. ZAB協議是專門為zookeeper實現分散式協調功能而設計。zookeeper主要是根據ZAB協議是實現分散式系統數據一致性。
  2. zookeeper根據ZAB協議建立了主備模型完成zookeeper集群中數據的同步。這裡所說的主備系統架構模型是指,在zookeeper集群中,只有一台leader負責處理外部客戶端的事物請求(或寫操作),然後leader伺服器將客戶端的寫操作數據同步到所有的follower節點中。
  1. ZAB的協議核心是在整個zookeeper集群中只有一個節點即Leader將客戶端的寫操作轉化為事物(或提議proposal)。Leader節點再數據寫完之後,將向所有的follower節點發送數據廣播請求(或數據複製),等待所有的follower節點回饋。在ZAB協議中,只要超過半數follower節點回饋OK,Leader節點就會向所有的follower伺服器發送commit消息。即將leader節點上的數據同步到follower節點之上。
  1. ZAB協議中主要有兩種模式,第一是消息廣播模式;第二是崩潰恢復模式

消息廣播模式

  1. 在zookeeper集群中數據副本的傳遞策略就是採用消息廣播模式。zookeeper中數據副本的同步方式與二階段提交相似但是卻又不同。二階段提交的要求協調者必須等到所有的參與者全部回饋ACK確認消息後,再發送commit消息。要求所有的參與者要麼全部成功要麼全部失敗。二階段提交會產生嚴重阻塞問題。
  2. ZAB協議中Leader等待follower的ACK回饋是指」只要半數以上的follower成功回饋即可,不需要收到全部follower回饋」
  3. 圖中展示了消息廣播的具體流程圖
  1. 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消息。
  2. zookeeper採用ZAB協議的核心就是只要有一台伺服器提交了proposal,就要確保所有的伺服器最終都能正確提交proposal。這也是CAP/BASE最終實現一致性的一個體現。
  3. leader伺服器與每個follower之間都有一個單獨的隊列進行收發消息,使用隊列消息可以做到非同步解耦。leader和follower之間只要往隊列中發送了消息即可。如果使用同步方式容易引起阻塞。性能上要下降很多。

崩潰恢復

  1. zookeeper集群中為保證任何所有進程能夠有序的順序執行,只能是leader伺服器接受寫請求,即使是follower伺服器接受到客戶端的請求,也會轉發到leader伺服器進行處理。
  2. 如果leader伺服器發生崩潰,則zab協議要求zookeeper集群進行崩潰恢復和leader伺服器選舉。
  3. ZAB協議崩潰恢復要求滿足如下2個要求: 3.1. 確保已經被leader提交的proposal必須最終被所有的follower伺服器提交。 3.2. 確保丟棄已經被leader出的但是沒有被提交的proposal。
  4. 根據上述要求,新選舉出來的leader不能包含未提交的proposal,即新選舉的leader必須都是已經提交了的proposal的follower伺服器節點。同時,新選舉的leader節點中含有最高的ZXID。這樣做的好處就是可以避免了leader伺服器檢查proposal的提交和丟棄工作。
  5. leader伺服器發生崩潰時分為如下場景: 5.1. leader在提出proposal時未提交之前崩潰,則經過崩潰恢復之後,新選舉的leader一定不能是剛才的leader。因為這個leader存在未提交的proposal。 5.2 leader在發送commit消息之後,崩潰。即消息已經發送到隊列中。經過崩潰恢復之後,參與選舉的follower伺服器(剛才崩潰的leader有可能已經恢復運行,也屬於follower節點範疇)中有的節點已經是消費了隊列中所有的commit消息。即該follower節點將會被選舉為最新的leader。剩下動作就是數據同步過程。

數據同步

  1. 在zookeeper集群中新的leader選舉成功之後,leader會將自身的提交的最大proposal的事物ZXID發送給其他的follower節點。follower節點會根據leader的消息進行回退或者是數據同步操作。最終目的要保證集群中所有節點的數據副本保持一致。
  2. 數據同步完之後,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協議原理

  1. ZAB協議要求每個leader都要經歷三個階段,即發現,同步,廣播。
  2. 發現:即要求zookeeper集群必須選擇出一個leader進程,同時leader會維護一個follower可用列表。將來客戶端可以這follower中的節點進行通訊。
  3. 同步:leader要負責將本身的數據與follower完成同步,做到多副本存儲。這樣也是體現了CAP中高可用和分區容錯。follower將隊列中未處理完的請求消費完成後,寫入本地事物日誌中。
  4. 廣播:leader可以接受客戶端新的proposal請求,將新的proposal請求廣播給所有的follower。

Zookeeper設計目標

  1. zookeeper作為當今最流行的分散式系統應用協調框架,採用zab協議的最大目標就是建立一個高可用可擴展的分散式數據主備系統。即在任何時刻只要leader發生宕機,都能保證分散式系統數據的可靠性和最終一致性。
  2. 深刻理解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過程中的數據一致性
  • 伺服器收到數據後先寫本地文件再進行處理,保證了數據的持久性