分布式注册中心框架分析与介绍!详细解析Zookeeper中的集群选举算法

这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

基本概念

  • Zookeeper使用集群形态部署来保证高可用. 在集群中,只要集群中大部分机器是可用的,那么整个Zookeeper服务就是可用的
  • Zookeeper集群是一个基于主从复制的高可用集群.通常情况下,至少要保证有3台服务器才能构成一个Zookeeper集群
  • Zookeeper集群模式 : 主备Master-Slave模式
    • Master服务器: 主服务器提供写服务
    • Slave服务器: 通过异步复制的方式获取主服务器上的最新数据提供读服务

在这里插入图片描述

  • 每一个Server代表一个安装Zookeeper服务的服务器
  • 组成Zookeeper服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信
  • Zookeeper集群之间通过ZAB协议 (Zookeeper Atomic Broadcast) 保持数据的一致性

Zookeeper集群角色

  • Zookeeper中没有传统的Master-Slave的概念,而是包含以下三个角色:
    • Leader : 为客户端提供读和写服务,负责投票的发起和决议,更新系统状态
      • 一个Zookeeper集群中同一时间只会有一个实际工作的Leader, 负责发起并维护和各个Follower以及Observer间的心跳
      • 所有的写操作必须通过Leader完成,再由Leader将写操作广播给其余服务器
    • Follower : 为客户端提供读服务,如果是写服务则转发给Leader. 在选举过程中参与投票
      • 一个Zookeeper集群中可能同时存在多个Follower, 会响应Leader的心跳
      • Follower可以直接处理并返回客户端Client的读请求,同时会将客户端的写请求转发给Leader处理,并且负责在Leader处理请求时对请求进行投票
    • Observer :
      • 为客户端提供读服务,如果是写服务则转发给Leader.Follower的功能类似,只是没有投票权
      • 不参与选举过程中的投票,也不参与过半写成功策略
      • 在不影响写性能的情况下提升集群的读性能
      • Observer角色是Zookeeper3.3之后新增的角色

Leader

  • Leader的主要功能:
    • 恢复数据
    • 维持和Follower的心跳,接收Follower请求并判断Follower的请求消息类型
    • 根据Follower不同的消息类型进行不同的处理
      • Follower的消息类型:
        • PING消息 : Follower的心跳消息
        • REQUEST消息 : Follower发送的提议消息,包括写请求以及同步请求
        • ACK消息 : Follower对提议的回复消息,超过半数的Follower通过,就会commit这个提议
        • REVALIDATE消息 : 用来延长Session有效时间

Follower

  • Follower的主要功能:
    • Leader发送请求消息.包括PING消息 ,REQUEST消息 ,ACK消息 ,REVALIDATE消息
    • 接收Leader消息并进行处理
    • 接收客户端Client的请求.如果是写请求,就转发给Leader处理,并进行投票
    • 返回客户端Client的请求的处理结果
  • Follower接收并循环处理以下来自Leader的消息:
    • PING消息 : 心跳消息
    • PROPOSAL消息 : Leader发起的提议消息,要求Follower进行投票
    • COMMIT消息 : 服务端最新一次提议的消息
    • UPTODATE消息 : 同步完成消息
    • REVALIDATE消息 : 根据LeaderREVALIDATE结果,决定关闭待revalidateSession还是允许这个待revalidateSession接收消息
    • SYNC消息 : 返回SYNC结果给客户端Client, 这个消息最初是由客户端Client发起,用来强制获取最新的更新

Zookeeper集群选举

