1.场景
有一个向外提供的服务,服务必须7*24小时提供服务,不能有单点故障,所以采用集群的方式,采用master、slave的结构.一台主机多台备机,主机向外提供服务,备机负责监听主机的状态,一旦主机宕机,备机要迅速接代主机继续向外提供服务,从备机选择一台作为主机,这就是master重新选举/leader重新选举;
2.Zookeeper集群中的角色
如图:
说明:
①.ZooKeeper集群的所有机器通过一个 Leader 选举过程来选定一台被称为[Leader]的机器,Leader服务器为客户端提供读和写服务;
②.Follower和 Observer 都能提供读服务,不能提供写服务.两者唯一的区别在于,Observer机器不参与 Leader 选举过程,也不参与写操作的[过半写成功]策略,因此Observer可以在不影响写性能的情况下提升集群的读性能;
3.Zookeeper工作原理
1>.Zookeeper的核心是原子广播机制,这个机制保证了各个Server之间的同步.实现这个机制的协议叫做Zab协议
.Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步);
2>.当服务启动或者在领导者崩溃后,Zab协议就进入了恢复模式,当Leader被选举出来,且大多数Server完成了和Leader的状态同步以后,恢复模式就结束了.状态同步保证了Leader和Server具有相同的系统状态;
3>.一旦Leader已经和多数的Follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态.这时候当一个Server加入Zookeeper服务中,它会在恢复模式下启动,发现Leader,并和Leader进行状态同步.待到同步结束,它也参与消息广播.Zookeeper服务一直维持在Broadcast状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持;
4>.广播模式需要保证Proposal被按顺序处理,因此ZK采用了递增的事务id号(ZXID)来保证所有的提议(Proposal)都在被提出的时候加上了ZXID.实现中ZXID是一个64位的数字,它的高32位是Epoch用来标识Leader关系是否改变,每次一个Leader被选出来,它都会有一个新的Epoch,标识当前属于那个Leader的统治时期.而低32位用于递增计数;
5>.当Leader崩溃或者Leader失去大多数的Follower,这时候ZK进入恢复模式,恢复模式需要重新选举出一个新的Leader,让所有的Server都恢复到一个正确的状态
6>.Leader选举
①.每个Server启动以后都询问其它的Server它要投票给谁;
②.对于其他Server的询问,Server每次根据自己的状态都回复自己推荐的Leader的id和上一次处理事务的zxid(系统启动时每个server都会推荐自己);
③.收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
④.计算这过程中获得票数最多的的Server为获胜者,如果获胜者的票数超过半数,则该Server被选为Leader.否则,继续这个过程,直到Leader被选举出来;
⑤.Leader被选举出来之后就会开始等待Server连接;
⑥.Follower连接Leader,将最大的zxid发送给Leader;
⑦.Leader根据Follower的zxid确定同步点,进行状态同步;
⑧.完成同步后通知Follower已经成为uptodate状态;
⑨.Follower收到uptodate消息后,又可以重新接受client的请求进行服务了;
7>.每个Zookeeper服务在工作过程中有四种状态:
①.LOOKING:竞选状态.当前Server不知道Leader是谁,正在搜寻;
②.LEADING:领导者状态.表明当前服务器角色是Leader;
③.FOLLOWING:随从状态.表明当前服务器角色是Follower,同步Leader状态,参与投票;
④.OBSERVING:观察状态.表明当前服务器角色是Observer,同步Leader状态,不参与投票;
4.Zookeeper的Leader选举原理分析
4.1.Leader选举的前提条件
①.只有服务器状态在LOOKING(竞选状态)状态才会去执行选举算法;
②.Zookeeper的集群规模至少是2台机器,才可以选举Leader(这里以5台机器集群为例);
③.当集群中只有一台服务器启动是不能选举的,等第二台服务器启动后,两台机器之间可以互相通信,才可以进行Leader选举;
④.服务器运行期间无法和Leader保持连接的时候,也需要进行Leader选举;
4.2.选举机制中的一些基本概念
1>.SID:服务器ID
即myid,表示服务器的编号;编号越大在选择算法中的权重越大;
2>.ZXID:zookeeper事务id
zookeeper状态的每一次改变,都对应着一个递增的Transaction ID,该id称为zxid,他展示了所有的zookeeper的变更顺序.由于zxid的递增性质,如果zxid1小于zxid2,那么zxid1肯定先于zxid2发生
3>.Epoch:逻辑时钟或者叫投票的次数
同一轮投票过程中的逻辑时钟值是相同的.
每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断;如果收到低于当前轮次的投票结果,该投票无效,需更新到当前轮次和当前的投票结果;
4>.Vote:投票
Leader选举,顾名思义必须通过投票来实现.当集群中的机器发现自己无法检测到Leader机器的时候,就会开始尝试进行投票;
5>.Quorum:过半机器数
这是整个Leader选举算法中最重要的一个术语,我们可以把这个术语理解为是一个量词,指的是ZooKeeper集群中过半的机器数,如果集群中总的机器数是n的话,那么可以通过下面这个公式来计算quorum的值:
quorum = (n/2 + 1)
例如如果集群机器总数是3,那么quorum就是2;
6>.state:server选举状态
LOOKING:竞选状态;
FOLLOWING:随从状态,同步Leader状态,参与投票;
OBSERVING:观察状态,同步Leader状态,不参与投票;
LEADING:领导者状态;
4.3.服务器启动时期的Leader选举
zookeeper默认的算法是FastLeaderElection.采用投票数大于半数则胜出的逻辑
如图:
选举过程说明: 假设zookeeper集群有五台机器,都是最新启动的没有历史数据
①.服务器1启动,读取自身的zxid,然后进行投票.首先给自己投票,然后再发出一个投票信息给集群中其他机器
(投票信息包含两个最基本信息:所选举Leader的ServerID,Zxid)
;由于其它机器还没有启动所以服务器1收不到反馈信息,服务器1的状态一直属于Looking状态;
②.服务器2启动,首先给自己投票,然后再发出一个投票信息给集群中其他机器;服务器1收到投票信息后,首先判断该投票的有效性(如检查是否是本轮投票、是否来自LOOKING状态的服务器),然后要处理投票(对每一个投票,服务器都需要将别人的投票和自己的投票进行PK)
投票处理规则如下:
A.优先检查ZXID.ZXID比较大的服务器优先作为Leader;
B.如果ZXID相同,那么就比较所选举的Leader的ServerID.ServerID较大的服务器作为Leader服务器;由于服务器2的所选举Leader的ServerID属性值大,所以服务器2胜出,之后服务器1更新自己的投票为(2,0),然后重新投票;而对于服务器2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可;
每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,
但此时还没有超过半数机器接收到相同的投票信息,所以两个服务器的状态依然是LOOKING;
③.服务器3启动,先给自己投票,然后再发出一个投票给集群中其他机器,此时服务器3的投票为(3,0);服务器1和服务器2收到投票后,首先判断该投票的有效性,然后要处理投票,由于服务器3的myid属性值最大,所以服务器3胜出,之后服务器1和服务器2更新自己的投票信息为(3,0),然后服务器经过统计发现有超过半数机器接收到相同的投票信息,所以服务器3当选Leader,状态变成LEADING;服务器1,2的状态会变成FOLLOWING;
④.服务器4启动,先给自己投票,然后再发出一个投票给集群中其他机器,但是其他服务器都不是LOOKING状态,无法再参与投票,尽管服务器4的myid属性值大,但之前服务器3已经获得了3票,服务器4会遵从半数原则(/少数服从多数),将自己的1票投给服务器3,而服务器4的状态会变成FOLLOWING;
⑤.服务器5启动,先给自己投票,然后再发出一个投票给集群中其他机器,但是其他服务器都不是LOOKING状态,无法再参与投票,尽管服务器5的myid属性值大,但之前服务器3已经获得了4票,服务器5会遵从半数原则(/少数服从多数),将自己的1票投给服务器3,而服务器5的状态会变成FOLLOWING;
⑥.等到所有的服务器都启动完成,服务器3会成为Leader,其他的服务器会变成Follower;
*注意:
- 集群中每个Server启动时要先进行Leader选举,先投自己一票,然后在发出一个投票信息给集群中的其他Server;
- 每个Server在接收到集群中其他机器的投票信息之后,首先判断该投票的有效性(如检查是否是本轮投票、是否来自LOOKING状态的服务器),然后要处理投票(对每一个投票,服务器都需要将别人的投票和自己的投票进行PK);
- 每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息;
- 一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING;
4.4.Zookeeper在运行期的Leader选举
在Zookeeper运行期间,Leader与非Leader服务器各司其职,即便当有非Leader服务器宕机或新加入,此时也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致.假设正在运行的有Server1、Server2、Server3三台服务器,当前Leader是Server2,若某一时刻Leader挂了,此时便开始Leader选举.选举过程如下:
①.Leader宕机后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程;
②.每个Server会发出一个投票.在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,然后各自将投票发送给集群中所有机器;
③.每个Server会接收来自各个服务器的投票.与启动时过程相同;
④.处理投票.与启动时过程相同,此时,Server1将会成为Leader;
⑤.统计投票.与启动时过程相同;
⑥.改变服务器的状态.与启动时过程相同;
4.5.Leader选举过程中的一些细节
1>.每个投票中包含了两个最基本的信息:所推举服务器的SID和ZXID,投票(Vote)在Zookeeper中包含字段如下:
①.id:被推举的Leader的SID;
②.zxid:被推举的Leader事务ID;
③.electionEpoch:逻辑时钟,用来判断多个投票是否在同一轮选举周期中,该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作;
④.peerEpoch:被推举的Leader的epoch;
⑤.state:当前服务器的状态;
2>.选举Leader过程中集群各个节点在接收到其他节点的投票信息时首先要判断本次投票的有效性,具体流程如下:
①.如果服务器B接收到服务器A的数据[服务器A处于选举状态(LOOKING状态)]
A.首先,判断逻辑时钟值:
1>>.
如果发送过来的逻辑时钟Epoch大于目前的逻辑时钟.
首先,更新本逻辑时钟Epoch,同时清空本轮逻辑时钟收集到的来自其他Server的选举数据.然后判断是否需要更新当前自己的选举Leader的ServerID.[判断规则rules judging:使用保存的zxid最大值和Leader的ServerID来进行判断的.先看数据zxid,数据zxid大者胜出;其次再判断Leader 的ServerID,Leader的ServerID大者胜出;]
然后再将自身最新的选举结果(也就是上面提到的几种数据(被推举Leader的ServerID,Zxid,Epoch)广播给其他Server);
2>>.如果发送过来的逻辑时钟Epoch小于目前的逻辑时钟.
说明对方Server在一个相对较早的Epoch中,这里只需要将本机的几种数据(被推举Leader的ServerID,Zxid,Epoch)发送过去就行;
3>>.如果发送过来的逻辑时钟Epoch等于目前的逻辑时钟.
再根据上述判断规则rules judging来选举Leader,然后再将自身最新的选举结果(也就是上面提到的几种数据(被推举Leader的ServerID,Zxid,Epoch)广播给其他Server);B.其次,判断服务器是不是已经收集到了所有服务器的选举状态:若是,根据选举结果设置自己的角色(FOLLOWING还是LEADER),退出选举过程就是了;
C.最后,若没有收到没有收集到所有服务器的选举状态:也可以判断一下根据以上过程之后最新的选举Leader是不是得到了超过半数以上服务器的支持,如果是,那么尝试在200ms内接收一下数据,如果没有新的数据到来,说明大家都已经默认了这个结果,同样也设置角色退出选举过程;②.如果所接收的服务器A处在其它状态(FOLLOWING或者LEADING)
A.如果逻辑时钟Epoch等于目前的逻辑时钟,将该数据保存到recvset.此时Server已经处于LEADING状态,说明此时这个Server已经投票选出结果.若此时这个接收服务器宣称自己是Leader,那么将判断是不是有半数以上的服务器选举它,如果是,则设置选举状态,然后退出选举过程;
B.否则这是一条与当前逻辑时钟不符合的投票消息,那么说明在另一个选举过程中已经有了选举结果,于是将该选举结果加入到outofelection集合中,再根据outofelection来判断是否可以结束选举,如果可以也是保存逻辑时钟,设置选举状态,退出选举过程;
3>.QuorumCnxManager:网络I/O
每台服务器在启动的过程中,会启动一个QuorumPeerManager,负责各台服务器之间的底层Leader选举过程中的网络通信
①.消息队列
QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合,如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰;1>>.recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息;
2>>.queueSendMap:消息发送队列,用于保存那些待发送的消息,按照SID进行分组;
3>>.senderWorkerMap:发送器集合,每个SenderWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,也按照SID进行分组;
4>>.lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息;
②.为了能够相互投票,Zookeeper集群中的所有机器都需要两两建立起网络连接.QuorumCnxManager在启动时会创建一个ServerSocket来监听Leader选举的通信端口(默认为3888).开启监听后,Zookeeper能够不断地接收到来自其他服务器的创建连接请求,在接收到其他服务器的TCP连接请求时会进行处理.为了避免两台机器之间重复地创建TCP连接,Zookeeper只允许SID大的服务器主动和其他机器建立连接,否则断开连接.在接收到创建连接请求后,服务器通过对比自己和远程服务器的SID值来判断是否接收连接请求,如果当前服务器发现自己的SID更大,那么会断开当前连接,然后自己主动和远程服务器建立连接.一旦连接建立,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动;
③.消息接收与消息发送1>>.消息接收
由消息接收器RecvWorker负责,由于Zookeeper为每个远程服务器都分配一个单独的RecvWorker,因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中;
2>>.消息发送
由于Zookeeper为每个远程服务器都分配一个单独的SendWorker,因此,每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息发送即可,同时将这个消息放入lastMessageSent中.在SendWorker中,一旦Zookeeper发现针对当前服务器的消息发送队列为空,那么此时需要从lastMessageSent中取出一个最近发送过的消息来进行再次发送,这是为了解决接收方在消息接收前或者接收到消息后服务器挂了,导致消息尚未被正确处理.同时,Zookeeper能够保证接收方在处理消息时,会对重复消息进行正确的处理;
4.6.Leader选举算法分析
1>.在ZooKeeper中,提供了三种Leader选举的算法,分别是:
①.1对应:LeaderElection 算法;
②.2对应:AuthFastLeaderElection 算法;
③.3对应:FastLeaderElection 默认的算法;
注意:可以通过zoo.cfg 配置文件中的 electionAlg 属性指定(1-3)!
在3.4.x版本之后的Zookeeper只保留了TCP版本的 FastLeaderElection选举算法.
当一台机器进入Leader选举时当前集群可能会处于以下两种状态:
①.集群中已存在Leader;
②.集群中不存在Leader;
2>.对于集群中已经存在Leader而言,此种情况一般都是某台机器启动得较晚,在其启动之前,集群已经在正常工作,对这种情况,该机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可;
3>.而对于集群中不存在Leader情况下则会相对复杂,其步骤如下:
①.
第一次投票.
无论哪种(启动时/运行期)导致进行Leader选举,集群的所有机器都处于试图选举出一个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);
②.变更投票.
每台机器发出投票后,也会收到其他机器的投票,每台机器会根据一定规则来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票,这个规则也是整个Leader选举算法的核心所在,其中术语描述如下:1>>.vote_sid:接收到的投票中所推举Leader服务器的SID;
2>>.vote_zxid:接收到的投票中所推举Leader服务器的ZXID;
3>>.self_sid:当前服务器自己的SID;
4>>.self_zxid:当前服务器自己的ZXID;
每次对收到的投票的处理,都是对(vote_sid, vote_zxid)和(self_sid, self_zxid)对比的过程:
1>>.规则一: 如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去;
2>>.规则二: 如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更:
3>>. 规则三: 如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去;
4>>.规则四: 如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更;
结合上面规则,给出下面的集群变更过程:
③.
确定Leader.
经过第二轮投票后,集群中的每台机器都会再次接收到其他机器的投票,然后开始统计投票,如果一台机器收到了超过半数的相同投票,那么这个投票对应的SID机器即为Leader.此时Server3将成为Leader;
由上面规则可知,通常那台服务器上的数据越新(ZXID会越大),其成为Leader的可能性越大,也就越能够保证数据的恢复.如果ZXID相同,则SID越大机会越大;
4.7.Leader选举源码分析
4.7.1.QuorumPeer类
主要看这个类,只有LOOKING状态才会去执行选举算法.
每个服务器在启动时都会选择自己做为领导,然后将投票信息发送出去,循环一直到选举出领导为止;
@Override
public void run() {
updateThreadName();
LOG.debug("Starting quorum peer");
try {
jmxQuorumBean = new QuorumBean(this);
MBeanRegistry.getInstance().register(jmxQuorumBean, null);
for(QuorumServer s: getView().values()){
ZKMBeanInfo p;
if (getId() == s.id) {
p = jmxLocalPeerBean = new LocalPeerBean(this);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxLocalPeerBean = null;
}
} else {
RemotePeerBean rBean = new RemotePeerBean(this, s);
try {
MBeanRegistry.getInstance().register(rBean, jmxQuorumBean);
jmxRemotePeerBean.put(s.id, rBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
}
}
}
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxQuorumBean = null;
}
try {
/*
* Main loop 主循环:集群启动的核心代码
* 只要zookeeper处于运行状态,一直循环
*/
while (running) {
switch (getPeerState()) {
case LOOKING: //LOOKING状态
LOG.info