Zab系列博客
Raft Vs Zab
https://www.jianshu.com/p/24307e7ca9da
Zab系列1 核心概念
https://www.jianshu.com/p/76e5dba31ea4
Zab系列2 角色和存储
https://www.jianshu.com/p/d80f9250ffd1
Zab系列3 选举
https://www.jianshu.com/p/0d2390c242f6
Zab系列4 zookeeper特性
https://www.jianshu.com/p/08b62ca1fe4e
Zab系列5 选举恢复(源码分析)
https://www.jianshu.com/p/b6acd99921b7
Zab系列6 zk单机版工作原理
https://www.jianshu.com/p/ed45982b18b4
Zab系列7 集群工作原理Leader篇
https://www.jianshu.com/p/59240c36ba1b
Zab系列8 集群工作原理Follower篇
https://www.jianshu.com/p/8d7c7f1b2838
Zab系列9 消息顺序性
https://www.jianshu.com/p/0aa96b6a2070
申明
- 必须要看懂Zab系列1 基本概念才好理解这篇,因为内容都是环环相扣的!
- 最好结合Zab系列3 Raft Vs Zab一起看
核心算法 FastLeaderElection.lookForLeader()
只要Node进入Looking状态,就会触发这个方法
- 触发条件:
- 节点刚刚启动,会进入Looking状态
- follower监测超时,会进入Looking状态
- Leader未收到过半节点的心跳恢复,会进入Looking状态
- 结束该循环的3种情况(2种大类)
要么recvset中半数节点的投票给A(可以是following的投票,也可以是leading的投票累加),才会认可A为自己的leader,退出循环
要么outofelection中半数节点都认可A为leader,自己才会认可A为自己的leader,退出循环
核心参数:
- 这些参数初始化的值都是自己
- 所有的参数值都是递增的,会更新成最新最大的那个值
- 会随着接收到更新数据的投票信息,来动态更新这些参数值,如果有更新就广播出去
- 如果没有更新的值,就不处理
QuorumPeer self;
Messenger messenger;
AtomicLong logicalclock = new AtomicLong(); //数据最新的leader electionEpoch
long proposedLeader; //数据最新的leader myid
long proposedZxid; //数据最新的leader lastCommitZxid
long proposedEpoch; //数据最新的leader peerEpoch
重点注意:proposedZxid 不等于lastCommitZxid,proposedZxid>=lastCommitZxid。lastCommitZxid只是初始值,后面如果收到了更新的zxid,会更新的。
更新参数信息
//先比较,如果投票信息里面的数据更新,就更新自己的参数,再广播出去
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
}
sendNotifications();
synchronized void updateProposal(long leader, long zxid, long epoch){
proposedLeader = leader;
proposedZxid = zxid;
proposedEpoch = epoch;
}
- 核心变量:recvset、voteSet、outofelection
- recvset 归档set,key是vote发起方的ID,value是该发起方的最新的Vote值。包括state为Following的消息,也包括Leading的消息
Map recvset = new HashMap();
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
- voteSet,根据recvset推算出来的,最适合当leader的节点数据,如果voteSet.hasAllQuorums()返回true,则代表满足了产生新leader的条件
- outofelection 选主成功消息,如果半数节点都选某节点为leader,那么即使该节点的数据更新,也没办法,必须服从成follower,一般这种情况的概率很小。
退出选举的3种情况
- Notification.state=following
如果NodeA收到一个新的“正在投票未有新leader”消息,并且新得主的选举票数超过半数,且200ms内仍然没有更适合的leader产生的话,退出选举。新leader可能是自己,也可能是别人。
switch (n.state) {
case LOOKING:
比较数据
...
if (voteSet.hasAllQuorums()) {
while((n = recvqueue.poll(finalizeWait,TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
if (n == null) {
setPeerState(proposedLeader, voteSet);
Vote endVote = new Vote(proposedLeader,proposedZxid, logicalclock.get(), proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
}
- Notification.state=leading,这种情况下只会比较只会比较electionEpoch而不比较Zxid,也就是说,可能数据更新myid更大的Node,因为收到了半数以上的其它节点的投票认同leader的消息,也不得不委曲求全,接收比自己差的Node为新leader
2.1 recvset投票过半,如果NodeA收到一个新的"投票结束,new leader有了"消息,并且新得主的选举票数超过半数
这是一种非常灵活的方法,因为当LEADING的消息时,
switch (n.state) {
case LEADING:
if(n.electionEpoch == logicalclock.get()){
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
voteSet = getVoteTracker(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() && checkLeader(outofelection, n.leader, n.electionEpoch)) {
setPeerState(n.leader, voteSet);
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
}
2.2 Notification.state=leading,且outofelection投票过半,一般这种情况发生的概率不大,因为recvset里面包含outofelection的所有信息,所以recvset里面更容易产生leader。因为网络原因,可能某个结点错过了广播中的voting前期的投票消息,但是接收到了超过半数的节点都选举结束的消息时,也可以退出选举,进入follower状态
outofelection.put(n.sid, new Vote(n.version, n.leader,n.zxid, n.electionEpoch, n.peerEpoch, n.state));
voteSet = getVoteTracker(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() &&checkLeader(outofelection, n.leader, n.electionEpoch)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
setPeerState(n.leader, voteSet);
}
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
选主的逻辑
在选举初期,大家都在根据广播里面的Voting Notification更新自己的recvset,这个时候,都是PK 数据和myid的,谁更有资格当老大。
如果自己接收到有比自己当前存储的数据更新的数据,就更新自己的数据,并且广播出来。这样Zxid和myid越大,被广播的次数越多,越有机会成为leader。大家每次都会统计投票,看是否有过半节点的投票同一节点当leader的情况产生。在选举中后期,部分节点率先完成leader选举,就会把自己的选主信息广播出来,未完成选主的节点就不仅仅比较竞选投票,也要比较选主投票,只要半数节点都认同了一个Node当leader,那么即使新leader的数据比自己旧,那么也不得不低头。
选举的过程(有一个广播的功能)
- serverA首先将electionEpoch自增,然后为自己投票
- 从内存树和日志中获取到 lastProcessedZxid
- 向所有节点发送投票消息:electionEpoch、lastProcessedZxid、proposedLeader(myid)
logicalclock.incrementAndGet();
//getInitId:myid
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
sendNotifications();
- 循环监听,直到PK完之后,如果本机器投票被pk掉,则更新投票信息为对方投票信息,同时重新发送该投票信息给所有的server。
while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){
Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS);
}
得到选票的条件:
- 如果serverB收到的通知中的electionEpoch比自己的大,则serverB更新自己的electionEpoch为serverA的electionEpoch
- 如果该serverB收到的通知中的electionEpoch比自己的小,则serverB向serverA发送一个通知,将serverB自己的投票以及electionEpoch发送给serverA,serverA收到后就会更新自己的electionEpoch
- 如果electionEpoch相等,则继续PK Zxid,仍然相等则PK myid
解析:
electionEpoch:谁先发现leader异常,谁先自增electionEpoch
Zixd:在投票期间,指的就是lastProcessedZxid,也就是谁的数据更新
- 正常情况下,第一个发现Leader挂了的Node会自增electionEpoch,而此时B节点还没发觉到leader过期,那么A的electionEpoch就会比B大,那么理所当然B应该接受投票
- 如果leader挂了,A、B节点几乎同时发现了这个事情,都自增electionEpoch,都发起投票,那么此时A想要得到B的投票,就要PK zxid,要让数据最新的节点获胜,这样才能保证已经confirm了的数据不会丢失
- 如果A、B几乎同时发现了leader挂了,而且数据都一样新,那么就PK myid,这是为了避免投票失败而设置的,因为这个设置,每轮投票,肯定有一个Node当选。
- 如果ServerA的electionEpoch比B的要小,说明A节点的数据已经是过期的数据,则B会通知A要更新数据了
PK数据的过程
比较哪个节点更适合当leader
- 如果接收到的约票的任期大于自己的(比如S1节点首先发现Leader挂了,自增了任期,发给S2,因为S2还是之前的任期,所以 Epoch1>Epoch2)
但是Epoch比自己大的约票不一定数据比自己新,可能S1和leader的网络分区了,导致S1的数据不全,且第一个超时而发起投票,所以还需要比较数据是否比自己新
那么节点会:
- 更新自己的任期为最新任期
- 清空recvset,因为那些投票都是前一任期的投票,保证了每个recvset里面不会存储不同任期的投票
- 比较数据totalOrderPredicate(),挨个比较epoch、Zxid、myid
- 发送广播投票结果(因为至少自己的logicalclock发生了变更)
如果接收到的约票的任期小于自己的,说明发起投票人的数据过期了,直接忽略,不做处理
如果接收到的约票的任期等于自己的,而且Myid还比自己的大,则发送一个更新投票的广播,会覆盖掉之前
// If notification > current, replace and send messages out
if (n.electionEpoch > logicalclock.get()) {
logicalclock.set(n.electionEpoch);
recvset.clear();
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
if(LOG.isDebugEnabled()){
LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
+ Long.toHexString(n.electionEpoch)
+ ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
}
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
算法细节
通过FastLeaderElection类本身的logicalclock、proposedLeader、proposedZxid来记录当前节点认为数据最新或者myid最大的节点的信息
通过recvset这个Set来记录所有的来自其它节点的投票或者是投票结果广播消息,的最新值。也就是某个sid的节点认为的最适合当leader的信息值
不是PK following选票过半就能成为最终的leader,选举结束后,会等待200ms,看还有没有变更的,没有则宣布选举结束。
有新的消息话,会先pk一下,如果最新的消息没有这个新,则put到recvqueue中,继续等待更新的投票信息
如果有更适合当leader的数据产生,会开始一个新的循环。
这种机制促使让选主变的更严格,系统促使数据最新且myid最大的Node成为leader
while((n = recvqueue.poll(finalizeWait,TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
if (n == null) {
setPeerState(proposedLeader, voteSet);
Vote endVote = new Vote(proposedLeader,proposedZxid, logicalclock.get(), proposedEpoch);
leaveInstance(endVote);
return endVote;
}
参考
ZooKeeper的一致性算法赏析
https://my.oschina.net/pingpangkuangmo/blog/778927?spm=a2c4e.11153940.blogcont62901.11.12c62ee5vwzG6o
辅助
https://www.jianshu.com/p/357ca7c3b2af
zookeeper官网说明
https://zookeeper.apache.org/doc/r3.5.4-beta/zookeeperOver.html
zookeeper官网说明中文翻译版
https://blog.csdn.net/lisuo1234/article/details/55826380
大白话,真的好
https://blog.csdn.net/gaoshan12345678910/article/details/67638657