在这里插入图片描述

  • Zookeeper集群的选举过程:Zookeeper中的Leader服务器出现网络中断,崩溃退出以及重启等异常情况时 ,Zookeeper进入崩溃恢复模式,恢复模式需要重新选举出一个新的Leader, 让所有服务器Server恢复到一个正确的状态,此时就会进入Leader选举过程,这个过程会产生新的Leader服务器
    • 选举阶段Leader Election:
      • 节点在一开始都是处于选举阶段
      • 只要有一个节点得到超过半数节点的票数,就成为准Leader
    • 发现阶段Discovery:
      • Follower和准Leader之间进行通信,同步Follower最近接收的事务提议
    • 同步阶段Synchronization:
      • 利用准Leader在发现阶段获得的最新提议历史,同步集群中所有的副本
      • 同步完成之后准Leader成为真正的Leader
    • 广播阶段Broadcast:
      • Zookeeper集群可以正式对外提供事务服务,并且Leader可以进行消息广播
      • 如果有新的节点加入,需要对新节点进行同步

Zookeeper集群选举算法

  • Zookeeper的集群选举算法有以下几种选项:
    • 0 - 基于UDPLeaderElection
    • 1 - 基于UDPFastLeaderElection
    • 2 - 基于UDP和认证的FastLeaderElection
    • 3 - 基于TCPFastLeaderElection
  • Zookeeper默认使用的集群选举算法是3 - 基于TCP的FastLeaderElection算法,其余几个算法已经弃用
  • FastLeaderElection:
    • FasterLeaderElection选举算法是标准的Fast Paxos算法实现,解决了LeaderElection算法中的收敛速度慢的问题
    • Paxos是一个consensus共识算法,用于解决分布式共识问题.目的是在一个分布式系统中如何就某一个值proposal达成一致

Basic Paxos算法

  • 选举线程由当前Server发起选举的线程担任.主要功能是对投票结果进行统计,并且推选出Server
  • 选举线程首先向包括自己在内的所有Server发起一次询问
  • 选举线程收到回复后,验证是否为自身发起的询问验证logicClock, 然后看最后提交的事务ID是否一致,也就是最大的zxid是否一致,接着获取对方的myid, 最后获取对方提议的Leader的相关信息 (myid,zxid), 并且将这些信息存储到此次选举的投票记录中
  • 线程收到所有服务器Server的回复后,计算出zxid最大的服务器Server, 并且将这个服务器Server相关信息设置成为下一次要投票的服务器Server
  • 线程将当前zxid最大的服务器Server设置为服务器Server要推荐的Leader, 如果此时获胜的服务器Server获得超过半数的选票,就将获胜的服务器Server设置为Leader, 根据获胜服务器Server的信息设置自身状态.否则,就一直持续这个过程,直到领导者Leader选举成功

Zookeeper集群选举选票数据结构

  • 每个服务器在进行领导者Leader选举时,会发送以下数据结构的信息作为选票:
    • logicClock: 表示服务器发起的第多少轮的投票.每个服务器都会维护这样一个值,值是一个自增的整数
    • state: 表示当前服务器的状态
    • self_id: 表示当前服务器的myid
    • self_zxid: 表示当前服务器上保存的数据的最大zxid
    • vote_id: 表示推举的服务器的myid
    • vote_zxid: 表示推举的服务器上保存的数据的最大zxid

