面試官上來就問 ZAB 協(xié)議,瑟瑟發(fā)抖…
Zookeeper 是通過 ZAB 一致性協(xié)議來實現(xiàn)分布式事務(wù)的最終一致性。
ZAB 協(xié)議介紹
ZAB 全稱為 Zookeeper Atomic Broadcast(Zookeeper 原子廣播協(xié)議)
ZAB 協(xié)議是為分布式協(xié)調(diào)服務(wù)ZooKeeper專門設(shè)計的一種支持崩潰恢復(fù)的一致性協(xié)議。基于該協(xié)議,ZooKeeper 實現(xiàn)了一種主從模式的系統(tǒng)架構(gòu)來保持集群中各個副本之間的數(shù)據(jù)一致性。
ZAB的消息廣播過程使用的是原子廣播協(xié)議,類似于二階段提交。針對客戶端的請求,Leader服務(wù)器生成對應(yīng)的事務(wù)提議,并將其發(fā)送給集群中所有的 Follower 服務(wù)器。然后收集各自的選票,最后進(jìn)行事務(wù)提交。如圖:

在 ZAB 協(xié)議中二階段提交,移除了中斷邏輯。所有的 Follower 服務(wù)器要么正常反饋 Leader 提出的事務(wù)提議,要么就拋棄 Leader 服務(wù)器。同時,我們可以在過半的 Follower 服務(wù)器已經(jīng)反饋 ACK 后,就開始提交事務(wù)提議了。
Leader 服務(wù)器會為事務(wù)提議分配一個全局單調(diào)遞增的 ID,稱為事務(wù) ID(ZXID)。由于 ZAB 協(xié)議需要保證每一個消息嚴(yán)格的因果關(guān)系,因此需要將每一個事務(wù)提議按照其 ZXID 的先后順序進(jìn)行處理。
在消息廣播過程中,Leader 服務(wù)器會為每一個 Follower 服務(wù)器分配一個隊列,然后將事務(wù)提議依次放入到這些隊列中去,并且根據(jù) FIFO 的策略進(jìn)行消息發(fā)送。
每一個 Follower 服務(wù)器接收到這個事務(wù)提議后,會把該事務(wù)提議以事務(wù)日志的形式寫入到本地磁盤中,并且寫入成功后,反饋給 Leader 服務(wù)器 ACK。
當(dāng) Leader 服務(wù)器收到過半 Follower 服務(wù)器的 ACK,就發(fā)送一個 COMMIT 消息,同時 Leader 自身完成事務(wù)提交,F(xiàn)ollower 服務(wù)器接收到 COMMIT 消息后,也進(jìn)行事務(wù)提交。
之所以采用原子廣播協(xié)議協(xié)議,是為了保證分布式數(shù)據(jù)一致性。過半的節(jié)點數(shù)據(jù)保存一致性。
消息廣播
你可以認(rèn)為消息廣播機(jī)制是簡化版的 2PC協(xié)議,就是通過如下的機(jī)制保證事務(wù)的順序一致性的。

客戶端提交事務(wù)請求時 Leader 節(jié)點為每一個請求生成一個事務(wù) Proposal,將其發(fā)送給集群中所有的 Follower 節(jié)點,收到過半 Follower的反饋后開始對事務(wù)進(jìn)行提交,ZAB 協(xié)議使用了原子廣播協(xié)議;在 ZAB 協(xié)議中只需要得到過半的 Follower 節(jié)點反饋 Ack 就可以對事務(wù)進(jìn)行提交,這也導(dǎo)致了 Leader 節(jié)點崩潰后可能會出現(xiàn)數(shù)據(jù)不一致的情況,ZAB 使用了崩潰恢復(fù)來處理數(shù)字不一致問題;消息廣播使用了TCP 協(xié)議進(jìn)行通訊所有保證了接受和發(fā)送事務(wù)的順序性。廣播消息時 Leader 節(jié)點為每個事務(wù) Proposal分配一個全局遞增的 ZXID(事務(wù)ID),每個事務(wù) Proposal 都按照 ZXID 順序來處理;
Leader 節(jié)點為每一個 Follower 節(jié)點分配一個隊列按事務(wù) ZXID 順序放入到隊列中,且根據(jù)隊列的規(guī)則 FIFO 來進(jìn)行事務(wù)的發(fā)送。Follower節(jié)點收到事務(wù) Proposal 后會將該事務(wù)以事務(wù)日志方式寫入到本地磁盤中,成功后反饋 Ack 消息給 Leader 節(jié)點,Leader 在接收到過半Follower 節(jié)點的 Ack 反饋后就會進(jìn)行事務(wù)的提交,以此同時向所有的 Follower 節(jié)點廣播 Commit 消息,F(xiàn)ollower 節(jié)點收到 Commit 后開始對事務(wù)進(jìn)行提交;
崩潰恢復(fù)
消息廣播過程中,Leader 崩潰了還能保證數(shù)據(jù)一致嗎?當(dāng) Leader 崩潰會進(jìn)入崩潰恢復(fù)模式。其實主要是對如下兩種情況的處理。
- Leader 在復(fù)制數(shù)據(jù)給所有 Follwer 之后崩潰,怎么處理?
- Leader 在收到 Ack 并提交了自己,同時發(fā)送了部分 commit 出去之后崩潰,怎么處理?
針對此問題,ZAB 定義了 2 個原則:
- ZAB 協(xié)議確保執(zhí)行那些已經(jīng)在 Leader 提交的事務(wù)最終會被所有服務(wù)器提交。
- ZAB 協(xié)議確保丟棄那些只在 Leader 提出/復(fù)制,但沒有提交的事務(wù)。
至于如何實現(xiàn)確保提交已經(jīng)被 Leader 提交的事務(wù),同時丟棄已經(jīng)被跳過的事務(wù)呢?核心是通過 ZXID 來進(jìn)行處理。在崩潰過后進(jìn)行恢復(fù)的時候會選擇最大的 zxid 作為恢復(fù)的快照。這樣的好處是: 可以省略事務(wù)提交的檢查和事務(wù)的丟棄工作以提升效率
數(shù)據(jù)同步
完成Leader選舉之后,在正式開始工作之前,Leader服務(wù)器會去確認(rèn)事務(wù)日志中所有事務(wù)提議(指已經(jīng)提交的事務(wù)提議)是否都已經(jīng)被過半的機(jī)器提交了,即是否完成數(shù)據(jù)同步。下面是ZAB協(xié)議的 數(shù)據(jù)同步過程。
Leader服務(wù)器為每一個Follower服務(wù)器準(zhǔn)備一個隊列,將那些沒有被Follower服務(wù)器同步的事務(wù)以事務(wù)提議的形式逐個發(fā)送給Follower服務(wù)器,并在每一個事務(wù)提議消息后面發(fā)送一個commit消息,表示該事務(wù)已被提交。
等到Follower服務(wù)器將所有其未同步的事務(wù)提議都從Leader服務(wù)器上面同步過來,并且應(yīng)用到本地數(shù)據(jù)庫后,Leader服務(wù)器就會將該Follower服務(wù)器加入到真正可用的Follower列表中。
ZXID 的設(shè)計
ZXID 是一個64位的數(shù)字, 如下圖所示。

