Zookeeper源码学习(三):Leader选举和各服务器角色介绍

433 阅读17分钟

1. Leader选举

1. Leader选举概述

服务器启动时期的Leader选举

  1. 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
  2. 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。
  3. 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
    • 优先检查ZXID。ZXID比较大的服务器优先作为Leader。
    • 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
    • 对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
  4. 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。
  5. 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。

服务器运行时期的Leader选举   

  1. 变更状态。Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
  2. 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。
  3. 接收来自各个服务器的投票。与启动时过程相同。
  4. 处理投票。与启动时过程相同,此时,Server1将会成为Leader。
  5. 统计投票。与启动时过程相同。
  6. 改变服务器的状态。与启动时过程相同。

2. Leader选举的算法分析

术语解释

  • SID:服务器ID,唯一标识一台Zookeeper集群中的机器。
  • ZXID:事务ID,唯一标识一次服务器状态的变更。
  • Vote:投票
  • Quorum:过半机器数

算法分析

在3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法。当Zookeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举

  • 服务器初始化启动
  • 服务器运行期间无法和Leader保持连接
  1. 第一次投票。集群的所有机器都处于试图选举出一个Leader的状态,即LOOKING状态,LOOKING机器会向所有其他机器发送消息,该消息称为投票。投票中包含了SID(服务器的唯一标识)和ZXID(事务ID),(SID, ZXID)形式来标识一次投票信息。假定Zookeeper由5台机器组成,SID分别为1、2、3、4、5,ZXID分别为9、9、9、8、8,并且此时SID为2的机器是Leader机器,某一时刻,1、2所在机器出现故障,因此集群开始进行Leader选举。在第一次投票时,每台机器都会将自己作为投票对象,于是SID为3、4、5的机器投票情况分别为(3, 9),(4, 8), (5, 8)。
  2. 变更投票。每台机器发出投票后,也会收到其他机器的投票,每台机器会根据一定规则来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票,这个规则也是整个Leader选举算法的核心所在,每次对收到的投票的处理,都是对(vote_sid, vote_zxid)和(self_sid, self_zxid)对比的过程。
    • 规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去。
    • 规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更。
    • 规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去。
    • 规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更。

选举投票

  1. 确定Leader。经过第二轮投票后,集群中的每台机器都会再次接收到其他机器的投票,然后开始统计投票,如果一台机器收到了超过半数的相同投票,那么这个投票对应的SID机器即为Leader。此时Server3将成为Leader。

数据越新,那么它的ZXID也就越大,也就越能够保证数据的恢复。

3. Leader选举的实现细节

Vote类

Vote类是ZK选举的实体类,Zookeeper的快速选举算法就是利用id, zxid和epoch来选举出新的Leader。

  • id:选票推举的Leader的SID(配置文件中配置的)
  • zxid:被推举的Leader的事务ID
  • electionEpoch:逻辑时钟。是一个递增的数字,通过对比electionEpoch来判断Server自己的Vote和其他Vote是否在同一个选举轮次中。每次进入一个新的选举轮次,electionEpoch都会+1。
  • peerEpoch:被推举的Leader的epoch。
  • state:就是前面说的几种当前Server的状态。

选举类图关系

QuorumCnxManager:网络I/O

每台服务器启动的时候,都会启动一个QuorumCnxManager,负责各台服务器之间的底层Leader选举过程中的网络通信。

QuorumCnxManager

消息队列

QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合,如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰。

  • recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息。
  • queueSendMap:消息发送队列,用于保存那些待发送的消息,按照SID进行分组。
  • senderWorkerMap:发送器集合,每个SenderWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,也按照SID进行分组。
  • lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息。
// 发送器集合,每个sid都会有一个发送器,用来发送对应sid的消息发送队列的消息
final ConcurrentHashMap<Long, SendWorker> senderWorkerMap;
// 消息发送队列,每个sid都会有一个队列
final ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap;
// 保存每个sid发送的最后一条消息
final ConcurrentHashMap<Long, ByteBuffer> lastMessageSent;

// recvQueue 消息接收队列,用于存放那些从其他服务器接收到的消息。
public final ArrayBlockingQueue<Message> recvQueue;

建立连接

