• 分布式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
  • 相关阅读:
    Python 环境安装教程(Windows 10)
    去掉页面状态栏的方法
    iOS开发init方法解析
    xCode删除storyboard,新建window并启动
    应用程序代理AppDelegate解析
    nib文件的注册及加载
    cocospods的安装与应用
    Objective-C 理解之方括号[ ]的使用
    36辆自动赛车和6条跑道的问题
    编程珠玑第二章
  • 原文地址:https://www.cnblogs.com/aspirant/p/13308093.html
Copyright © 2020-2023  润新知