Zookeeper在Dubbo中的作用及Zk集群的选举原理

Zk在Dubbo中的作用

  zk在dubbo中是服务注册与发现的注册中心,dubbo的调用过程是consumer和provider在启动的时候就和注册中心建立一个socket长连接。provider将自己的服务注册到注册中心上,注册中心将可用的提供者列表notify给consumer,consumer会将列表存储到本地缓存,consumer选举出一个要调用的提供者,去远程调用。
这里写图片描述
  如果这时候使用的是单点的zk,当zk宕机了,会发生什么呢?zk宕机后不会影响现有consumer和provider之间的调用,但是新的provider想要注册到注册中心上是不行的,因为zk已经宕机了。因此单点zk一旦宕机就会影响新的提供者的注册,和新的消费者去订阅可用列表。
  因此我们需要将zk搭建成集群的,那么问题又来了,搭建集群时搭建几台呢?建议是搭建奇数台,最少搭建三台,搭建偶数可以吗?是可以的,但是在选举过程中没有太大用处,因为zk的leader也是遵从半数规则,3的半数是2,4的半数也是2,因此在选举中奇数和偶数起到的作用是相同的。
  搭建好了zk集群后,问题又出现了,数据的一致性如何保证呢?先来看一下zk的数据结构,是类似于树形结构的,我们可以看到在com.foo.BarService这个接口下的providers下可以有多个节点,也即多个提供者,假设BarService这个接口对应的提供者有两个,这时有两个客户端同时请求这个接口,一个客户端请求提供者1,另一个客户端请求提供者2,同时执行的业务逻辑是将a的值加1,他们请求到的a的值都是0,这时,最终的结果是什么呢?在分布式情况下保证数据一致性是一个必须思考的问题,解决办法有很多,例如,分布式锁,版本控制等。
这里写图片描述
  zk有自己的机制,在集群的情况下,zk会选举出一个leader负责写操作,剩下的都可以负载读操作,这样就可以将读写分离,保证的单一性,因此就不会出现上述数据不一致的情况了。那么选举的过程是什么呢?请看第二部分,zk的leader选举原理
这里写图片描述

zk的leader选举原理

1. 选举算法

zk提供了四种选择机制,分别为

  • 0 基于UDP的LeaderElection
  • 1 基于UDP的FastLeaderElection
  • 2 基于UDP和认证的FastLeaderElection
  • 3 基于TCP的FastLeaderElection
    在3.4.10版本中,默认值为3,可通过electionAlg配置项设置,另外三种算法被弃用了,并且有计划在之后的版本中将它们彻底删除.

2. FastLeaderElection

2.1 服务器状态

  • LOOKING:不确定Leader状态,该状态下认为集群中没有Leader,会发起Leader选举
  • FOLLOWING:跟随者状态,表明当前服务器角色是Follower,并且知道Leader是谁
  • LEADING:领导者状态,代表当前服务器角色是Leader
  • OBSERVING:观察者状态:表明当前服务器角色是Observer,不参与Leader的选举,也不参与集群写操作时的投票

2.2 选票数据结构

当服务器间进行领导者选举时,会发送如下信息:

  • logicClock每个服务器会维护一个自增的整数,表明这是该服务器发起的第多少轮投票,每次进入新一轮的投票后,都会对该值进行加1操作.
  • state当前服务器的状态
  • self_id:当前服务器的myid
  • self_zxid:当前服务器上所保存的数据的最大zxid,最大的表示复制最完全的
  • vote_id:被推举的服务器的myid
  • vote_zxid:被推举的服务器上所保存的数据的最大zxid,最大的表示复制最完全的

2.3 QuorumCnxManager

每台服务器在启动过程中,会启动一个QuorumCnxManager,负责各台服务器之间的底层Leader选举过程中的网络通信.
(1)消息队列:QuorumCnxManager内部维护一系列的队列

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

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

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

2.4 算法核心

这里写图片描述

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