为了能够相互投票,Zookeeper集群中的所有机器都需要两两建立起网络连接。QuorumCnxManager在启动时会创建一个ServerSocket来监听Leader选举的通信端口(默认为3888)。为了避免两台机器之间重复地创建TCP连接,Zookeeper只允许SID大的服务器主动和其他机器建立连接,否则断开连接。一旦连接建立,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动。

消息接收与发送

  • 消息接收:由消息接收器RecvWorker负责,由于Zookeeper为每个远程服务器都分配一个单独的RecvWorker,因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中。
  • 消息发送:由于Zookeeper为每个远程服务器都分配一个单独的SendWorker,因此,每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息发送即可,同时将这个消息放入lastMessageSent中。在SendWorker中,一旦Zookeeper发现针对当前服务器的消息发送队列为空,那么此时需要从lastMessageSent中取出一个最近发送过的消息来进行再次发送,这是为了解决接收方在消息接收前或者接收到消息后服务器挂了,导致消息尚未被正确处理。同时,Zookeeper能够保证接收方在处理消息时,会对重复消息进行正确的处理。

选票管理

  • sendqueue:选票发送队列,用于保存待发送的选票。
  • recvqueue:选票接收队列,用于保存接收到的外部投票。
  • WorkerReceiver:选票接收器。其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中,在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票,同时立即发送自己的内部投票。
  • WorkerSender:选票发送器,不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。
LinkedBlockingQueue<ToSend> sendqueue;
LinkedBlockingQueue<Notification> recvqueue;
class WorkerSender extends ZooKeeperThread {
class WorkerReceiver extends ZooKeeperThread {

选举类交互

Notification:它的目的就是通知其他Peer修改了选票。从Notification的成员变量可以看,Notification基本和Vote类一致。但是在Notification类里有一个version用来标记当前Notification的version,可能是为了用来做不同版本ZK之间通信来做一些逻辑处理,这部分目前没看到有什么实际的使用。

ToSend:ToSend主体和Vote类也一致,但是ToSend类多了一个sid,用来判断发给哪个server,为了要包装这样一个类,我的想法是方便在FastLeaderElection处理业务逻辑的便利。

Messenger:从代码结构中可以看到,Messenger主要分为WorkerReceiver和WorkerSender两个子类。

算法核心

  1. 自增选举轮次。Zookeeper规定所有有效的投票都必须在同一轮次中,在开始新一轮投票时,会首先对logicalclock进行自增操作。
  2. 初始化选票。在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader。
  3. 发送初始化选票。完成选票的初始化后,服务器就会发起第一次投票。Zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去。
  4. 接收外部投票。每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票。
  5. 判断选举轮次。在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进行不同的处理。
    • 外部投票的选举轮次大于内部投票。若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。
    • 外部投票的选举轮次小于内部投票。若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4。
    • 外部投票的选举轮次等于内部投票。此时可以开始进行选票PK。
  6. 选票PK。在进行选票PK时,符合任意一个条件就需要变更投票。
    • 若外部投票中推举的Leader服务器的选举轮次大于内部投票,那么需要变更投票。
    • 若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票。
    • 若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票。
  7. 变更投票。经过PK后,若确定了外部投票优于内部投票,那么就变更投票,即使用外部投票的选票信息来覆盖内部投票,变更完成后,再次将这个变更后的内部投票发送出去。
  8. 选票归档。无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票(按照服务队的SID区别,如{(1, vote1), (2, vote2)...})。
  9. 统计投票。完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,则终止投票。否则返回步骤4。
  10. 更新服务器状态。若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为LEADING,若不是,则根据具体情况来确定自己是FOLLOWING或是OBSERVING。

从Paxos到Zookeeper-21

2. 各服务器角色介绍

各服务器角色介绍总结

1. Leader

  • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
  • 集群内部各服务器的调度者。

请求处理链

从Paxos到Zookeeper-22

PrepRequestProcessor

  • PrepRequestProcessor是Leader服务器的请求预处理器,PrepRequestProcessor能够识别出当前客户端请求是否是事务请求。对于事务请求PrepRequestProcessor处理器会对其进行系列一预处理,诸如创建请求事务头、事务体,会话检查、ACL检查和版本检查等。

ProposalRequestProcessor

  • ProposalRequestProcessor处理器是Leader服务器的事务投票处理器,也是Leader服务器事务处理流程的发起者。而对于事务请求,除了将请求交给CommitProcessor处理器外,还会根据请求类型创建对应的Proposal提议,并发送给所有的Follower服务器来发起一次集群内的事务投票。同时,ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进行事务日志的记录。

SyncRequestProcessor

