zookeeper的ZAB协议学习

本文深入剖析了Zookeeper的ZAB协议,包括复制状态机、角色、选举阶段、数据模型、消息通信机制及ZAB的选举、确认、同步和原子播报过程。详细阐述了选举逻辑时钟、ZXID、epoch等关键概念,并探讨了线性一致性读和客户端线性一致性保持。此外,还介绍了Zookeeper中日志和数据一致性保障措施。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  看完raft的论文,再来看zookeeper关于ZAB协议的介绍,感觉raft写的真是好,zookeeper我能够搜索到的论文实际上都只是讲了一个大概,感觉 这一篇还算是稍微讲了一些东西,但是还是很不全面。迫不得已看了一下源码,只看了leader选举部分的实现 FastLeaderElection,以及leader选举相关的工作,其实zookeeper的项目对于java开发人员还是很友好的,直接 git clone下来,在本地看的很爽。

下面尝试按照raft论文的方式来对zab协议进行阐述

1. zookeeper的复制状态机

zk的数据存在内存当中(高性能),但是同时记录操作日志+内存快照(二进制),持久化(类似于Redis)。
状态机+命令日志:内存中保存数据的最终状态,命令日志中保存所有的操作过程,内存快照中保存某一时间节点的状态机中的数据。
所以zk和raft基本一致,也是复制状态机的工作模式,由日志复制的线性化来保证系统的线性化。

2. zookeeper的角色

  1. LOOKING:进入leader选举状态
  2. FOLLOWING:leader选举结束,进入follower状态
  3. LEADING:leader选举结束,进入leader状态
  4. OBSERVING:处于观察者状态
    写的话写leader,读取的话可以通过FOLLOWING,OBSERVING,而且拓展OBSERVING的话可以提供更多读的能力,但是不会降低写入的速度。

3. zab协议的阶段

  这个阶段的划分不同的论文好像有不同的说法,这里先以zookeeper的代码中的为准介绍一下,再引述一些其他的方式,在zookeeper的源码中对zabState是这样定义的,有4种状态

ELECTION : leader选举阶段
DISCOVERY: leader确认阶段
SYNCHRONIZATION: 数据同步阶段
BROADCAST: 原子播报阶段
也有一些文章前面三个阶段合起来称为崩溃恢复阶段,这种也是可以的,这种情况下zab协议就被描述为奔溃恢复和原子播报两个阶段。

4. zookeeper的数据模型

1. 在zookeeper每个服务节点的持久化数据

这一块儿介绍的可能不是很全,主要关注了和选举相关的一些数据

字段含义
logs[]:日志
zxid :最后的log的zxid,这个zxid是一个64位的数字,高32位被称为epoch,类似raft日志中的term, 低32位是递增的counter类似于raft中的index,但是这里的counter不是全局递增的,每次leader选举出来之后,counter会被初始化为0,但是zxid还是全局递增的。所以日志是全局有效的。
epoch:zxid的高32位,会单独持久化
lastCommited最新的commited的zxid

2. 内存中的状态

字段含义
logicalclock :这个是选举专用的逻辑时钟,在服务启动后第一次选举开始的时候会初始化一个FastLeaderElection实例,logicalclock是他的一个属性,会被初始化为0;后期有可能因为一些异常原因重建这个实例,默认情况下服务不重启,这个logicalclock会是递增的状态,而且在zookeeper的代码中,有些地方把这个也叫epoch或electEpoch,颇具迷惑性
proposedLeader:当前节点认为的应该做leader的server id,根据当前节点收到的广播消息会动态变化,选举刚开始的时候初始化为当前节点的sid
proposedZxid:对应的应该做leader的server的zxid,根据当前节点收到的广播消息会动态变化,选举刚开始的时候初始化为当前节点的zxid
proposedEpoch:对应的应该做leader的server的epoch,这个epoch是zxid中的epoch,但是不一定相等,因为新的epoch生成了,但是包含这个epoch的zxid可能还没有生成,,根据当前节点收到的广播消息会动态变化,选举刚开始的时候初始化为当前节点的epoch
state每个节点处于的角色状态,可能是LOOKING,FOLLOWING,LEADING,OBSERVING,会随着选举过程逐渐变化,在节点启动或者当前节点要发起leader选举的时候是LOOKING,leader选出来后是后面三种的一种
zabState每个节点处于的zab协议的阶段,可能是ELECTION,DISCOVERY,SYNCHRONIZATION,BROADCAST,在节点启动或者leader选举开始的时候初始化为ELECTION,选举完成后epoch的确认阶段为DISCOVERY,数据同步阶段为SYNCHRONIZATION,数据同步完成之后是原子播报阶段,对应的则是BROADCAST
Map<Long, Vote> recvset:用来收集looking状态下的大家的选票信息,key是投票者的server id, Vote是对应的server投出的票,这个map数据结构是当前server用来记录同样处于LOOKING状态的server发出来的投票信息,如果这个达到了多数一致,那么久认为leader选出来了。
Map<Long, Vote> outofelection :这个对应收集的是leader或者是follower或者leader发出来的信息,这个也是按照多数生效(也就是超过半数的leader+follower信息发过来才认为真正找到了leader,感觉这个还是比较严格的),同时还会要求必须有leader广播的信息认为自己是leader.