其中低 32 位是一個簡單的單調(diào)遞增的計數(shù)器,Leader 服務(wù)器產(chǎn)生一個新的事務(wù)提議的時候,都會對該計數(shù)器 +1。
高 32 位,用來區(qū)分不同的 Leader 服務(wù)器。具體做法是,每選舉產(chǎn)生一個新的 Leader 服務(wù)器,就會從 Leader 服務(wù)器的本地日志中取出一個最大的 ZXID,生成對應(yīng)的 epoch 值,然后再進(jìn)行加1操作,之后就會以該值作為新的 epoch。并將低 32 位從 0 開始生成 ZXID。(我理解這里的 epoch 代表的就是一個 Leader 服務(wù)器的標(biāo)志,每次選舉 Leader 服務(wù)器,那么 epoch 值就會更新,代表是這段時期由這個新的 Leader 服務(wù)器進(jìn)行事務(wù)請求的處理)。
ZAB 協(xié)議中通過 epoch 編號來區(qū)分 Leader 周期變化,能夠有效避免不同 Leader 服務(wù)器使用相同的 ZXID。
下面是我 Leader 節(jié)點的 zxid 生成核心代碼大家可以看一下。
- // Leader.java
- void lead() throws IOException, InterruptedException {
- // ....
- long epoch = getEpochToPropose(self.getId(), self.getAcceptedEpoch());
- zk.setZxid(ZxidUtils.makeZxid(epoch, 0));
- // ....
- }
- //
- public long getEpochToPropose(long sid, long lastAcceptedEpoch) throws InterruptedException, IOException {
- synchronized (connectingFollowers) {
- // ....
- if (isParticipant(sid)) {
- // 將自己加入連接隊伍中,方便后面判斷 lead 是否有效
- connectingFollowers.add(sid);
- }
- QuorumVerifier verifier = self.getQuorumVerifier();
- // 如果有足夠多的 follower 進(jìn)入, 選舉有效,則無需等待,并通過其他等待的線程,類似 Barrier
- if (connectingFollowers.contains(self.getId()) && verifier.containsQuorum(connectingFollowers)) {
- waitingForNewEpoch = false;
- self.setAcceptedEpoch(epoch);
- connectingFollowers.notifyAll();
- } else {
- // ....
- // followers 不夠就進(jìn)入等待, 超時時間為 initLimit
- while (waitingForNewEpoch && cur < end && !quitWaitForEpoch) {
- connectingFollowers.wait(end - cur);
- cur = Time.currentElapsedTime();
- }
- // 超時退出,重新選舉
- if (waitingForNewEpoch) {
- throw new InterruptedException("Timeout while waiting for epoch from quorum");
- }
- }
- return epoch;
- }
- }
- // ZxidUtils
- public static long makeZxid(long epoch, long counter) {
- return (epoch << 32L) | (counter & 0xffffffffL);
- }
ZAB 協(xié)議實現(xiàn)
寫數(shù)據(jù)的過程
下面我梳理了 zookeeper 源碼中寫數(shù)據(jù)的過程,如下圖所示:

參考資料
https://www.cnblogs.com/veblen/p/10985676.html
https://zookeeper.apache.org





























