Zookeeper的核心是原子广播(Zab:Zookeeper Atomic Broadcast),该机制保证各个Server之间的同步。Zab协议有两种模式,分别是恢复模式和广播模式。其中恢复模式就代表我们的leader选举流程,在Zookeeper集群中有两种情况需要进入leader选举:
状态(state) | 定义 | 描述 |
---|---|---|
LOOKING | 不确定Leader状态 | 该状态下的服务器认为当前集群中没有Leader,会发起Leader选举 |
FOLLOWING | 跟随者状态 | 表明当前服务器角色是Follower,并且它知道Leader是谁 |
LEADING | 领导者状态 | 表明当前服务器角色是Leader,它会维护与Follower间的心跳 |
OBSERVING | 观察者状态 | 表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票 |
FastLeaderElection算法(源码)
可通过electionAlg配置项设置Zookeeper用于领导选举的算法,一般使用Zookeeper的默认算法FastLeaderElection
其中核心源码lookForLeader()方法如下:
public Vote lookForLeader() throws InterruptedException {
//计算选举的耗时
if (self.start_fle == 0) {
self.start_fle = Time.currentElapsedTime();
}
try {
//选票票箱
Map recvset = new HashMap();
Map outofelection = new HashMap();
//接收消息的时间
int notTimeout = minNotificationInterval;
synchronized (this) {
logicalclock.incrementAndGet();
//先投给自己
updateProposal(getInitId()/** 被推举服务器myid(vote_id) **/, getInitLastLoggedZxid() /** 被推举服务器zxid(vote_zxid) **/, getPeerEpoch() /** 投票轮次(logicClock) **/);
}
//发送投票,包括发给自己
sendNotifications();
SyncedLearnerTracker voteSet;
//主循环,直到选出leader
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)) {
//从IO线程里拿到投票消息,自己的投票也在这里处理
//LinkedBlockedQueue() 接收
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
if (n == null) {//没有获取到选票
//如果空闲情况,消息发完了,继续发送,一直到选出leader为止
if (manager.haveDelivered()) {
sendNotifications();
} else {
//消息还没投递出去,可能是其他server 还没启动,尝试再连接
manager.connectAll();
}
//延长超时时间
int tmpTimeOut = notTimeout * 2;
notTimeout = (tmpTimeOut < maxNotificationInterval ?
tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
} else if (validVoter(n.sid) && validVoter(n.leader)) {//收到一个有效的选票结果
switch (n.state) {
case LOOKING: //发送消息放的节点状态LOOKING
if (getInitLastLoggedZxid() == -1) {
LOG.debug("Ignoring notification as our zxid is -1");
break;
}
if (n.zxid == -1) {
LOG.debug("Ignoring notification from member with -1 zxid" + n.sid);
break;
}
// If notification > current, replace and send messages out
//判断收到消息投票的轮次epoch(接收消息的投票轮次大于自己的轮次)
if (n.electionEpoch > logicalclock.get()) {
//更新本地的投票的轮次epoch
logicalclock.set(n.electionEpoch);
//清空接收队列
recvset.clear();
//检查接收到的这个消息是否可以胜出,logicClock/peerEpoch(选举轮次),vote_zxid/zxid(事物id),vote_id/leader(被推举服务器myid)
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()) {
//如果收到的消息epoch小于当前节点的epoch,则忽略这条消息
if (LOG.isDebugEnabled()) {
//...
}
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)) {
//如果是epoch相同的话,就是比较zxid,myid,如果胜出,则更新自己的票据,并且发出广播
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
// don't care about the version if it's in LOOKING state
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 把与自己当前的选票相同的票加到voteSet中去
voteSet = getVoteTracker(
recvset, new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch));
//收到了大多数的选票
if (voteSet.hasAllQuorums()) {
// Verify if there is any change in the proposed leader
//一直等到新的notification的到达,直到超时
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;
}
}
break;
case OBSERVING:
//如果收到的消息选票状态不是LOOKING,比如这台机器刚加入一个已经正在运行的ZK集群时//observer 不参与选举
LOG.debug("Notification from observer: " + n.sid);
break;
case FOLLOWING:
case LEADING:
//说明当前系统中已经存在Leader了,直接同步Leader
if (n.electionEpoch == logicalclock.get()) {//判断epoch是否相同
//加入到本机的投票集合
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//投票是否结束,如果结束,再确认Leader是否有效
//如果结束,修改自己的状态并返回投票结果
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;
}
}
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;
}
break;
default:
LOG.warn("Notification state unrecoginized: " + n.state
+ " (n.state), " + n.sid + " (n.sid)");
break;
}
} else {
// 收到了选票结果,但是无效的
if (!validVoter(n.leader)) {
LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
}
if (!validVoter(n.sid)) {
LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
}
}
}
return null;
} finally {
//jmx相关处理...
}
}
分析选举源码步骤
一.Server初始化启动
集群刚启动时,所有服务器的logicClock都为1,zxid都为0;所有的服务器发送初始化选票,并把自己的选票记录到票箱。如下图:每个Server票箱中都是自己的选票,并把自己的选票信息广播到其他服务器中;广播的选票信息包括sid(投票服务器的myid)、logicClock(选票轮次)、vote_id(被推举服务器myid)、vote_zxid(被推举服务器的事务id);
在上图中Server1收到Server2和Server3的选票后,Server1的选票信息就和Server2和Server3发生选票pk;由于集群刚启动所有的logicClock和zxid都相同所以根据vote_id判断,Server3中的vote_id比Server1中的vote_id大,所以Server3胜出;Server1更新自己的选票信息推举Server3,并把更新后的选票和Server3的选票记录入票箱;然后将自己更新后的选票信息广播出去,此时Server1票箱recvset={(3,3),(1,3)}
Server2收到Server3的选票后同理吧更新自己的选票,并且Server1票箱recvset={(3,3),(2,3)}
Server3收到选票后根据选票pk规则无需更新选票recvset={(3,3)}
Server1,Server2更新选票后都推举Server3并且广播选票,这时3个服务的选票都是推荐Server3,并且3个票箱都是recvset={(3,3),(2,3),(1,3)},此时Server3就为Leader,如下图:
二.集群中Leader挂掉了
Server3为集群中的LEADER,当Server3出现故障发生了宕机;Server1,Server1立即进入LOOKING状态并发起选举流程,初始化选票如下图:Server1和Server2分别推荐自己成为LEADER。
Server1接收到Server2的选票由于logicClock相同Server1的vote_id=101大于Server2的vote_id=100,所以Server1选票不变;
Server2接收到Server1的选票根据上面规则推举Server1成为LEADER,更新选票和票箱并且广播自己更新的选票;此时Server1和Server2都推荐Server为LEADER。
当Server3修复启动后进入LOOKING状态,发现Server1为LEADER后则进入FOLLOWER状态