上面字段中的proposedLeader,proposedZxid,proposedEpoch,logicalclock是创建本地广播的选票信息的主要来源(new Vote对象的时候使用到这些变量),所以我们为了下面描述起来更加方便,将这些变量称为本地选票信息

vote的信息

字段含义
leader投票认为的leader的server id
zxid认为的leader的zxid
electionEpoch选举的逻辑时钟logicalclock
state投票者的server state ,一般是LOOKING
configData集群的服务器配置,用来验证quorum,这个字段应该是包含了当前集群有哪些节点
peerEpoch被认为是leader的节点的epoch

vote的信息是一个选票的信息,就是下面广播的投票信息是一致的

3. 选举过程中发送的信息

字段含义
leader投票认为的leader的server id
zxid认为的leader的zxid
electionEpoch选举的逻辑时钟logicalclock
state投票者的server state ,一般是LOOKING
configData集群的服务器配置,用来验证quorum,这个字段应该是包含了当前集群有哪些节点
peerEpoch被认为是leader的节点的epoch

5.消息通信机制

在正式了解zookeeper的zab工作模式以前有必要先简单介绍一下zookeeper的通信方式,更加有助于理解。

  1. 在zookeeper中,服务器中的连接是两两互联,构成网状状态,server与server之间直接使用的socket的长连接(俗称BIO),每两个server之间只会建立一个连接,sid大的去主动连接sid小的。
  2. 消息的发送不像http模式下一个请求过来之后要返回一个相应那样一一对应。因为tcp是全双工的,流式的,所以这里请求和相应是独立的,也就是可能连续发了5条消息,后面又收到其中3个消息的答复,这种哪条消息是请求,哪条消息是答复,需要通过消息的类型进行识别,消息之间的配对(a是b的答复)也需要通过消息号等匹配起来。
  3. 同样的,zookeeper是使用tcp长连接来保证接收方接收到的消息的顺序是和发送方发送消息的顺序是一致的,这个也是实现数据全局有序的重要保证

6. zab的几个过程

1. leader选举过程 zabState为ELECTION

