Zookeeper Leader选举实现分析

Vote

        Zookeeper进行Leader选举时,各服务器通过发送投票来进行Leader的选举,因此先了解一下投票的结构。Zookeeper中投票被抽象为org.apache.zookeeper.server.quorum.Vote,主要属性有:
        id:被推举的Leader的SID。
        zxid:被推举的Leader的事务ID。
        electionEpoch:逻辑时钟,用于判断多个投票是否在同一轮选举周期中,是一个自增序列,每次进入新一轮的投票后,都会对其进行加1操作。
        peerEpoch:被推举的Leader的epoch。
        state:当前服务器的状态。

QuorumCnxManager

        在选举期间,各服务器不但要将自己的投票发送给其他的服务器,还要接收其他服务器发送给自己的投票,因此需要将集群中的所有机器都两两建立网络连接。在Zookeeper中通过org.apache.zookeeper.server.quorum.QuorumCnxManager来负责Leader选举过程中的网络通信。QuorumCnxManager在启动时,会创建一个ServerSocket来监听Leader选举的通信端口,这个端口就是zoo.cfg文件中机器列表中配置的第二个端口,在开启端口监听后,会接收到来自其他服务器的创建连接请求,在接收到其他服务器的连接请求时,由receiveConnection处理。为避免两台机器之间重复创建连接,Zookeeper设定了一个规则:只允许SID大的服务器主动和其他服务器连接。
        QuorumCnxManager中维护了一系列的队列,用来保存接收到的、待发送的消息,以及消息的发送器。这些队列按照SID分组形成队列集合。
         recvQueue:消息接收队列,用于存放从其他服务器接收到的消息。
         queueSendMap:消息发送队列,用于保存待发送的消息,按SID进行分组,分别为集群中的每台机器分配了一个单独的队列,从而保证各台机器之间的消息发送互不影响。
         senderWorkerMap:发送器集合,每个SendWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,同样按照SID进行了分组。
         lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息。
        每个服务器都单独分配了消息发送器SendWorker,每个SendWorker只需要不断的从对应的消息发送队列中获取一条消息来发送即可,同时将此消息放入lastMessageSent中。
        

FastLeaderElection

        目前Zookeeper中只保留了FastLeaderElection这个一种选举策略。

基本概念

        外部投票:其他服务器发来的投票。
        内部投票:服务器自身当前的投票。
        选举轮次:Zookeeper服务器Leader选举的轮次,即logicalclock。
        PK:对内部投票和外部投票进行一个对比来确定是否需要变更内部投票。

选票管理

        sendqueue:选票发送队列,用于保存待发送的选票。
        recvqueue:选票接收队列,用于保存接收到的外部选票。
        WorkerReciver:选票接收器。不断的从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue队列中。在选票接收的过程中,如果发现外部选票的选举轮次小于当前服务器,就直接忽略该选票,同时立即发出自己的内部投票。如果当前服务器不是LOOKING状态,同样也会忽略选票,并将Leader信息以投票的方式发送出去。如果接收到的消息来自Observer服务器,那么就忽略该消息,并将自己当前的投票发送出去。
        WorkerSender: 选票发送器。不断的从sendqueue队列中获取待发送的选票,并将其传递到底层QuorumCnxManager中去。

选举过程

        1.自增选举轮次。在FastLeaderElection实现中,有一个logicalclock属性,用于标识当前选举轮次。在开始新一轮投票前会首先对logicalclock进行自增操作。
        2.初始化选票。在初始化阶段每台服务器都会推举自己为Leader。
        3.发送初始化选票。在初始化完成后,服务器会发起第一轮投票。Zookeeper会将初始化好的选票放入sendqueue队列中,由发送器WorkerSender负责发送出去。
        4.接收外部选票。每台服务器都会不断的从recvqueue队列中获取外部投票。如果服务器发现无法获取到任何的外部投票,会立即确定自己是否和集群中其他服务器保持着有效连接。如果发现没有建立连接,就会马上建立连接,如果已经建立连接,就会再次发送自己当前的投票。
        5.判断选举轮次。
                外部投票的选举轮次大于内部投票,立即更新自己的选举轮次,并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。
                外部投票的选举轮次小于内部投票,忽略外部投票,不做任何处理。
                外部投票的选举轮次和内部投票一致,进行选票PK。
        只有在同一选举轮次的投票才是有效的投票。
        6.选票PK。选票PK是FastLeaderElection.totalOrderPredicate方法的核心逻辑。选票PK的目的是为了确定当前服务器是否需要变更投票,主要从选举轮次、ZXID和SID三个因素来考虑。
                如果外部投票中被选举的Leader服务器的选举轮次大于内部投票,就需要进行投票变更。
                如果选举轮次一致,对比ZXID,如果外部投票的ZXID大于内部投票,就需要进行投票变更。
                如果ZXID一致,就对比SID,如果外部投票的SID大于内部投票,就需要进行投票变更。
        7.投票变更。通过选票PK后,如果确定了外部投票优于内部投票,就进行投票变更——使用外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的内部投票发送出去。
        8.选票归档。
        无论是否进行了投票变更,都会将刚刚收到的那份外部投票放入“选票集合”recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票——按照服务器对应的SID来区分。
        9.统计投票。
        完成了选票归档之后,就可以开始统计投票。统计投票的目的是为了统计集群中是否已经有过半的服务器认可了当前的内部投票。如果确定已经有过半的服务器认可了该内部投票,则终止投票。
        10.更新服务器状态。
        统计投票后,如果确定可以终止投票,就开始更新服务器状态。服务器首先会判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,如果是自己,就将自己的状态更新为LEADING,如果自己不是被选举产生的Leader,就根据具体情况来去顶自己是FOLLOWING或是OBSERVING。
        步骤4-9会经过几轮循环,直到Leader选举产生,另外在完成步骤9后,如果统计投票发现已经有过半的服务器认可了当前的选票,不会立即进入步骤10,而是会等待一段时间(默认200ms)来确定是否有新的更优的投票。

猜你喜欢

转载自blog.csdn.net/asty9000/article/details/80247369