Zookeeper集群选举投票流程

  • 自增选举轮次:
    • Zookeeper中规定所有有效的投票都必须在同一轮次中
    • 每个服务器在开始新的一轮投票时,都会首先对自身维护的logicClock的值进行自增操作
  • 初始化选票:
    • 每个服务器在广播自身的选票前,会将记录了接收到的选票的投票箱清空
    • 示例:
      • 服务器B投票给服务器C, 服务器C投票给服务器A, 服务器A自投.此时服务器A的投票箱为 (A,C),(C,A),(A,A)
      • 投票箱中只会记录每一位投票者的最后一票,如果投票者更新了自己的选票,其余服务器接收到新的选票后会在自身的投票箱中更新这个投票者服务器的选票
  • 发送初始化选票:
    • 每个服务器最开始都是通过广播将选票投给自身
  • 接收外部选票:
    • 服务器尝试从其余服务器获取投票并记入自身的投票箱
    • 如果无法获取任何外部选票,会确认自身是否与集群中的其余服务器保持有效连接:
      • 如果保持着有效连接,就再次发送自身的投票
      • 如果没有保持有效连接,就马上再次建立连接
  • 判断选举轮次:
    • 收到外部投票后,首先会根据投票信息中包含的表示投票轮次数logicClock的值进行不同的处理:
      • 外部投票的logicClock的值大于自身的logicClock的值:
        • 表示当前服务器的选举轮次落后于其余服务器的选举轮次
        • 立即清空自身的投票箱,并将自身的logicClock的值更新为接收到的外部投票的logicClock的值.然后对比自身之前的投票与接收到的外部投票来确定是否需要变更自身的投票,最后将自身的投票广播出去
      • 外部投票的logicClock的值小于自身的logicClock的值:
        • 表示其余服务器的选举轮次落后于当前服务器的选举轮次
        • 当前服务器直接忽略接收到的外部投票,继续处理其余投票
      • 外部投票的logicClock的值等于自身的logicClock的值:
        • 表示其余服务器的选举轮次就是当前服务器的选举轮次
        • 此时进行选票PK
  • 选票PK: 选票PK是基于self_id,self_zxidvote_id,vote_zxid的对比
    • 如果两者的logicClock一致,就对比两者的vote_zxid, 比较后选择较大的vote_zxid
      • 如果外部投票的vote_zxid比较大,就将自身的票中的vote_zxidvote_id更新为接收到的外部投票的vote_zxidvote_id并且广播出去
      • 将接收到的选票以及自身更新后的选票放入自身的投票箱
      • 如果投票箱中已经存在相同的选票,也就是self_idself_zxid的值相等,就直接覆盖
    • 如果两者的vote_zxid一致,就对比两者的vote_id, 比较后选择较大的vote_id
      • 如果外部投票的vote_id比较大,就将自身的票中vote_id更新为接收到的外部投票的vote_id并且广播出去
      • 将接收到的选票以及自身更新后的选票放入自身的投票箱
  • 统计选票:
    • 如果已经确定有超过半数的服务器认可自身的投票,就终止投票
    • 如果没有超过半数就继续接收其余服务器的投票
  • 更新服务器状态:
    • 投票终止后,服务器更新自身的状态
    • 如果有超过半数的票投给自身,就将自身的服务器状态更新为LEADING状态
    • 如果没有超过半数的票投给自身,就将自身的服务器状态更新为FOLLOWING状态

Zookeeper集群启动领导Leader选举

  • 集群中三个服务器1,2,3. 当集群启动时,所有服务器的logicClock的值都为1,zxid的值都为0
  • 初始投票给自身:
    • 各个服务器初始化后,都给自身投票,并将自身的一票存入自身的投票箱
      • (1,1,0):
        • 第一个数字1代表投出选票的服务器的投票轮次logicClock
        • 第二个数字1代表被推荐的服务器的myid
        • 第三个数字0代表被推荐的服务器的最大zxid
          • 在这个初始投票步骤中,所以第一个数字为1
          • 所有的选票都投给自身,所以第二位的数字myid就是服务器自身的myid, 第三位的数字zxid就是服务器自身的zxid
          • 此时服务器自身的投票箱中只有自身投给自身的一票
  • 更新选票:
    • 服务器接收到外部投票后,进行选票PK, 根据情况更新自身的选票结果并广播出去,然后将合适的选票结果存储到自身的投票箱
      • 服务器1接收到服务器2的选票 (1,2,0) 和服务器3的选票 (1,3,0) 后,因为所有的logicClock值相等,所有的zxid的值相等,所以选择myid最大的服务器的外部投票更新自身的选票结果为**(1,3,0),** 并且将自身的投票箱全部清空,再将服务器3的选票和自身的选票存入到自身的投票箱,最后将自身更新后的选票结果广播出去.这时服务器1的投票箱中的选票为 (1,3),(3,3)
      • 服务器2接收到服务器3的选票后,和服务器1类似,也会将自身的选票更新为 (1,3,0) 并存入到自身的投票箱,最后将自身更新后的选票结果广播出去.这时服务器2的投票箱中的选票为 (2,3),(3,3)
      • 服务器3根据上述规则,不需要更新投票,自身的投票箱中的选票依旧是 (3,3). 服务器1和服务器2更新后的选票更新出去,因为三个服务器最新选票都相同,最后三个服务器的投票箱中都包含三张投票给服务器3的选票
  • 根据选票确定角色:
    • 根据选票结果,确定各个服务器的角色并更新服务器的状态
      • 根据上述选票结果,三个服务器一致认为服务器3为领导者Leader
      • 这时,服务器3更改自身的状态为LEADING状态,服务器1和服务器2更改自身的状态为FOLLOWING状态
      • 最后领导者Leader发起并维护和跟随者Follower间的心跳