下面的代码部分都在FastLeaderElection,方法lookForLeader()作为入口

  1. 选举开始之后,每个server都会初始化内存中的状态部分中的proposedLeader(使用当前节点的sid),proposedZxid(使用当前节点的zxid),proposedEpoch(使用当前节点的epoch),state(为LOOKING),zabState(为ELECTION) 作为当前节点的本地选票信息,然后广播出去一个投票信息,广播的信息的格式就是上面的选举过程中发送的信息,leader字段使用的是proposedLeader,zxid是proposedZxid,peerEpoch 是proposedEpoch (注意着三个信息因为最开始初始化的时候是本机的信息,所以这里广播出去的也是本机的信息,但是随着选举过程的推进,后面可能就不是本机的信息了,但是后面介绍的其他几个字段都还是本机的信息) electionEpoch是当前机器的logicalclock,state是当前节点的state,configData是当前节点的config,等不再赘述,可以直接参考上面的表格。
  2. 然后如果当前是looking状态的话就会等待和收取广播消息(可能是自己发出去的,也可能是别人发出去的)假设收取的广播消息为n,
    1. 如果是自己的,就直接记录到recvset当中,key是当前的sid,

    2. 如果是别人的,而且n.state是LOOKING,先比较选举逻辑时钟electionEpoch

      1. 如果广播消息中的逻辑时钟和当前节点的logicalclock一样大小,则比较n的选票信息和当前节点的本地选票信息比较(主要就是当前节点的proposedLeader,proposedZxid,proposedEpoch),比较的规则参看下面的选票信息比较规则
        1. 如果选票n胜出,则修改本地的选票信息,主要就是当前节点的proposedLeader,proposedZxid,proposedEpoch
        2. 如果n没有本地选票信息新,则不作更新
        3. 将选票n放入recvset当中,key为n.sid,对应消息的发送方sid
        4. 广播本地选票信息(可能会重复发送,不影响)
      2. 如果本地逻辑时钟落后,则直接把本地之前收到的选票全部作废(清空recvset),重置当前节点的logicalclock为消息中的electionEpoch,然后进行选票信息比对,注意这里收到的选票信息不是和本地选票信息比较(因为proposedLeader,proposedZxid,proposedEpoch在对应的之前的electionEpoch中可能已经被改变过了),而是和本机的sid,zxid,epoch信息比较
        1. 如果选票n胜出,则修改本地的选票信息为n中对响应的信息,主要就是当前节点的proposedLeader,proposedZxid,proposedEpoch
        2. 如果n没有本地选票信息新,则更新本地选票信息为本机信息(和选举刚开始时候的初始化信息是一致的)
        3. 将选票n放入recvset当中,key为n.sid,对应消息的发送方sid
        4. 广播本地选票信息(可能会重复发送,不影响)
      3. 如果n中的逻辑时钟electionEpoch小于当前节点的logicalclock,则忽略这个选票信息
      4. 查看recvset中的信息是否对本地选票中的proposedLeader达成了多数一致
        1. 如果未达成则进入大步骤中的2,继续处理收到的广播消息
        2. 如果达成多数赞成proposedLeader,则会继续轮询接收消息的队列看看有没有优先级更高的选票(这里的优先级更高的就是使用下面的选票信息比较规则最后胜出)
          1. 如果在处理队列中的消息发现有优先级更高的消息,则会把这个消息再放回到接收消息的队列中,跳到大步骤中的2,继续处理收到的广播消息,选出更加合适的leader
          2. 如果接收消息的队列已经空了,且没有优先级更高的选票,则会等待200ms
            1. 有优先级更高的消息的话,同3.2.1,跳到下一轮大循环
            2. 有消息,优先级不高,同3.2.2,继续消费队列
            3. 还是没有消息,可以认为这次选举结束了,将当前server的state设置为leading,following,observing三个状态中的一个,同时清空接收消息的队列
    3. 如果是别人的,而且n.state是OBSERVING,接着进入步骤2,不以OBSERVING的消息为准,因为他没有选举权限

    4. 如果是别人的,而且n.state是FOLLOWING,LEADING,这个时候

      1. 判断n和本地是否处于用一个选举周期中n.electionEpoch == logicalclock.get(),如果在一个选举周期中则放入recvset,并查看recvset中的信息是否对n选票中的n.leader达成了多数一致,如果达成了并且leader的选票也认为自己是leader,那么就结束选举,这种情况应该是本次选举基本可以认为结束了,但是当前节点收到的消息比较滞后的情况,这个时候可能会受到leader和其他follower的信息,这个时候还是按照原来的规则计算即可
      2. 如果上一步没有计算出来leader放入另一个计票容器outofelection当中,这一步没有要求选举周期是一致的,然后看看outofelection中是否对n.leader达到了多数一致性,如果达成了也会结束选举,这个应该应用的场景就是类似某个follower和leader失去联系了,然后发起选举,结果收到了其他人都告诉他leader存在的消息(这个时候消息的选举周期和当前节点肯定不一致),然后,当前节点就会接受当前leader存在的事实,防止频繁进行选举过程。

选票信息比较规则

