ZooKeeper源码学习笔记(4)--集群选主算法

时间:2022-05-21 08:24:45

FastLeaderElection

ZooKeeper 中一共有三个实现了Election接口的选举类,分别是 LeaderElection , AuthFastLeaderElectionFastLeaderElection

前两个类已经在3.4.0版本之后被废弃掉,因此在本节中,我只会介绍LeaderElection 的选主算法。

接下来我会以一个5台节点的集群为例,介绍 ZooKeeper 中的选主算法。

ZooKeeper源码学习笔记(4)--集群选主算法

如图所示,ABCDE代表着一个集群中的5台节点机器,冒号后面的数字代表各个机器上的sid,紫色的节点代表着 PARTICIPANT , 绿色的节点代表着 OBSERVER

currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());

this.electionAlg = createElectionAlgorithm(electionType);

每个节点都存在一个 currentVote 对象,我们可以把他称作是这个节点的候选人。

每个节点在启动之后,首先在 QuorumPeer::start 通过 startLeaderElection 设定初始化选举配置,将候选人设置为自身,并创建对应的选举算法对象。

构造 FastLeaderElection 的时候,会启动一个 QuorumCnxManager.Listener 线程,负责监听选举端口(electionAddr),在选举过程中维护各个节点的点对点通信。

选主流程的具体入口可以在 QuorumPeer::run 看到,当 QuorumPeer 的状态处于 LOOKING 的时候, 会调用 Election::lookForLeader 进行选主流程。

private void sendNotifications() {
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock,
QuorumPeer.ServerState.LOOKING,
sid,
proposedEpoch);
sendqueue.offer(notmsg);
}

FastLeaderElection::lookForLeader 中通过 sendNotifications 同其他PARTICIPANT节点建立链接关系。

我们看到 sendNotifications 中构造了一个 ToSend 对象,proposedLeader 代表当前节点的候选人的sid,proposedZxid 代表着当前节点的候选人的zxid,logicalclock 在默认情况下是通过 ZxidUtils.getEpochFromZxid(newLeaderZxid); 根据 zxid 进行计算出的,可以认为是zxid的另一种表现形式。

ToSend 对象被加入到 sendqueue 栈后,会有一个独立线程 WorkSender 专门负责将 ToSend 发送给对应 sid 的节点,告知他们本节点的候选人情况。

//If wins the challenge, then close the new connection.
if (sid < self.getId()) {
SendWorker sw = senderWorkerMap.get(sid);
if (sw != null) {
sw.finish();
}
closeSocket(sock);
connectOne(sid);
} else {
SendWorker sw = new SendWorker(sock, sid);
RecvWorker rw = new RecvWorker(sock, sid, sw);

sw.setRecv(rw);

SendWorker vsw = senderWorkerMap.get(sid);
if(vsw != null)
vsw.finish();

senderWorkerMap.put(sid, sw);

if (!queueSendMap.containsKey(sid)) {
queueSendMap.put(sid, new ArrayBlockingQueue<ByteBuffer>(
SEND_CAPACITY));
}

sw.start();
rw.start();
}

QuorumCnxManager.Listener 接受到消息之后,如果发现发送socket的节点的sid小于当前节点的sid,则关闭链接。否则保持当前的socket链接。

根据这个解释,虽然我们在每个节点的 Election::lookForLeader 的阶段都向其他节点进行了点对点链接,这样会导致两个节点互相给对方建立socket,但接受到消息的节点会根据 sid 关闭掉由 低 sid 发送给 高 sid 的socket,从而保证两个节点间的通信是唯一的。

ZooKeeper源码学习笔记(4)--集群选主算法

如上图所示,箭头指向代表着Socket到ServerSocket的指向,我们可以看到箭头指向总是从比较高的sid节点指向比较低的sid节点。

同时需要留意的是,两个绿色的 OBSERVER 节点之间是没有通信关系的,因为在 sendNotifications 的时候只会同 PARTICIPANT 节点进行通信。

ZooKeeper源码学习笔记(4)--集群选主算法

在节点间中点对点通信中,节点会不断接收到来自其他节点的 Message 对象 response, 如果发现 response 中候选人不是 PARTICIPANT 而是 OBSERVER, 则会将自身节点的候选人 currentVote 告知来源节点。

如果其他节点的候选人是 PARTICIPANT, 则会将这条 Message 封装成一个 Notification 对象同时放到 recvqueue 中。

FastLeaderElection::lookForLeader 会不断的从 recvqueue 中获取 Notification , 当发现满足 totalOrderPredicate 条件,即:

protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
return ((newEpoch > curEpoch) ||
((newEpoch == curEpoch) &&
((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}

如果满足了 totalOrderPredicate 条件,则认为其他节点的候选人比当前的候选人要优秀,则通过 updateProposal 将这个更优秀的候选人设定为当前的候选人。

if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock, proposedEpoch))) {
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock, proposedEpoch);
leaveInstance(endVote);
return endVote;
}

当发现大部分的节点的候选人都趋于统一的时候,则认为选举结束,退出选举流程。

总结

通过 FastLeaderElection ,我们看到只有 PARTICIPANT 的节点才会被列入候选人,即便 OBSERVERPARTICIPANT 中推荐自身,但是也会被在第一时间打回。从而确保了 Leader节点只会产生在 PARTICIPANT 中。

根据 totalOrderPredicate 条件我们还可以看出,FastLeaderElection 的选主算法所能够选举出的主节点是固定的,在选主的过程中,一定会选出拥有最大的zxid的节点(epoch的值也是根据zxid进行计算的,zxidA>zxidB 时,必然有 epochA>=epochB)。如果拥有最大的 zxid 的节点有多个,则一定会选择 sid 更大的那一个。

FastLeaderElecton 的选举中,整个选举算法的时间复杂度是 O(n), 能够确保只要节点同其他节点沟通一次之后,一定能够找到最优秀的候选人,从而将其设置为Leader节点。

PS: 整个ZooKeeper 的源码分析就到此结束了,谢谢大家的阅读。