Follower重启

  • Follower重启时,发生网络分区脑裂后找不到领导者Leader时,就会进入LOOKING状态并发起新的一轮投票
    • 服务器3接收到服务器1的投票后,将自身的LEADING状态以及选票返回给服务器1
    • 服务器2接收到服务器1的投票后,将自身的FOLLOWING状态以及选票返回给服务器1
    • 这时服务器1知道服务器3Leader, 并且通过服务器2和服务器3的选票可以确定服务器3得到超过半数的选票.服务器1将自身的状态更改为FOLLOWING状态

Leader重启

  • Follower发起重新投票:
    • 领导者Leader发生宕机后,跟随者Follower发现领导者Leader不工作,就会进入LOOKING状态并发起新的一轮投票,都将票投给自身
  • 选票PK和广播:
    • 服务器1和服务器2根据外部投票情况确定是否要更新自身的选票,包含以下两种情况:
      • 服务器1和服务器2的最大的zxid相同:
        • 比如在领导者Leader服务器3宕机之前,服务器1和服务器2完全后服务器3同步
        • 这时选票的更新主要取决于服务器的id, 也就是myid的大小,选票投给myid的值大的服务器
      • 服务器1和服务器2的最大的zxid不同:
        • 领导者Leader主导的写操作,只需要过半服务器确认就生效,不需要所有服务器确认
        • 这样在服务器1和服务器2中可能一个和Leader同步,也就是zxid相同,另一个和Leader不同步,也就是zxid要更小
        • 这时选票的更新取决于zxid较大的服务器
  • 选出新的领导者Leader:
    • 经过选票PK的步骤后,可以选举出新的领导者Leader
      • 服务器1和服务器2都将选票都给服务器1, 这样服务器2就成为跟随者Follower, 服务器1成为新的领导者Leader并且维护和服务器2的心跳
  • 旧领导者Leader恢复后转为FOLLOWING状态:
    • 重启成功后的旧领导者LeaderLOOKING状态转变为FOLLOWING状态
      • 旧的领导者Leader服务器3恢复启动后,进入LOOKING状态并发起新的一轮领导选举,将选票投给自身
      • 这时服务器1会将自身LEADING状态和选票结果 (3,1,zxid) 返回给服务器3. 服务器2会将自身FOLLOWING状态和选票结果 (3,1,zxid) 返回给服务器3
      • 服务器3得到消息确定领导者Leader为服务器1, 并且根据选票结果确定服务器1获得超过半数的服务器选票,自身进入FOLLOWING状态

Zookeeper集群选举总结

  • Paxos算法:
    • Fast Paxos算法 : 选举开始时,各个服务器Server都提议自身为领导者Leader
    • Basic Paxos算法: 询问计算合适的服务器Server作为领导者Leader
  • Zookeeper集群选举流程:
    • 要使Leader获得多数服务器Server的支持,那么服务器Server总数必须是奇数 2 n + 1 , 2n+1, 并且存活的服务器Server的数目不能少于 n + 1 n+1
    • 每一个服务器Server重启后,都会重复Zookeeper集群选举流程.在恢复模式下,如果是刚刚从崩溃状态恢复的或者刚刚启动的服务器Server会从磁盘快照中恢复数据和会话信息 .Zookeeper会记录事务日志并且定期快照,便于在恢复时进行状态恢复

