文章目录
在上一篇的文章中简单描述了 Zookeeper Leader 选举的过程,点击链接
这一次主要来了解下,Leader 选举过程具体是怎么实现的。
QuorumPeer
QuorumPeer 是 ZK 中的核心对象,在一个多节点的 ZK 集群中,当节点启动时,当前节点就会创建一个这样一个对象,负责节点之间的信息沟通以及 Leader 选举等工作。
启动类 QuorumPeerMain
在 ZK 的源码的启动类上,有一些基础配置参数的描述,这些配置基本上属于 ZK 中最重要的配置。在 mian 方法中,进行了 QuorumPeer 线程初始化并运行的操作。
/**
*
* <li>dataDir - 数据存储地址.</li>
* <li>dataLogDir - 日志存储地址.</li>
* <li>clientPort - ZK对外提供服务的端口.</li>
* <li>tickTime - 毫秒为单位,是ZK中的基础时间,默认时长为 3000ms.</li>
* <li>initLimit - 追随者初始化同步领导者信息的最大时长限制,最大时长为(initLimit * tickTime).</li>
* <li>syncLimit - 追随者等待领导者发送消息(包含心跳检测消息)的最大等待时长限制,最大时长为(syncLimit * tickTime).</li>
* <li>server.<i>id</i> - 节点配置如下例
* A:B:C => [A]:hostname,[B]:节点间沟通的端口,[C]:选举时用到的端口,[TYPE]:节点类型(当设置为observer时,不参与选举投票)
* server.1 = A1:B1:C1
* server.2 = A2:B2:C2
* server.3 = A3:B3:C3:observer
* </ol>
* 除了配置文件之外,在data目录下还有一个 myid 文件,包含当前节点的 server id(ASCII decimal)
*/
@InterfaceAudience.Public
public class QuorumPeerMain {
protected QuorumPeer quorumPeer;
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
main.initializeAndRun(args);
} catch
...
}
}
当我们在配置文件中配置了多个 ZK 节点的时候,会走进 runFromConfig(config) 方法,初始化并启动一个 QuorumPeer 线程。
protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
//在这里对配置文件进行了解析
config.parse(args[0]);
}
...
if (args.length == 1 && config.isDistributed()) {
//Distributed模式
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running in standalone mode");
// 单节点模式
ZooKeeperServerMain.main(args);
}
}
public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException {
...
quorumPeer.start();
ZKAuditProvider.addZKStartStopAuditLog();
//等待结束
quorumPeer.join();
...
}
启动 QuorumPeer 线程
- 在 QuorumPeer 线程启动后,我们可以看到,线程执行了初始化 Leader Election 算法的操作
@Override
public synchronized void start() {
...
startLeaderElection();
...
super.start();
}
public synchronized void startLeaderElection() {
...
this.electionAlg = createElectionAlgorithm(electionType);
}
- createElectionAlgorithm 方法中的 case 3 就是快速选举的算法,也是 ZK 默认的选举算法。
- 创建一个 QuorumCnxManager 对象,负责 Leader Election 的通讯
- 创建 FastLeaderElection 算法,并启动该算法,我们在后面再去详细了解这个算法的实现过程
protected Election createElectionAlgorithm(int electionAlgorithm) {
Election le = null;
switch (electionAlgorithm) {
...
case 3:
QuorumCnxManager qcm = createCnxnManager();
QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);
if (oldQcm != null) {
LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");
oldQcm.halt();
}
QuorumCnxManager.Listener listener = qcm.listener;
if (listener != null) {
listener.start();
FastLeaderElection fle = new FastLeaderElection(this, qcm);
fle.start();
le = fle;
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
QuorumPeer 线程主逻辑
Main loop 循环
查看 QuorumPeer 中的 run 方法,里面有个 Main loop 逻辑,
@Override
public void run() {
...
try {
/*
* Main loop
*/
while (running) {
if (unavailableStartTime == 0) {
unavailableStartTime = Time.currentElapsedTime();
}
//获取当前节点的服务状态
//LOOKING:寻找LEADER
//FOLLOWING:追随者状态
//OBSERVING:观察者状态
//LEADING:领导者状态
switch (getPeerState()) {
case LOOKING:
...
//LOOKING状态下会寻找Leader ,该方法确认当前节点决定要为谁投票
setCurrentVote(makeLEStrategy().lookForLeader());
...
break;
case OBSERVING:
...
observer.observeLeader();
//更新服务状态 通过比较自身ID和CurrentVote中的ID
//如果CurrentVote中的 ID 和 自身 ID 一致,就认为自己是 Leader
//否则如果是 Principle 就表示是追随者状态
//否则如果是 Observer 就是 观察者状态
//以上都不是 就是正在寻找 Leader 的状态
updateServerState();
...
break;
case FOLLOWING:
...
follower.followLeader();
updateServerState();
...
break;
case LEADING:
...
leader.lead();
updateServerState();
...
break;
}
}
} ...
}
寻找Leader 的算法 :FastLeaderElection.lookForLeader()
先看源码
最后,压力来到了 FastLeaderElection 这边,我们主要看当前节点是 LOOKING 状态的时候,节点是怎么通过 lookForLeader 方法来确认最后的 Leader 的。
...
synchronized (this) {
//将自身的 Epoch + 1,即 Leader 任期标志,该标志越大,说明参与的选举次数越多,数据更新
logicalclock.incrementAndGet();
//更新选举提案信息Vote
//getInitId 自身ID 否则返回 0
//getInitLastLoggedZxid 自身是参与者则返回当前最大的事务ID, 否则返回 0
//getPeerEpoch 自身是参与者则返回自身的 Epoch 否则返回 0
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
//给所有的节点发送 Vote(proposedLeaderId,proposedZxid,proposedEpoch)
sendNotifications();
//向所有节点发送完Vote信息之后,开始接收其他节点发送的投票信息
//当自身的状态时 LOOKING 并且 stop 为 false 时,循环以下逻辑寻找 Leader
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
//从消息接收队列中了拉取一条数据,当消息队列为空时,该方法最多等待 200ms 等待消息
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
if (n == null) {
//当从消息队列中确实已经获取不到消息时,表明当前节点不会在接收到其他节点的Vote信息,就不会再进行Vote的比较操作
//也就表明当前节点在此次选举过程中已经确认了 Leader 是谁,所以当前节点要发消息通知所有节点。
//判断时候已经给所有的节点发送过消息,如果已经发送过,直接再发一遍
//否则先创建与各节点的连接,等到下一次循环时在发送选举投票
if (manager.haveDelivered()) {
sendNotifications();
} else {
manager.connectAll();
}
...
} else if (validVoter(n.sid) && validVoter(n.leader)) {
//如果选举消息队列中的Vote信息不为空 则和自身的Vote比较,选出最新的Vote
switch (n.state) {
//判断接受到的信息中节点的状态
case LOOKING:
//对方是 LOOKING 状态,表明选举还在执行,判断两者的 Vote 谁更新
//对比三步骤
//1. 先比较 Epoch 谁更新
//2. Epoch 相同则比较,当前最大事务 Zxid 谁更新
//3. 以上两者都相同的情况下,则开始比较 Myid 谁更大
if (n.electionEpoch > logicalclock.get()) {
//如果对方的 electionEpoch 更新,表明对方正在进行一轮更新的选举过程
//则将当前 logicalclock 更新为收到的的 electionEpoch,并清空所有已经接收到的投票信息
//即从现在开始当前节点重新加入到更新的一轮投票选举中
logicalclock.set(n.electionEpoch);
recvset.clear();
//判断 两者Vote 信息谁更新
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
//对面更新则将信息更新为接收到的Vote信息
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
//否则保持自己的原立场
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
//最后将 Vote 信息发送出去
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
...
//electionEpoch < logicalclock
//表明接受到的投票信息是旧的选举过程中的
//可能是因为网络中断等原因导致其他节点在之前的选举过程中被迫中断,网络恢复后节点又重新发起选举过程
//这种情况下直接忽略
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch))
//当接收到的electionEpoch = logicalclock 表明两者在同一选举过程中
//如果接收到的 Vote 更胜一筹
//那么就更新当前节点的 Vote 信息,并发送给所有的节点。
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
// 保存收到的ACK信息
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));
//判断获取到了大多数节点对当前 Vote 的支持
if (voteSet.hasAllQuorums()) {
//但是这个时候并不能直接判断 当前Vote 选择的Leadee就一定是最终的Leader
//先等待 finalizeWait = 200ms 的时长,如果接收到了消息
//且接收到的 Vote 信息更新,那么放入到接收队列中,在下一次循环中再次比较谁的 Vote 更胜一筹
while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
recvqueue.put(n);
break;
}
}
//当确实没有 Vote 消息再传进来的时候,可以确认最终的Leader,选举结束
//更新节点状态
//如果选举出的 leaderId和自身id一样,表明自己是Leader ,状态为LEADING
//如果不一样,表明自己是Follower ,状态为FOLLOWING
//最后,退出选举过程
if (n == null) {
setPeerState(proposedLeader, voteSet);
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break;
}
...
}
}
选举过程的简单实例
下面用最简单三节点的 Zookeeper 集群,来展示集群在初始化的时候是如何进行 Leader 选举的。
- 首先一个三节点的集群,集群中的每个节点都在寻找 Leader,因为集群是第一次进行选举,所以没有事务信息。
- 节点之间开始相互通信,交换 Vote 信息,先看节点三,节点三将接收到来自节点一和节点二的投票信息
- 由 Fast Leader Elaction 算法,节点三认为自己的 Vote 信息是最新的,就不理睬节点二和节点一 的 Vote 信息
- 我们再来看节点一和节点二,以节点一为例。节点一此时投票给了节点三,同理节点二也投票给了节点三。
- 因为节点一和节点二的投票信息发生了变化,所以一和二都将再次重新发送 Vote 信息给其他节点。
如图所示,当节点三再次接收到节点一或者节点二的 Vote 信息的时候,Vote(3,-1,1) 的次数 +1 ,因为本身的投票信息就是 Vote(3,-1,1) ,满足大多数原则,所以节点三认为自身就是 Leader。
而当节点一接收到来自自身或者节点二的消息的时候,Vote(3,-1,1) 的次数 +1 ,因为节点一在此之前已经接收到了来自节点三的 Vote(3,-1,1) 信息,满足大多数原则,所以节点一认为节点三就是 Leader。节点二和节点一同理。
随后节点一和节点二作为 Follower 进入追随者状态 - 针对可能部分节点出现网络延迟,未能会及时通讯而导致脑裂的情况,在源码中我们看到过一个 finalWait 时间,默认200ms,再节点确认 Leader 之前,会等待 200ms 以防止后来的投票请求。具体情形可以参照上一篇博文最后,点击链接