1. 程式人生 > 實用技巧 >分散式ZAB協議

分散式ZAB協議

提到ZAB,恐怕大家第一時間就會想到Zookeeper,然後由Zookeeper又會聯想到Paxos。這之間的聯絡是不是因為有本暢銷書叫《從Paxos到Zookeeper分散式一致性原理與實踐》,使得大家常常把Zookeeper和Paxos關聯起來,畢竟“買了就是讀了”,開個玩笑,ZAB和Paxos的確也是有很多相似之處的,理解了Paxos也的確對學習其它分散式一致性或共識演算法非常有幫助。不過Paxos的內容我們以後再說,在這裡只討論ZAB。 ZAB的全稱為Zookeeper Atomic Broadcast,其實大家看到“Atomic”和“Broadcast”兩個詞,應該就能大概明白ZAB的主要工作方式了。他是一個為Zookeeper量身定製的支援崩潰恢復的原子廣播協議,用於保障Zookeeper各副本之間在正常或異常情況下的資料一致性。
既然是支援崩潰恢復的原子廣播協議,那我們介紹ZAB就可以從他的這兩個名詞說起,分別是“原子廣播“和“崩潰恢復“。 原子廣播 在ZAB協議中,存在兩種角色,Leader和Follower(在Zookeeper中實際上還有一個角色叫Observer,但他和ZAB協議沒有直接關係,所以在這裡不做討論。),Leader負責資料的讀和寫請求,Follower只負責讀請求,外部應用可以給任意的Zookeeper端傳送請求,所以如果是寫請求,就會轉給Leader處理,讀的話則就地響應。這樣做同時也是為了達到所有寫請求都能有序處理的效果。 “原子”可以理解為事務,在Zookeeper收到一個數據寫請求後,對其分配一個全域性唯一且遞增的Zxid,然後將該請求轉化為事務Proposal,嚴格按照請求的接收次序放到針對每個Follower的FIFO佇列中,即向叢集中所有Follower廣播該資料,Follower成功收到資料後,會發送Ack給Leader,當Leader收到的Ack超過半數,則向Follower傳送commit命令,完成事務的提交。
可以看出在這個分散式事務的提交過程中是遵循了2PC協議的,即事務的預處理請求和事務提交是分成兩階段進行的,不同之處在於2PC要求所有副本應答,而ZAB只要求超過半數的副本應答即可,這樣也避免了2PC單點超時造成阻塞的問題。 崩潰恢復 Zookeeper作為一個典型的CP(一致性/分割槽容錯性)系統,在設計上必須考慮節點異常的情況,所以ZAB針對崩潰恢復的設計是必不可少的,這也是Zookeeper拋棄可用性的證明,在崩潰恢復過程中,Zookeeper服務對外是不可用的。 崩潰恢復的過程可以分成兩個階段來說:一是Leader選舉,二是資料同步。 1、Leader選舉 如果Leader節點崩潰,則Follower節點的狀態會從FOLLOWING變為LOOKING,這裡節點的狀態是用一個列舉標識的(碼1),即進入選舉狀態,選舉的方式簡單來講就是看誰能得到超過半數的選票。
// 碼 1 QuorumPeer.java:節點狀態列舉
public enum ServerStatepublic enum ServerState {
        LOOKING,
        FOLLOWING,
        LEADING,
        OBSERVING
}
選票的資訊見class Vote(碼2),還記得剛才提到的Leader為每個事務分配的Zxid吧,該欄位為一個64位長整形,其中高32位稱作Epoch,低32位是一個遞增的計數器(碼3)。Epoch代表了Leader的編號,每次選舉出了新的Leader,該數值就被+1,並將Counter清0,之後該Leader每收到一個請求,都會將Counter+1。
// 碼 2 Vote.java:選票結構
public class Vote {
    private final int version;
    private final long id; //伺服器ID
    private final long zxid; //Epoch + Counter
    private final long electionEpoch; //選舉輪次
    private final long peerEpoch; //被推舉的Leader所在的選舉輪次
    private final ServerState state; //當前伺服器狀態
}
// 碼 3 ZxidUtils.java:Zxid結構
public class ZxidUtils {
    public static long getEpochFromZxid(long zxid) {
        return zxid >> 32L;
    }
    public static long getCounterFromZxid(long zxid) {
        return zxid & 0xffffffffL;
    }
    public static long makeZxid(long epoch, long counter) {
        return (epoch << 32L) | (counter & 0xffffffffL);
    }
    public static String zxidToString(long zxid) {
        return Long.toHexString(zxid);
    }
}
在選舉初期,每個節點都會初始化自身選票(Vote例項化),節點預設都是推舉自己做Leader的,之後將自己的資訊填到選票後放到佇列中傳送給其它節點,也包括他自己,當其他節點收到了選票之後,先對比electionEpoch是否和自身一樣,如果比自身大,則清空自身的Vote和已收到的選票,更新electionEpoch後重新對比。如果比自身小,則直接丟棄該選票。如果和自身一樣,則會進行下一階段的對比,這個對比次序依次為peerEpoch、zxid和id,規則為比自身大,則更新自身選票,比自身小,則丟棄(碼4)。所有對比更新完成後,發出自身選票。最後統計所有選票,當某個節點的票數超過一半(Quorum規則),則該節點被推舉為新的Leader。
// 碼 4 FastLeaderElection.java:選票對比
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
        LOG.debug(
            "id: {}, proposed id: {}, zxid: 0x{}, proposed zxid: 0x{}",
            newId,
            curId,
            Long.toHexString(newZxid),
            Long.toHexString(curZxid));
        if (self.getQuorumVerifier().getWeight(newId) == 0) {
            return false;
        }
        /*
         * We return true if one of the following three cases hold:
         * 1- New epoch is higher
         * 2- New epoch is the same as current epoch, but new zxid is higher
         * 3- New epoch is the same as current epoch, new zxid is the same
         * as current zxid, but server id is higher.
         */
         return ((newEpoch > curEpoch)
                || ((newEpoch == curEpoch)
                    && ((newZxid > curZxid)
                        || ((newZxid == curZxid)
                            && (newId > curId)))));
    }
在新Leader被選舉出後,則將自己的狀態從LOOKING更新為LEADING,其它節點變為FOLLOWING,然後進入資料同步階段。 2、資料同步 在新Leader正式開始工作之前,每個Follower會主動和Leader建立連線,然後將自己的zxid傳送給Leader,Leader從中選出最大的Epoch並將其+1,作為新的Epoch同步給每個Follower,在Leader收到超過半數的Follower返回Epoch同步成功的資訊之後,進入資料對齊的階段。對齊的過程也比較直接,如果Follower上的事務比Leader多,則刪除,比Leader少,則補充,重複這個過程直到過半的Follower上的資料和Leader保持一致了,資料對齊的過程結束,Leader正式開始對外提供服務。 還有一種情況是,在新Leader選舉出來之後,原來掛掉的Leader又重新連線上了,那麼此時因為原Leader所持有的Epoch已經比新Leader的Epoch小了,故原Leader將會變為Follower,並和新的Leader完成資料對齊。 在這個資料對齊過程中還是有很多細節的,感興趣的人可以從原始碼再進行更深入的研究,我這裡就不做介紹了。 * 所用原始碼均引自 apache-zookeeper-3.6.0 *https://zookeeper.apache.org/releases.html