  • SyncRequestProcessor是事务日志记录处理器,该处理器主要用来将事务请求记录到事务日志文件中去,同时还会触发ZooKeeper 进行数据快照。

AckRequestProcessor

  • AckRequestProcessor处理器是Leader 特有的处理器,其主要负责在SyncRequestProcessor处理器完成事务日志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务日志记录。

CommitProcessor

  • CommitProcessor是事务提交处理器。对于非事务请求,该处理器会直接将其交付给下一级处理器进行处理,而对于事务请求,CommitProcessor 处理器会等待集群内针对Proposal的投票直到该Proposal可被提交。

ToBeCommitProcessor

  • ToBeCommitProcessor处理器中有一个toBeApplied队列,专门用来存储那些已经被CommitProcessor处理过的可被提交的Proposal。ToBeCommitProcessor处理器将这些请求逐个交付给FinalRequestProcessor处理器进行处理一一等到FinalRequestProcessor处理器处理完之后,再将其从toBeApplied队列中移除。

FinalRequestProcessor

  • FinalRequestProcessor是最后一个请求处理器。该处理器主要用来进行客户端请求返回之前的收尾工作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应用到内存数据库中去。

LearnerHandler

  • 为了保持整个集群内部的实时通信,同时也是为了确保可以控制所有的Follower/ Observer服务器,Leader服务器会与每一个Follower/Observer服务器都建立一个TCP长连接,同时也会为每个Follower/Observer 服务器都创建一个名为LearnerHandler的实体。
  • LearnerHandler,是ZooKeeper集群中Learner服务器的管理器,主要负责Follower/Observer服务器和Leader服务器之间的一系列网络通信,包括数据同步、请求转发和Proposal提议的投票等。Leader服务器中保存了所有Follower/Observer对应的LearnerHandler。

2. Follower

  • 处理客户端非事务请求,转发事务请求给Leader服务器
  • 参与事务请求Proposal的投票
  • 参与Leader选举投票

从Paxos到Zookeeper-23

FollowerRequestProcessor

  • FollowerRequestProcessor是Follower服务器的第一个请求处理器,其主要工作就是识别出当前请求是否是事务请求。如果是事务请求,那么Follower就会将该事务请求转发给Leader服务器,Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进行处理。

SendAckRequestProcessor

  • SendAckRequestProcessor是Follower服务器上另外一个和Leader服务器有差异的请求处理器。
  • Leader服务器上有一个叫AckRequestProcessor的请求处理器,其主要负责在SyncRequestProcessor处理器完成事务日志记录后,向Proposal的投票收集器进行反馈。
  • 在Follower服务器上,SendAckRequestProcessor处理器同样承担了事务日志记录反馈的角色,在完成事务日志记录后,会向Leader服务器发送ACK消息以表明自身完成了事务日志的记录工作。两者的唯一区别在于,AckRequestProcessor处理器和Leader服务器在同一个服务器上,因此它的ACK反馈仅仅是一个本地操作,而SendAckRequestProcessor处理器由于在Follower服务器上,因此需要通过以ACK消息的形式来向Leader服务器进行反馈。

3. Observer

  • 和Follower唯一的区别在于,Observer不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票。
  • Observer服务器只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

从Paxos到Zookeeper-24

4. 集群间消息通信

Zookeeper的消息类型大体上可以分为四类,分别是:数据同步型,服务器初始化型,请求处理型和会话管理型。

数据同步型

数据同步型消息是指在Learner和Leader服务器进行数据同步的时候,网络通信所用到的消息。

从Paxos到Zookeeper-25

服务器初始化型

服务器初始化型消息是指在整个集群或是某些新机器初始化的时候,Leader和Learner之间相互通信所使用的消息类型。

从Paxos到Zookeeper-26

从Paxos到Zookeeper-27

请求处理型

请求处理型消息是指在进行请求处理的过程中,Leader和Learner服务器之间互相通信所使用的消息。

从Paxos到Zookeeper-28

从Paxos到Zookeeper-29

会话管理型

会话管理型消息是指Zookeeper在进行会话管理的过程中,和Learner服务器之间互相通信所使用的信息。

从Paxos到Zookeeper-30

最后

大家可以关注我的微信公众号一起学习进步。