1.谁的peerEpoch高谁谁胜利
2.如果peerEpoch相等,则谁的zxid更大谁胜出
3.如果peerEpoch,zxid都相等,那么谁的sid大谁胜出
4.这里补充一个疑惑点,为什么还要先比较peerEpoch,直接比较zxid不就行了么,因为zxid不是包含了epoch的信息么,肯能是因为某个节点当选了master然后很快超时了重新选举了?有待后续探索

2. leader确认阶段

  zabState为DISCOVERY,对应代码在QuorumPeer的run()方法之中,这里针对不用角色的节点leading,following,observing,都会有DISCOVERY阶段。
  这个阶段就是leader会生成新的epoch(从各个follower收集到的最大的epoch+1),并使用(epoch,0)组合生成zxid,把自己的zxid封装成Leader.LEADERINFO包发送给发送给follower,然后follower确认这个epoch是大于等于自己当前看到的epoch的。如果不是就会抛异常,不承认当前leader(理论上不应该发生),如果接受了就会更新当前服务器的epoch,封装成Leader.LEADERINFO包发送给leader,在leader收到过半的follower的ack消息之后就说明大家都承认他是leader了,后面就可以开始数据同步工作了。
这里的epoch一般是和zxid中的一致的,因为follower的都是从master的消息当中得到的。

3. leader的数据同步阶段

  在过半follower回复了ack消息之后,leader就可以开始数据同步工作了,数据同步的时候是采用强leader的方式,也就是大家的数据都要和leader的对齐,这时zabState为SYNCHRONIZATION

  1. 如果follower的zxid比leader的小则leader会发送后面的数据给对应的follower(也是使用两阶段提交的方式),在发送完后会发送一个Leader.NEWLEADER数据包,follower在同步完成后发送响应的ack消息
  2. 如果follower的数据zxid比leader的大,则对应的数据都会被删除,完成后也要发送ack消息
    在leader收到过半的follower的ack消息之后就认为数据同步完成了,后面就可以进入原子播报阶段了

4. 原子播报阶段,这个时候使用的是二阶段提交模式

zabState为 BROADCAST

  1. 对于每一个事务请求过来的时候,都要由leader进行FIFO处理,假如是follower或者observer接收到了这个请求,那么会把这个请求转发给leader
  2. leader接收到事务请求后,会先生成一个zxid(epoch+有序递增的事务id),然后将该事务存储到本地日志,
  3. 接着将这个事务广播所有的follower进行事务日志存储,
  4. follower 在接收到leader事务请求时,要么选择执行该事务,要么选择抛弃leader,重新发起leader选举(但是会变成无效,直到他重新认领原来的leader,又会通过同步的方式进行数据同步)
  5. 等待过半的follower都回应ack表示可以存储之后,再发送一个commit信息给所有的follower进行提交(更新lastcommited并应用log到状态机当中),然后本地也进行一个提交(更新lastcommited并应用log到状态机当中),之后返回给客户端成功。

  zk的leader处理事务时FIFO机制保持了数据的一致性,这个可以保证leader上的顺序性,同时,leader在给follower的信息传递中也通过tcp的有序机制保证了follower每台节点上的日志的顺序一致性。所以日志可以保持全局有序性,这个和raft是一致的。

7. 小结

1. 线性一致性读

zk的写是具备线性一致性的,但是读的话要分两种情况,如果是普通的read,则不满足线性一致性,因为读取没有走zab的流程,这个时候这个请求可能到了某个follower,而该follower还没有同步到这个数据的话,可能读不到最新的数据,但是这只是zookeeper对读的一种优化,可以更快响应,如果对数据的及时性有非常高的要求的话,那么园长(zookeeper也被称为动物园园长)也提供了线性一致性的读方式,就是在真正读取之前调用一下zk.sync()方法,这个方法会获取当前最大的zxid,知道本机提交这个zxid才会返回,也就保证了在这之前执行的操作在本机都是可见的了。

2. 与client的线性一致性保持