非公平Leader选举

  • 非公平模式实现简单,每一轮选举方法都完全一样
  • 在竞争参与方数目较少的情况下,效率很高:
    • 每个Follower节点通过watch感知到Leader节点被删除的时间并不完全一样
    • 只要有一个Follower节点得到节点Leader节点删除的通知就发起竞选,就能够保证有新的领导者Leader选举出来
  • 非公平模式造成Zookeeper集群负载大,扩展性差:
    • 如果大量的客户端都参与竞选,那么就会同时存在大量的写请求发送给Zookeeper
    • Zookeeper存在单点写的问题,写操作的性能低
    • 领导者Leader放弃领导权时 ,Zookeeper需要同时通知大量的跟随者Follower, 负载大

选主过程

  • 比如有3Zookeeper客户端Client1,Client2,Client3同时竞争领导者Leader:
    • 3个客户端同时向Zookeeper集群注册Ephemeral临时且Non-sequence无序类型的节点,路径为 /zkroot/leader
    • 由于是无序Non-sequence节点.这3个客户端只有一个可以创建成功,其余节点都创建失败
    • 这时客户端Client1创建成功,成为领导者Leader, 其余客户端Client2和客户端Client3成为跟随者Follower

放弃领导权

  • 领导者Leader需要放弃领导权时,直接删除 /zkroot/leader节点即可
  • 领导者Leader进程意外宕机,那么和Zookeeper之间的Session也会结束.因为节点是Ephemeral临时类型的节点,所以会自动被删除
    • 这时 /zkroot/leader就不存在了,对于其余客户端来说,说明旧领导者Leader放弃了领导权

感知领导权的放弃

  • 节点创建领导者Leader失败后,成为跟随者Follower, 还会向 /zkroot/leader注册一个watch. 如果Leader放弃领导权,也就是这个领导者Leader节点删除时,所有的跟随者Follower都会收到通知

重新选举

  • 感知到旧领导者Leader放弃领导权后,所有跟随者Follower可以重新发起新一轮的领导选举:
    • 新一轮的领导者Leader选举方法和上一轮的领导者Leader的选举方法完全一样,都是发起节点创建请求,创建成功就是领导者Leader, 否则就是说跟随者Follower, 并且Followerwatch这个Leader节点
    • 新一轮领导者Leader选举结果无法预测,和上一轮选举中的顺序无关.所以这个方案称作非公平选举

公平Leader选举

  • 公平模式实现相对复杂
  • 扩展性好:
    • 每个客户端都只watch一个节点并且每次节点删除时只需要通知一个客户端
  • 旧的领导者Leader放弃领导权时,其余客户端根据竞选的先后顺序,也就是节点的序号依次成为新的领导者Leader. 所以这个方案称作公平选举
  • 公平模式的延迟相对于非公平模式要高,因为公平模式中必须等待特定的节点得到通知后才可以开始选举出新的领导者Leader

选主过程

  • 公平领导者选举中,各个客户端都会创建 /zkroot/leader节点,并且类型为Ephemeral临时且Sequence有序类型的节点
    • 由于是Sequence有序类型的节点,所以三个客户端都能创建成功,只是节点的序号不一样
    • 这时,客户端都会判断自身创建成功的节点的序号是不是当前最小的序号
      • 如果节点序号是最小的,那么就成为领导者Leader节点
      • 如果节点序号不是最小的,那么就成为跟随者Follower节点
  • 比如客户端Client1创建的节点序号为1, 客户端Client2创建的节点序号为2, 客户端Client3创建的节点序号为3:
    • 因为最小的序号为1, 并且序号为1的节点由客户端Client1创建,所以客户端Client1成为领导者Leader. 其余客户端Client2,Client3成为跟随者Follower

