服务器状态
为了能够清楚地对ZooKeeper集群中每台机器的状态进行标识,在org.apache.zookeeper. server.quorum.QuorumPeer.ServerState类中列举了4种服务器状态,分别是:LOOKING、 FOLLOWING、LEADING 和OBSERVING。
LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举流程。
FOLLOWING:跟随者状态,表明当前服务器角色是Follower。
LEADING:领导者状态,表明当前服务器角色是Leader。
OBSERVING:观察者状态,表明当前服务器角色是Observer。
投票数据结构
Leader 的选举过程是通过投票来实现的,同时每个投票中包含两个最基本的信息:所推举服务器的SID和ZXID。现在我们来看在ZooKeeper中对Vote数据结构的定义,如下表所示。
Vote |
- id: long - zxid: long - electionEpoch: long - peerEpoch: long - state: ServerState |
读者可以在org.apache.zookeeper.server.quorum.Vote类中查看其完整的定义,下表中列举了Vote中的几个属性。
属性 |
说明 |
Id |
被推举的Leader的SID值 |
Zxid |
被推举的Leader的事务ID |
electionEpoch |
逻辑时钟,用来判断多个投票是否在同一轮选举周期中。该值在服务端: 是一个自增序列。每次进入新一轮的投票后,都会对该值进行加1操作 |
peerEpoch |
被推举的Leader的epoch |
state |
当前服务器的状态 |
QuorumCnxManager:网络I/O
ClientCnxn是ZooKeeper客户端中用于处理网络I/O的一个管理器。在Leader选举的过程中也有类似的角色,那就是QuorumCnxManager——每台服务器启动的时候,都会启动一个QuorumCnxManager,负责各台服务器之间的底层Leader选举过程中的网络通信。
消息队列
在QuorumCnxManager这个类内部维护了一系列的队列,用于保存接收到的、待发送的消息,以及消息的发送器。除接收队列以外,这里提到的所有队列都有一个共同点——按SID分组形成队列集合,我们以发送队列为例来说明这个分组的概念。假设集群中除自身外还有4台机器,那么当前服务器就会为这4台服务器分别创建一个发送队列,互不干扰。
recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息。
queueSendMap:消息发送队列,用于保存那些待发送的消息。queueSendMap 是一个Map,按照SID进行分组,分别为集群中的每台机器分配了一个单独队列,从而保证各台机器之间的消息发送互不影响。
senderWorkerMap:发送器集合。每个SendWorker消息发送器,都对应一台远程ZooKeeper服务器,负责消息的发送。同样,在senderWorkerMap中,也按照SID进行了分组。
lastMessageSent:最近发送过的消息。在这个集合中,为每个SID保留最近发送过的一个消息。
建立连接
为了能够进行互相投票,ZooKeeper集群中的所有机器都需要两两建立起网络连接。QuorumCnxManager在启动的时候,会创建一个ServerSocket来监听Leader选举的通信端(Leader 选举的通信端口默认是3888)。开启端口监听后,ZooKeeper就能够不断地接收到来自其他服务器的“ 创建连接”请求,在接收到其他服务器的TCP连接请求时,会交由receiveConnection函数来处理。为了避免两台机器之间重复地创建TCP连接,ZooKeeper 设计了一种建立TCP连接的规则:只允许SID大的服务器主动和其他服务器建立连接,否则断开连接。在ReceiveConnection函数中,服务器通过对比自己和远程服务器的SID 值,来判断是否接受连接请求。如果当前服务器发现自己的SID值更大,那么会断开当前连接,然后自己主动去和远程服务器建立连接。
一旦建立起连接,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动他们。
消息接收与发送
消息的接收过程是由消息接收器RecvWorker来负责的。在上面的讲解中,我们已经提到了ZooKeeper 会为每个远程服务器分配一个单独的RecvWorker,因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中。
消息的发送过程也比较简单,由于ZooKeeper同样也已经为每个远程服务器单独分别分配了消息发送器SendWorker,那么每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息来发送即可,同时将这个消息放入lastMessageSent中来作为最近发送过的消息。在SendWorker的具体实现中,有一个细节需要我们注意一下:一旦ZooKeeper发现针对当前远程服务器的消息发送队列为空,那么这个时候就需要从lastMessageSent中取出一个最近发送过的消息来进行再次发送。这个细节的处理主要是为了解决这样一类分布式问题:接收方在消息接收前,或者是在接收到消息后服务器挂掉了,导致消息尚未被正确处理。那么如此重复发送是否会导致其他问题呢?
当然,这里可以放心的一点是,ZooKeeper 能够保证接收方在处理消息的时候,会对重复消息进行正确的处理。
FastLeaderElection:选举算法的核心部分
下面我们来看Leader选举的核心算法部分的实现。在讲解之前,我们首先约定几个概念。
外部投票:特指其他服务器发来的投票。
内部投票:服务器自身当前的投票。
选举轮次:ZooKeeper服务器Leader选举的轮次,即logicalclock。
PK:指对内部投票和外部投票进行一个对比来确定是否需要变更内部投票。
选票管理
我们已经讲解了,在QuorumCnxManager中,ZooKeeper是如何管理服务器之间的投票发送和接收的,现在我们来看对于选票的管理。下图所示是选票管理过程中相关组件之间的协作关系。
Sendqueue:选票发送队列,用于保存待发送的选票。
Recvqueue:选票接收队列,用于保存接收到的外部投票。
WorkerReceiver:选票接收器。该接收器会不断地从QuorumCnxManager中获取出其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue队列中去。在选票的接收过程中,如果发现该外部投票的选举轮次小于当前服务器,那么就直接忽略这个外部投票,同时立即发出自己的内部投票。当然,如果当前服务器并不是LOOKING状态,即已经选举出了Leader,那么也将忽略这个外部投票,同时将Leader信息以投票的形式发送出去。
另外,对于选票接收器,还有一个细节需要注意,如果接收到的消息来自Observer服务器,那么就忽略该消息,并将自己当前的投票发送出去。
WorkerSender:选票发送器,会不断地从sendqueue队列中获取待发送的选票,并将其传递到底层QuorumCnxManager中去。
算法核心
在下图中,我们可以看到FastLeaderElection模块是如何与底层的网络I/O进行交互的,其中不难发现,在“选举算法”中将会对接收到的选票进行处理。下面我们就来看看这个选举过程的核心算法实现,下图二展示了Leader选举算法实现的流程示意图。
上图中展示了Leader选举算法的基本流程,其实也就是lookForLeader方法的逻辑。当ZooKeeper服务器检测到当前服务器状态变成LOOKING时,就会触发Leader选举,即调用lookForLeader方法来进行Leader选举。
(1)自增选举轮次。
在FastLeaderElection实现中,有一个logicalclock属性,用于标识当前Leader的选举轮次,ZooKeeper规定了所有有效的投票都必须在同一轮次中。
ZooKeeper在开始新一轮的投票时,会首先对logicalclock进行自增操作。
(2)初始化选票。
在开始进行新一轮的投票之前,每个服务器都会首先初始化自己的选票。在上面中我们已经讲解了Vote数据结构,初始化选票也就是对Vote属性的初始化。
在初始化阶段,每台服务器都会将自己推举为Leader,下表展示了一个初始化的选票。
属性 |
说明 |
Id |
当前服务器自身的SID |
Zxid |
当前服务器最新的ZXID值 |
electionEpoch |
当前服务器的选举轮次 |
peerEpoch |
被推举的服务器的选举轮次 |
state |
LOOKING |
(3)发送初始化选票。
在完成选票的初始化后,服务器就会发起第一次投票。ZooKeeper会将刚刚初始化好的选票放人sendqueue队列中,由发送器WorkerSender负责发送出去。
(4)接收外部投票。
每台服务器都会不断地从recvqueue队列中获取外部投票。如果服务器发现无法获取到任何的外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效连接。如果发现没有建立连接,那么就会马上建立连接。如果已经建立了连接,那么就再次发送自己当前的内部投票。
(5)判断选举轮次。
当发送完初始化选票之后,接下来就要开始处理外部投票了。在处理外部投票的时候,会根据选举轮次来进行不同的处理。
外部投票的选举轮次大于内部投票。
如果服务器发现自己的选举轮次已经落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票,最终再将内部投票发送出去。
外部投票的选举轮次小于内部投票。
如果接收到的选票的选举轮次落后于服务器自身的,那么ZooKeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4。
外部投票的选举轮次和内部投票一致。
这也是绝大多数投票的场景,如果外部投票的选举轮次和内部投票-致的话,
那么就开始进行选票PK。
总的来说,只有在同一个选举轮次的投票才是有效的投票。
(6)选票PK。
在步骤5中提到,在收到来自其他服务器有效的外部投票后,就要进行选票PK了也就是FastLeaderElection.totalOrderPredicate方法的核心逻辑。
选票PK的目的是为了确定当前服务器是否需要变更投票,主要从选举轮次、ZXID和SID三个因素来考虑,具体条件如下:在选票PK的时候依次判断,符合任意一个条件就需要进行投票变更。
如果外部投票中被推举的Leader服务器的选举轮次大于内部投票,那么就需要进行投票变更。
如果选举轮次一致的话,那么就对比两者的ZXID。如果外部投票的ZXID大于内部投票,那么就需要进行投票变更。
如果两者的ZXID 一致,那么就对比两者的SID。如果外部投票的SID大于内部投票,那么就需要进行投票变更。
(7)变更投票。
通过选票PK后,如果确定了外部投票优于内部投票(所谓的“优于”,是指外部投票所推举的服务器更适合成为Leader),那么就进行投票变更——使用外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的内部投票发送出去。
(8)选票归档。
无论是否进行了投票变更,都会将刚刚收到的那份外部投票放入“选票集合”recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票——按照服务器对应的SID来区分,例如,{(1, vote1), (2,vote2),...}。
(9)统计投票
完成了选票归档之后,就可以开始统计投票了。统计投票的过程就是为了统计集群中是否已经有过半的服务器认可了当前的内部投票。如果确定已经有过半的服务器认可了该内部投票,则终止投票。否则返回步骤4。
(10)更新服务器状态。
统计投票后,如果已经确定可以终止投票,那么就开始更新服务器状态。服务器会首先判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,如果是自己的话,那么就会将自己的服务器状态更新为LEADING。如果自己不是被选举产生的Leader 的话,那么就会根据具体情况来确定自己是FOLLOWING或是OBSERVING。
以上10个步骤,就是FastLeaderElection 选举算法的核心步骤,其中步骤4~9会经过几轮循环,直到Leader选举产生。另外还有一个细节需要注意,就是在完成步骤9之后,如果统计投票发现已经有过半的服务器认可了当前的选票,这个时候,ZooKeeper并不会立即进入步骤10来更新服务器状态,而是会等待一段时间(默认是200毫秒)来确定是否有新的更优的投票。