2.5 Leader选举步骤

  • 第一步:自增选举轮次
    Zookeeper规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的logicClock进行自增操作。

  • 第二步:初始化投票
    每个服务器在广播自己的选票前,会将自己的投票箱清空。

  • 第三步:发送初始化投票
    每个服务器最开始都是通过广播把票投给自己。

  • 第四步:接收外部投票
    服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接。
  • 第五步:判断选举轮次
    收到外部投票后,首先会根据投票信息中所包含的logicClock来进行不同处理
    外部投票的logicClock大于自己的logicClock。说明该服务器的选举轮次落后于其它服务器的选举轮次,立即清空自己的投票箱并将自己的logicClock更新为收到logicClock,然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,最终再次将自己的投票广播出去。
    外部投票的logicClock小于自己的logicClock。当前服务器直接忽略该投票,继续处理下一个投票。
    外部投票的logickClock与自己的相等。当时进行选票PK。
  • 第六步:选票PK
    选票PK是基于(self_id, self_zxid)与(vote_id, vote_zxid)的对比
    ①外部投票的logicClock大于自己的logicClock,则将自己的logicClock及自己的选票的logicClock变更为收到的logicClock
    ②若logicClock一致,则对比二者的vote_zxid,若外部投票的vote_zxid比较大,则将自己的票中的vote_zxid与vote_myid更新为收到的票中的vote_zxid与vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖
    ③若二者vote_zxid一致,则比较二者的vote_myid,若外部投票的vote_myid比较大,则将自己的票中的vote_myid更新为收到的票中的vote_myid并广播出去,另外将收到的票及自己更新后的票放入自己的票箱
  • 第七步:统计选票
    如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票。
  • 第八步:更新服务器状态
    投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为LEADING,否则将自己的状态更新为FOLLOWING

举例分析

3.1服务器第一次启动时的Leader选举

第一次启动时,所有zk中都是没有数据的,因此zxid均为0,而且轮次logicClock都是1,假设本集群中有3台zk要启动,分别是server1,server2,server3

server.1=192.168.25.1:2888:3888
server.2=192.168.25.2:2888:3888
server.3=192.168.25.3:2888:3888

server1自己启动时,因为集群中的只有1台,因此无法单独进行选举,等到server2启动后,可以满足半数条件,因此可以进行选举,两台机器自增logicClock,然后都初始化投票,各自发送自己的投票,server1发送(1,0),server2发送(2,0).
这里写图片描述
下一步是接收外部投票,接收完后,server1此时拥有的投票是(1,0)和(2,0),在同一轮次,进行PK,先比较zxid,即两个都是0,因此继续比较sid,分别是1和2,越大的会被选成leader的机会越大,因此server1需要修改自己的投票是(2,0),同理比较server2,server2不用做任何改变
下一步统计投票:统计发现集群中有两台都接受是(2,0),同时已经达到半数,此时,server2被选举成leader
下一步是改变服务器状态,server2是leader,改变状态为LEADING,其他的为follower的改变状态为FOLLOWING.

3.2 服务器运行过程中的Leader选举

例如现在有三台,server1,server2,server3,其中server2是leader,其他两台是follower。由于某些原因,导致server2宕机,此时选举新的leader
这里写图片描述
第一步:server1和server3变更自己的状态为LOOKING状态,表明当前集群中没有leader,要进行选举
第二步:server1和server3初始化自己的投票
第三步:发送初始化投票,server1发送给server2投票信息是(1,9),server2发送给server1投票信息是(3,8),
第四步:判断是否在同一轮次
第五步:pk:server1将自己的投票和接收到的投票比较,先比较zxid,发现自己的是9>接收到的8,因此自己不需要做变更,server2将自己的投票和接收到的投票比较,先比较zxid,发现自己的是8<接收到的9,因此自己要变更,变更为(1,9)
第六步:判断投票是否过半,发现有两台机器的投票都接受了(1,9),达到了一半的要求(3/2=2)
第七步:此时已经选出的leader为server1,变更状态,server1变为leader,变更状态为LEADING,server3称为了follower,变更状态为FOLLOWING。

总结

存在复制就会有一个问题的产生,在leader还未同步到follower时,leader宕机了,这时如何保证一致性呢?暂时留个小疑问。
根据以上分析得知,所有的写都是leader统一决定并处理的,读操作是集群中的任意一台都可以处理的,因此,适合于读多写少的场景。

推荐博文

因为自己学识有限,在不断的查阅博客中借鉴别人的博客才有了这篇博客,仅以此记录以下。

猜你喜欢

转载自blog.csdn.net/zh15732621679/article/details/80723358
今日推荐