放弃领导权

  • 公平领导者选举中,每个跟随者Followerwatch节点序号刚好比自身节点序号小的节点:
    • 比如在1,2,3三个序号的节点中,客户端Client2负责watch客户端Client1的节点 /zkroot/leader1, 客户端Client3负责watch客户端Client2的节点 /zkroot/leader2
    • 注意: 实际项目中,序号应该是10位数字

感知领导权的放弃

  • 如果领导者Leader宕机 ,/zkroot/leader1删除,这时只有watch领导者Leader节点客户端Client1的客户端Client2可以得到通知.客户端Client3因为watch的是客户端Client2的节点 /zkroot/leader2, 所以得不到通知

重新选举

  • 客户端Client2得到客户端Client1节点 /zkroot/leader1删除的通知后,不会立即成为新的Follower, 而是首先判断自身的节点序号是不是当前最小的节点序号.只有确定自身的序号是当前最小的序号,客户端Client2才会成为新的领导者Leader
  • 注意:
    • 如果在旧的领导者Leader节点客户端Client1放弃领导权之前,负责watch领导者Leader节点客户端Client1的客户端Client2发生宕机,客户端Client3会得到通知
    • 这时客户端Client3不会立即成为新的领导者Leader, 而是首先判断自身的节点序号是不是当前最小的节点序号
    • 由于客户端Client1创建的节点 /zkroot/leader1没有删除,客户端Client3不会成为新的领导者Leader, 并且会向客户端Client2前面的节点序号,也就是节点序号为1的客户端Client1创建watch监视

Zookeeper集群服务器状态

  • LOOKING: 寻找Leader状态. 当前Zookeeper服务器Server不知道Leader
  • LEADING: 领导者Leader状态. 对应的节点为Leader, 也就是当前服务器角色是Leader
  • FOLLOWING: 跟随者Follower状态. 对应的节点为Follower.Leader已经选举出来,当前服务器和Leader同步
  • OBSERVING: 观察者Observer状态. 对应的节点为Observer, 在大多数情况下行为和跟随者Follower完全一致,只是该节点不参与Leader选举和投票,仅仅接收选举和投票的结果
  • 过半写成功策略:
    • Leader节点接收到写请求后,这个Leader会将写请求广播给其余各个集群服务器Server
    • 其余各个集群服务器Server会将这个写请求加入待写队列,并且向Leader节点发送成功消息
    • Leader节点接收到一半以上的集群服务器Server成功消息后,就可以执行这个写操作
    • Leader节点会向其余各个几区服务器Server发送提交消息,其余各个集群服务器Server接收到消息后开始写请求操作
  • 只有Leader节点可以进行写操作 ,FollowerObserver只提供数据的读操作,如果接收到写请求时,会将写请求转发给Leader节点处理
  • Observer节点不参与选举和投票过程,也不参与写操作的过半写成功策略

Zookeeper集群服务器数目

  • Zookeeper集群中只要有半数以上的节点存活 ,Zookeeper集群就可以正常服务
  • Zookeeper集群中服务器的数目推荐为奇数台
    • Zookeeper集群在宕机部分Zookeeper服务器之后,只有保证剩余的Zookeeper服务器的数量大于宕机的数量,整个Zookeeper集群才会依然可用
    • 也就是说,假如集群中Zookeeper服务器的数量有 n n 台,那么要求剩与的Zookeeper服务器的数量必须大于 n 2 \frac{n}{2}
    • 所以 2 n 1 2n-1 2 n 2n 的容忍度是一样的,都是 n 1 n-1
    • 因此Zookeeper服务器的数量保证奇数量是和偶数量的效果一样的,所以不需要增加一个没有必要的Zookeeper服务器

猜你喜欢

转载自juejin.im/post/7060838186473226277