目录
前置
myid:机器在集群中的编号
SID:Server ID,是一个数字,和myid一致,标识集群中一台机器的唯一标识。
ZXID:事务的编号
一、不同场景下的选举
当一台机器加入到集群,该机器会发起Leader选举。如果集群中已经存在Leader服务器,则该机器会被告知Leader信息。该机器会退出Leader选举状态,连接Leader并和同步状态;如果此时集群中不存在Leader,则会进行Leader选举。
从3.4.0以后,ZooKeeper只支持FastLeaderElection选举算法。
1、服务启动时期的Leader选举
主要步骤如下:
1、初始投票
初始情况,每个机器并不了解其他机器的信息,所以每个机器都会将自己作为候选人发起投票。投票的内容为:(myid, ZXID)。每个机器都会将自己的投票情况发送给集群中的其他所有集群。
2、接收投票
机器在接收到其他机器的投票后,会首先检查该投票的有效性:轮次是否正确(是否是本轮的投票),是否来自LOOKING状态的机器。
3、处理投票
如果接收到的投票是有效的,那么机器会将该投票和自己的投票进行对比:
1)对比ZXID,选择ZXID大的服务器作为Leader;
2)如果ZXID相同,对比myid,选择myid大的服务器作为Leader。
因为是刚刚启动,所以此时ZXID都是相同的,实际上就是在比较myid。如果接收到的投票在对比中获胜,就更新自己的投票为该投票信息,并重新发送更新后的投票信息。
4、统计投票
每次投票后,每个服务器都会对集群中所有的投票情况进行统计。如果统计到此时有过半的机器接收到相同的投票信息,就会认为被过半机器选举的服务器是Leader机器。
5、变更状态
一旦确定了服务器状态,每个服务器就会更新自己的状态:Follower更新为FOLLOWING,Leader更新为LEADING。
2、服务器运行期间的选举
如果Leader服务器因为服务崩溃、关闭、重启、网络问题等原因,导致和集群中过半机器失去联系,此时这个集群将暂时对外停止写服务,并重新进入Leader选举状态。
此时的Leader选举的步骤:
1、变更状态
非Observer机器都会将自己的状态从FOLLOWING变更为LOOKING,进入选举流程。
2、初始投票
跟服务启动时间的Leader选举相同。此时每个机器都会将自己作为候选人进行投票,生成投票信息(myid, ZXID)并通知其他机器。
3、接收投票
跟服务启动时间的Leader选举相同。接收并校验投票的有效性。
4、处理投票
跟服务启动时间的Leader选举相同。对比接收到的投票和自己的投票,如果需要就更新并重新发松投票信息。
5、统计投票
跟服务启动时间的Leader选举相同。统计集群中的所有集群的投票信息,判断是否已经选出Leader。
6、变更投票
跟服务启动时间的Leader选举相同。Follower更新为FOLLOWING,Leader更新为LEADING。
二、算法细节
FastLeaderElection算法并不复杂,但是在实现过程中还是有一些细节方面的问题。
1、投票信息的数据结构
org.apache.zookeeper.server.quorum.Vote类用来表示投票信息。主要成员属性:
属性 | 说明 |
id | 被推荐的Leader的SID |
zxid | 被推举Leader的ZXID |
electionEpoch | 逻辑时钟,用来判断是不是在同一个投票轮次;发起新一轮投票时+1 |
peerEpoch | 被推举的Leader的epoch,即zxid的高32位,每次Leader选举完成后递增 |
state | 当前服务器的状态,枚举:LOOKING, FOLLOWING, LEADING, OBSERVING |
2、网络IO:QuorumCnxManager
QuorumCnxManager用来负责服务器之间底层Leader选举过程中的通信。
2.1 消息队列
QuorumCnxManager中维护了一系列的队列,用来存储消息的发送器、待发送的消息、最后一次发送的消息、以及接收到的消息。
队列如下:
类型 | 名称 | 说明 |
ConcurrentHashMap<Long, SendWorker> | senderWorkerMap | key:其他服务的SID;value:消息的发送器,Thread的从超类 |
ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> | queueSendMap | key:其他服务的SID;value:待发送消息的队列 |
ConcurrentHashMap<Long, ByteBuffer> | lastMessageSent | key:其他服务的SID;value:最后一次发送的消息 |
ArrayBlockingQueue<Message> | recvQueue | 接收到的消息的队列 |
2.2 连接建立
1)连接管理
为了保证能进行服务启动时期的投票,服务在启动的时候就会启动QuorumCnxManager,在服务器两两之间建立连接。在QuorumCnxManager的启动过程中创建一个ServerSocket来监听Leader选举端口。
2)处理逻辑
这个监听是通过Listener来实现的,这是Thread的超类,在run方法中通过两层while循环,创建ServerSock而并不断的监听端口,一旦接收到消息,就会交给receiveConnection方法进行处理。
3)避免重复
为了避免两台机器重复建立连接,zk要求只能SID大的服务主动发起连接建立。在处理连接的方法receiveConnection中,会对比目标服务和自己的SID,判断是否接受连接请求。
4)消息发送
连接建立后,会通过SID来管理消息和消息发送器,参考上面的消息队列。
2.3 消息接收和发送
1)消息接收
消息接收通过RecvWorker来实现。zk会为每个远程服务器分配一个单独的RecvWorker,每个RecvWorker只需要不断的从这个TCP连接中读取消息,并保存到RecvQueue中。
2)消息发送
消息发送通过SendWorker来实现。zk会为每个远程服务器分配一个单独的SendWorker,这个SendWorker只需要不断的根据SID从queueSendMap中取出消息并发送,同时将这条消息根据SID放入lastMessageSent中。
需要注意的是,如果待发送消息队列空了,此时SendWorker会从lastMessageSent中取出最后一条消息,然后不断重复发送。这是为了避免连接关闭导致的最后一条消息没有被正确处理的情况。当然,接收端也会正确处理重复消息。
发送端发送最近一条消息(SendWorker的run方法):
三、算法核心
1、基本概念
外部投票:其他服务器发来的投票
内部投票:当前服务器自身的投票
选举轮次:逻辑时钟,即本次Leader选举中的投票轮次
PK:对比内部投票和外部投票,看是否要变更内部投票
2、选票管理
zk通过QuorumCnxManager管理服务器之间的投票发送和接收。FastLeaderElection中定义一些组件,用于调用QuorumCnxManager来实现投票的管理:
sendqueue:选票发送队列,保存待发送的队列;
recvqueue:选票接收队列。保存接收到的外部投票;
WorkerSender:选票发送器。不断的从sendqueue中取出投票信息,并发送给QuorumCnxManager,存储到queueSendMap。
WorkerReceiver:选票接收器。不断从QuorumCnxManager中取出外部投票,并转换成选票的类型Message,保存到revcQueue。
这里需要注意的是,对对于选票接收器,接收到的选票轮次低于自身的轮次,则忽略该投票, 并将内部投票发出去;如果自身并不在LOOKING状态,就忽略该投票,并以投票的形式发送Leader的信息;如果该投票来自于Observer,则直接忽略该信息,并发送内部投票。
选票管理的过程:
3、选举流程
选举流程通过FastLeaderElection中的lookForLeader方法实现,这也是FastLeaderElection算法的核心。
1、自增选举轮次
在FastLeaderElection实现中,有一个logicalclock属性,用于标识当前Leader的选举轮次,ZooKeeper规定了所有有效的投票都必须在同一轮次中。ZooKeeper在开始新一轮的投票时,会首先对logicalclock进行自增操作。
2、初始化选票
在开始进行新一轮的投票之前,每个服务器都会首先初始化自己的选票。在初始阶段,每个服务器都会推荐自己成为Leader。初始化投票如下:
属性 | 值 |
id | 当前服务器自身的SID |
zxid | 当前服务器最新的ZXID |
electionEpoch | 当前服务器的选举轮次 |
peerEpoch | 被推荐的服务器的选举轮次(当前服务器) |
state | LOOKING |
3、发送初始化选票
在完成选票的初始化后,服务器就会发起第一次投票。ZooKeeper会将刚刚初始化好的选票放入sendqueue队列中,由发送器WorkerSender负责发送出去。
4、接收外部投票
每台服务器都会不断地从recvqueue队列中获取外部投票。如果服务器发现无法获取到任何外部投票,就会立即确认自己是否和集群中其他服务器保持着有效连接。如果发现没有建立连接,那么就会马上建立连接;如果已经建立了连接,那么就再次发送自己当前的内部投票。
5、判断选举轮次
当发送完初始化选票之后,接下来就要开始处理外部投票了。在处理外部投票的时候,会根据选举轮次来进行不同的处理。
1)外部投票的选举轮次大于内部投票
如果服务器发现自己的选举轮次已经落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票,最终再将内部投票发送出去。
2)外部投票的选举轮次小于内部投票
如果接收到的选票的选举轮次落后于服务器自身的,那么ZooKeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4。
3)外部投票的选举轮次和内部投票一致
这是绝大多数下的情况。此时会开启选票PK。
6、选票PK
选票PK的目的是为了确定当前服务器是否需要变更投票,主要从选举轮次、ZXID和SID三个因素来考虑,依次判断,符合任意一个条件就需要进行投票变更。具体如下:
1)如果外部投票被推举Leader的选举轮次大于内部投票,则进行投票变更;
2)如果选举轮次一致,就对比两者的ZXID。如果外部投票的ZXID大于内部投票,则进行投票变更;
3)如果两者的ZXID一致,就对比两者的SID。如果外部投票的SID大于内部投票,则进行投票变更。
7、变更投票
通过选票PK后,如果确定了外部投票优于内部投票,那么就进行投票变更,使用外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的内部投票发送出去。
8、选票归档
无论是否进行了投票变更,都会将刚刚收到的外部投票放入选票集合recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票。
9、统计投票
完成选票归档后,就可以开始统计投票。统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票。如果是,则终止投票;否则返回步骤4。
需要注意的是,当确认过半服务器认可投票后,仍然会等待一段时间,以判断是否有更优的投票。如果有,用更优投票替代当前统计结果。
10、更新服务器状态
统计投票后,如果确定可以终止投票,那么就开始更新服务器状态。服务器会首先判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,如果是自己的话,那么就会将自己的服务器状态更新为LEADING。如果自己不是被选举产生的Leader的话,那么就会根据具体情况来确定自己是FOLLOWING或是OBSERVING。
上述过程中,4~9是放在while循环中的,会经过多次循环,直到Leader选举产生。