git clone
下来,在本地看的很爽。
下面尝试按照raft论文的方式来对zab协议进行阐述
zk的数据存在内存当中(高性能),但是同时记录操作日志+内存快照(二进制),持久化(类似于Redis)。
状态机+命令日志:内存中保存数据的最终状态,命令日志中保存所有的操作过程,内存快照中保存某一时间节点的状态机中的数据。
所以zk和raft基本一致,也是复制状态机的工作模式,由日志复制的线性化来保证系统的线性化。
这个阶段的划分不同的论文好像有不同的说法,这里先以zookeeper的代码中的为准介绍一下,再引述一些其他的方式,在zookeeper的源码中对zabState是这样定义的,有4种状态
ELECTION : leader选举阶段
DISCOVERY: leader确认阶段
SYNCHRONIZATION: 数据同步阶段
BROADCAST: 原子播报阶段
也有一些文章前面三个阶段合起来称为崩溃恢复阶段,这种也是可以的,这种情况下zab协议就被描述为奔溃恢复和原子播报两个阶段。
这一块儿介绍的可能不是很全,主要关注了和选举相关的一些数据
字段 | 含义 |
---|---|
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 |
字段 | 含义 |
---|---|
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 |
用来收集looking状态下的大家的选票信息,key是投票者的server id, Vote是对应的server投出的票,这个map数据结构是当前server用来记录同样处于LOOKING状态的server发出来的投票信息,如果这个达到了多数一致,那么久认为leader选出来了。 |
Map |
这个对应收集的是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的信息是一个选票的信息,就是下面广播的投票信息是一致的
字段 | 含义 |
---|---|
leader | 投票认为的leader的server id |
zxid | 认为的leader的zxid |
electionEpoch | 选举的逻辑时钟logicalclock |
state | 投票者的server state ,一般是LOOKING |
configData | 集群的服务器配置,用来验证quorum,这个字段应该是包含了当前集群有哪些节点 |
peerEpoch | 被认为是leader的节点的epoch |
在正式了解zookeeper的zab工作模式以前有必要先简单介绍一下zookeeper的通信方式,更加有助于理解。
下面的代码部分都在FastLeaderElection,方法lookForLeader()
作为入口
选举过程中发送的
信息,leader字段使用的是proposedLeader,zxid是proposedZxid,peerEpoch 是proposedEpoch (注意着三个信息因为最开始初始化的时候是本机的信息,所以这里广播出去的也是本机的信息,但是随着选举过程的推进,后面可能就不是本机的信息了,但是后面介绍的其他几个字段都还是本机的信息) electionEpoch是当前机器的logicalclock,state是当前节点的state,configData是当前节点的config,等不再赘述,可以直接参考上面的表格。如果是自己的,就直接记录到recvset
当中,key是当前的sid,
如果是别人的,而且n.state是LOOKING,先比较选举逻辑时钟electionEpoch
选票信息比较规则
recvset
),重置当前节点的logicalclock为消息中的electionEpoch,然后进行选票信息比对,注意这里收到的选票信息不是和本地选票信息比较(因为proposedLeader,proposedZxid,proposedEpoch在对应的之前的electionEpoch中可能已经被改变过了),而是和本机的sid,zxid,epoch信息比较
如果是别人的,而且n.state是OBSERVING,接着进入步骤2,不以OBSERVING的消息为准,因为他没有选举权限
如果是别人的,而且n.state是FOLLOWING,LEADING,这个时候
选票信息比较规则
1.谁的peerEpoch高谁谁胜利
2.如果peerEpoch相等,则谁的zxid更大谁胜出
3.如果peerEpoch,zxid都相等,那么谁的sid大谁胜出
4.这里补充一个疑惑点,为什么还要先比较peerEpoch,直接比较zxid不就行了么,因为zxid不是包含了epoch的信息么,肯能是因为某个节点当选了master然后很快超时了重新选举了?有待后续探索
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了,后面就可以开始数据同步工作了。
在过半follower回复了ack消息之后,leader就可以开始数据同步工作了,数据同步的时候是采用强leader的方式,也就是大家的数据都要和leader的对齐
zk的leader处理事务时FIFO机制保持了数据的一致性,这个可以保证leader上的顺序性,同时,leader在给follower的信息传递中也通过tcp的有序机制保证了follower每台节点上的日志的顺序一致性。所以日志可以保持全局有序性,这个和raft是一致的。
zk的写是具备线性一致性的,但是读的话要分两种情况,如果是普通的read,则不满足线性一致性,因为读取没有走zab的流程,这个时候这个请求可能到了某个follower,而该follower还没有同步到这个数据的话,可能读不到最新的数据,但是这只是zookeeper对读的一种优化,可以更快响应,如果对数据的及时性有非常高的要求的话,那么园长(zookeeper也被称为动物园园长)也提供了线性一致性的读方式,就是在真正读取之前调用一下zk.sync()
方法,这个方法会获取当前最大的zxid,知道本机提交这个zxid才会返回,也就保证了在这之前执行的操作在本机都是可见的了。
从leader的选票比较规则以及多数投票一致性上可以得出,
事务日志
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 recvset = new HashMap();
用来记录自己收到的投票信息
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