从leader的选票比较规则以及多数投票一致性上可以得出,

  1. 肯定不会丢commit的数据,应为这个log必然已经存在了多个节点上
  2. 同时,对于只存在之前leader上的数据,如果leader挂了那么他的只有自己有的数据也会被忽略掉,即使他后来又回来,但是这个时候epoch偏低,所以不会接受他的日志。
  3. 但是对于那种有部分机器接受到了广播请求,只是存了log,这个时候leader挂了,该数据还没有走到commit阶段,这个时候如果新的leader恰好有这条数据,那么从zookeeper来看他是会保留这一条数据,在raft协议中,这样的数据也是会被保留,但是raft要求client重试请求的时候也要携带请求编号,来确认这个请求是否已经做了,而不会重复做两次(比如是创建节点),zookeeper这块是如何做的呢,解析zookeeper的log中发现石油session和cxid,zxid的,这样的话也就满足了幂等,可以通过这个来判断这个操作是否已经提交了。

事务日志

ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
1/20/20 4:29:59 AM UTC session 0x30014da58050000 cxid 0x0 zxid 0x10000000c createSession 30000

1/20/20 4:30:00 AM UTC session 0x30014da58050000 cxid 0x4 zxid 0x10000000d create '/test_zookeeper/test/item,,v{s{31,s{'djdigest,'CgcA1GMivoBYyZZWuQDgeLuz5L45jmuVDyLKi2J0swQ=:MEonRpvlUHyT9yHsCnPddPJ0QVCMGVM5ylIV0Zv/VaY=}}},F,2

1/20/20 4:30:00 AM UTC session 0x30014da58050000 cxid 0x5 zxid 0x10000000e delete '/test_zookeeper/test/item

1/20/20 4:30:00 AM UTC session 0x30014da58050000 cxid 0x6 zxid 0x10000000f setACL '/,v{s{31,s{'djdigest,'T9ihPbFmp0odTgrtigbbYJgBkC5Pe6XkWO543Hl1+jc=:dk8WmGqk2QsbWdyxv98BRJWaiW3xEGxpvbzVVx8z8ig=}}},2

1/20/20 4:30:00 AM UTC session 0x30014da58050000 cxid 0x8 zxid 0x100000010 setACL '/zookeeper,v{s{31,s{'djdigest,'V4AoltP7EqnmD6tlXT9D+yzozXnf2aN/FTmYOVekewQ=:vvEn1n8041x6LDHUgnGC/+tAAwKpFePtXZAdWP4hY3Y=}}},2

部分源码,附

 /**
     * Messages that a peer wants to send to other peers.
     * These messages can be both Notifications and Acks
     * of reception of notification.
     */
    public static class ToSend {

        /*
         * Proposed leader in the case of notification
         */ long leader;

        /*
         * id contains the tag for acks, and zxid for notifications
         */ long zxid;

        /*
         * Epoch
         */ long electionEpoch;

        /*
         * Current state;
         */ QuorumPeer.ServerState state;

        /*
         * Address of recipient,这里是接收端的server id,实际上这个字段的信息并不会发出去
         */ long sid;

        /*
         * Used to send a QuorumVerifier (configuration info)
         */ byte[] configData = dummyData;

        /*
         * Leader epoch
         */ long peerEpoch;

    }

public class Vote {

    private final int version;

    private final long id; //leader的id

    private final long zxid; // leader的zxid

    private final long electionEpoch; // leader的逻辑时钟logicalclock

    private final long peerEpoch; //leader的epoch

}

zxid的初始化

这个方法会找到当前收到的最大epoch然后执行+1操作得到当前的epoch
long epoch = getEpochToPropose(self.getId(), self.getAcceptedEpoch());
zk.setZxid(ZxidUtils.makeZxid(epoch, 0));

epoch和clock是不是一个,东西,每个选票的信息有

每一个投票者会维护一个

Map<Long, Vote> recvset = new HashMap<Long, Vote>();

用来记录自己收到的投票信息
map的key是server id,也就可以收集当前选举轮次中每个server的投票信息

vote的数据结构是期望leader的id,期望leader的zxid,期望leader的投票周期(逻辑时钟),期望leader的zxid中的epoch部分

recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

找到和当前认为的leader是一致的vote

voteSet = getVoteTracker(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch));

脑图,帮助理解

在这里插入图片描述

部分参考链接

https://www.jianshu.com/p/90e00da6d780

https://zhouj000.github.io/2019/02/11/zookeeper-03/

https://www.cnblogs.com/leesf456/p/6140503.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值