一、概述
在上一篇博文中我们分析了FastLeaderElection
中的lookForLeader
方法中的选举逻辑,当时分析完后觉得逻辑还是比较合理的,但是今早刷牙的时候突然产生了一个疑问,如果lookForLeader
中对于选票的处理是分状态进行处理的,那选票中的状态到底投票人的状态还是候选者的状态呢?
并且通过昨天的源码分析我们已经可以确定当节点完成选举后即会退出选举循环,但如果在lookForLeader
中接收到了非LOOKING
状态的节点则又意味着选举循环并没有结束,这两者之间产生了明显的矛盾,因此带着这样的疑问我再次踏上了探究FastLeaderElection
源码的路程。
二、初探 FLE 算法实现
三、源码解析
3.1 问题探究
在概述中已经描述了出现的问题,为了探究选票中状态的归属,我们首先进入到lookForLeader
方法中对于发送选票方法的调用,通过上篇博文的描述我们能够知道在选举中节点都是通过sendNotifications
方法来发送选票的,所以我们直接进入到这个方法的逻辑中。
当看到这个方法的逻辑时我突然就懵了,因为我们可以很明显的看到在通过这个方法发送选票时,对于选票中选票状态是直接写死为LOOKING
的,而这也就意味着通过lookForLeader
方法发送的选票的状态全部都应该为LOOKING
,但是如果这样分析那就意味着在lookForLeader
方法中应当永远接收不到别的状态的选票(因为当节点发现选举结束后会在修改状态后直接退出选举循环,因此也不会再发送新的选票)。
private void sendNotifications() {
for (long sid : self.getCurrentAndNextConfigVoters()) {
QuorumVerifier qv = self.getQuorumVerifier();
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock.get(),
QuorumPeer.ServerState.LOOKING,
sid,
proposedEpoch, qv.toString().getBytes());
sendqueue.offer(notmsg);
}
}
在从lookForLeader
方法寻找答案失败后,我的第二个思路就是如果通过这个方法发送的选票状态均为LOOKING
的话,那么就意味着在FastLeaderElection
中一定还有其它合成并发送选票的地方,因此通过在FastLeaderElection
中对发送选票方法的搜索,我终于在WorkerReceiver
这个内部类的run
方法中发现了问题的答案。
3.2 WorkerReceiver 中的选举逻辑
对于WorkerReceiver
这个内部类我们在进行选举初始化时也提到过,这个内部类继承了ZooKeeperThread
,因此本质也是一个线程,并且在选举初始化时已经通过start
方法调用了其run
方法。而这个内部类的作用正如它的类名所述就是一个 接受者线程 ,主要用于从网络层不断接收并处理其它节点发送的数据包。
// 创建一个空选票
Notification n = new Notification();
// 如果该选票是来自于一个无投票权的节点(比如观察者节点或者无投票权的跟随者节点)
if(!validVoter(response.sid)) {
// 获取当前节点所跟随的领导者的信息并将其返回给投票人
Vote current = self.getCurrentVote();
QuorumVerifier qv = self.getQuorumVerifier();
ToSend notmsg = new ToSend(ToSend.mType.notification,
current.getId(),
current.getZxid(),
logicalclock.get(),
self.getPeerState(),
response.sid,
current.getPeerEpoch(),
qv.toString().getBytes());
// 发送选票到网络层
sendqueue.offer(notmsg);
} else {
// 解析选票的投票人状态
QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING;
switch (rstate) {
case 0:
ackstate = QuorumPeer.ServerState.LOOKING;
break;
case 1:
ackstate = QuorumPeer.ServerState.FOLLOWING;
break;
case 2:
ackstate = QuorumPeer.ServerState.LEADING;
break;
case 3:
ackstate = QuorumPeer.ServerState.OBSERVING;
break;
default:
continue;
}
// 补全选票信息
n.leader = rleader;
n.zxid = rzxid;
n.electionEpoch = relectionEpoch;
n.state = ackstate;
n.sid = response.sid;
n.peerEpoch = rpeerepoch;
n.version = version;
n.qv = rqv;
// 如果节点当前的状态为 LOOKING 则将选票交由上层逻辑处理
if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){
recvqueue.offer(n);
// 如果选票的状态为 LOOKING 且该选票的 Epoch 落后于当前节点的 Epoch
// 则将当前节点预期领导者的信息组成选票发送给投票人
if((ackstate == QuorumPeer.ServerState.LOOKING) && (n.electionEpoch < logicalclock.get())){
// 获取当前节点预期领导者信息
Vote v = getVote();
QuorumVerifier qv = self.getQuorumVerifier();
ToSend notmsg = new ToSend(ToSend.mType.notification,
v.getId(),
v.getZxid(),
logicalclock.get(),
self.getPeerState(),
response.sid,
v.getPeerEpoch(),
qv.toString().getBytes());
// 发送选票到网络层
sendqueue.offer(notmsg);
}
} else {
// 如果当前节点的状态非 LOOKING 但是选票的投票人为 LOOKING 状态
// 则直接将当前节点所认定的领导者信息返回
Vote current = self.getCurrentVote();
if(ackstate == QuorumPeer.ServerState.LOOKING){
QuorumVerifier qv = self.getQuorumVerifier();
ToSend notmsg = new ToSend(
ToSend.mType.notification,
current.getId(),
current.getZxid(),
current.getElectionEpoch(),
self.getPeerState(),
response.sid,
current.getPeerEpoch(),
qv.toString().getBytes());
// 发送选票到网络层
sendqueue.offer(notmsg);
}
}
}
通过源码的分析我们可以发现其实FastLeaderElection
最初从网络层获取到的仅仅是投票人发送的一个投票数据包,这个数据包会在WorkerReceiver
这个线程中完成第一步的判断,并在确定投票数据包有效且需要进一步的判断情况下才会通过recvqueue
传递给上层的lookForLeader
方法做进一步的判断。
而对于网络层传来的投票数据包进行的第一步验证就是判断数据包的发送者 是否有资格参与投票 ,这主要是因为在Zookeeper
中如果一个节点在配置文件中被声明为Observer
时它是没有资格参加投票的,同时因为每个节点的配置文件是单独进行配置的,所以可能出现配置文件不一致的情况,因此在Zookeeper
的QuorumPeerConfig
中会通过lastSeenQuorumVerifier
和quorumVerifier
两个属性来分别记录前一次配置文件中所配置的集群中的节点信息和当前配置文件中节点的配置信息,如果一个Follower
既不在上一次的配置文件中也不在当前的配置文件中那么就可以认为这个Follower
是无效的,这时它也是没有权利来进行投票的。因此当接收到没有资格参与投票的节点发来的选票时,可以直接将当前节点所认为的领导者的相关信息组成新的选票后,发送给该投票者。所以可以认为在这种情况下投票数据包并没有生成选票,也并没有传递给上层的lookForLeader
方法进行处理。
但对于有资格参与投票的节点我们也需要进行区别对待,首先需要判断当前节点的状态是否为LOOKING
,如果是的话则证明当前节点仍处于选举状态中,所以需要将投票的数据包转化为选票后发送给上层的lookForLeader
方法做进一步的判断处理。而我们在lookForLeader
方法中也看到了当选票的Epoch
小于当前节点的Epoch
时lookForLeader
方法是不做任何处理的,这一点也反映在了WorkerReceiver
中,当其将选票发送给上层的lookForLeader
方法后会再次判断选票的Epoch
和当前节点Epoch
之间的关系,如果小于的话则默认在WorkerReceiver
中获取到当前节点所预期的领导者的信息,然后将其组成选票后发送给投票者。这其实也表明了,当选票的Epoch
小于当前节点的Epoch
时并非未做任何操作,而是将返回更新选票的工作交给了下层的WorkerReceiver
来完成,这样的机制保证了当节点发送选票后能够及时得到接收方的回应,从而及时的更新自己的相关信息。
而如果当前节点的状态为非LOOKING
时则证明其已经完成了选举,此时节点再获取到选票数据包时,只需要获取到节点当前所追随的领导者的相关信息,并在将其组成选票后发送给投票者即可。而这一步的操作其实也就解答了我们在lookForLeader
方法中的疑惑,因为对于lookForLeader
方法它的主要功能就是用来进行选举,所以当选举完成后节点会立即退出该方法,而对于之后再接收到的选票的处理工作则直接交给了底层一直运行的WorkerReceiver
来完成。通过这样的机制能够保证当一个节点完成选举后,即使它退出了选举的逻辑,那么也可以正常的处理来自其它节点的选票,这也就解释了为什么在lookForLeader
方法中我们会接收到状态为FOLLOWING
和LEADING
的选票。同时这样的机制也可以保证在WorkerReceiver
中的第一轮筛选就筛除掉一些无用的选票,而只将真正有意义的数据包组成选票交给上层的lookForLeader
来处理,大大简化了lookForLeader
中代码逻辑的复杂程度,并使整体代码的职责更加清晰明确。
3.3 FLE 算法代码实现中的消息传递
通过上一篇博文对FLE
算法实现的初探再加上这篇博文中对其部分代码实现的补充分析,我们已经对Zookeeper
中FLE
算法具体的代码实现有了一个比较整体的印象,在分析的过程中我们经常会提到一个recvqueue
队列,这个队列的主要作用就是在FLE
的应用层方法lookForLeader
和FLE
网络层方法之间传递选票,类似的还有sendqueue
队列,且这两个队列的类型均为LinkedBlockingQueue
,为了让大家对FLE
中消息传递的流程有一个更加整体化的认识,在总结分析后绘制了下面这张示意图。
因为这个系列的博文主要探究FLE
算法的实现,因此我在示意图中没有展示过多Zookeeper
网络层的内容,在接下来对FLE
消息传递的分析过程中我们也暂时屏蔽Zookeeper
网络层的具体实现。所以总结起来,对于FLE
消息传递的流程我觉得可以分为三个层次:
-
FLE
应用层:这一层以lookForLeader
方法和sendNotifications
方法为主,在lookForLeader
方法中会通过中间件层的recvqueue
队列来获取选票,在处理后会将选票发送给sendNotifications
方法,sendNotifications
方法又会通过中间件层的sendqueue
队列来发送选票到FLE
网络层; -
FLE
中间件层:这一层的主要包括recvqueue
队列和sendqueue
队列,FLE
应用层会从recvqueue
队列中获取FLE
网络层发送选票,并使用sendqueue
队列发送选票到FLE
网络层,整个FLE
的应用层和网络层正是靠这两个队列来进行连接,并做到充分解耦; -
FLE
网络层:这一层的主要包括WorkerReceiver
和WorkerSender
两个线程,WorkerReceiver
负责从Zookeeper
网络层中获取原始选票数据包,并在数据包解析后进行一定的逻辑判断,最后封装为选票通过recvqueue
队列发送给FLE
应用层或sendqueue
队列发送给WorkerSender
,而WorkerSender
主要负责的是从sendqueue
队列中获取选票,并将封装后的选票解析为数据包后发送给Zookeeper
网络层,让Zookeeper
网络层完成最终数据的发送;
应用层和网络层通过中间件层充分解耦,两者之间仅适用队列来进行数据的传递,一种典型的 生产者消费者模式 ,能够很好的平衡生产者(网络层)和消费者(应用层)之间的关系,并且能够在避免资源浪费的情况下提升处理的效率,这种模式的应用使得FLE
具有支持高并发的能力,且能够做到在不加锁的前提下实现数据的线程安全。
四、内容总结
在这篇博文里我们对Zookeeper
中FLE
算法的代码实现进行了再次探索,通过对WorkerReceiver
中代码逻辑的分析补全了初探中遗落的选举逻辑,并对FLE
代码实现中消息传递的方式进行了分析。在接下来的博文中会将两篇博文中的FLE
算法选举逻辑进行总结,并会对Zookeeper
中FLE
算法的代码实现和Zab
协议的规范进行对比分析。