ZooKeeper的由来

深入分析ZooKeeper的实现原理-LMLPHP

主要是解决分布式环境下服务协调问题而产生的,实现ZooKeeper需要做什么?

  • 防止单点故障
    • 所以这个中间件需要考虑到集群,而且这个集群还需要分摊客户端的请求流量
  • 集群存在数据同步和leader节点
  • 如何选举leader节点,leader挂了后的数据恢复
    • ZooKeeper基于paxos协议衍生出了ZAB协议
  • leader节点如何保持和别的节点的数据一致性,而且是强一致
    • 分布式事务:2PC、3PC

2PC提交

  • 分布式事务为了保持事务的ACID,需要引入协调者(TM)、被调度这(AP)
  • 所有事务分两个阶段提交,所以叫做二阶段提交
    1. 投票阶段(全部yes,才能提交,有一个no就需要全不会滚)
    2. 执行事务阶段

深入分析ZooKeeper的实现原理-LMLPHP

深入分析ZooKeeper的实现原理-LMLPHP

ZooKeeper采取的是类2PC的策略

  • 客户端随机连入ZooKeeper集群,
    • 如果是读请求,直接读取;
    • 如果是写请求,转发给leader提交事务,
    • leader广播事务,只需要超过一半的节点回复yes 就可提交

深入分析ZooKeeper的实现原理-LMLPHP

  • leader角色两个任务:
    • 事务请求的唯一调度者和处理者,保证集群事务处理的顺序性
    • 集群内部各个服务器的调度者
  • Follower 角色任务
    • 处理客户端非事务请求,转发事务请求给leader
    • 参与leader发起的事务提交投票,超过半数统一leader才能commit
  • observer角色任务
    • ZooKeeper3.3引进的
    • 观察ZooKeeper集群最新变化状态,并且同步套自己服务器上
    • 不参与一切投票:包括事务投票和leader选举
    • 仅仅提供非事务服务,在不影响集群事务处理能力的前提下,提升非事务处理能力
  • 集群组成
    • 集群存在几台机器大家都知道,互相知道彼此
    • ZooKeeper采用2n+1 server提供服务,最多允许n台挂掉
      • 举例:5台机器,最多允许挂2台;6台机器也是最多允许挂2台
      • 因此,5台6台容灾性差不多,6台反而增加了网络负担
      • 采取2n+1的原因

ZAB协议

  • 支持崩溃恢复原子广播协议
  • 集群启动leader网络中断(与半数以上Follower断开连接)时,ZAB协议进入崩溃恢复模式
  • 当集群选举出新的leader,并且过半数节点和leader保持数据同步的时候,进入消息广播模式(退出崩溃恢复模式)
  • leader节点正常,新节点加入,或Follower故障恢复后,进入数据恢复模式进行数据同步
    • 同步完成后才可对外提供非事务服务

消息广播是一个简化版本的2PC:

  • leader接收到消息请求后,赋予消息全剧唯一自增id(zxid)
    • 通过zxid 来实现因果有序
  • leader为每个Follower准备了一个FIFO的队列(tcp协议实现,以实现全局有序这一特点)
    • 将带有zxid的消息作为提案(proposal)分发给每一个Follower
    • Follower接收到proposal后,写入磁盘,再向leader回复ack
    • leader 收到半数以上ACK后,会向Follower发送commit,并且本地提交
    • 当Follower收到commit以后会提交该消息
  • 深入分析ZooKeeper的实现原理-LMLPHP

崩溃恢复模式需要解决的问题

  • 已经处理的消息不能丢失
  • 被丢弃的消息不能再次出现
    • leader生成proposal后就挂掉,其他Follower没有收到,该proposal被新leader跳过,
    • 老的leader恢复后(重新接入后变成Follower),保留了该proposal,不应该发出去,应该删掉

崩溃恢复模式需要满足:

  • leader选举算法,新选出来的leader具有所有proposal最大zxid
    • 就能保证其具有所有已提交的事务
  • 生成的zxid 比之前的都大
    • zxid 是64位,高32位是epoch编号,低32位是消息计数器
    • 新选举leader会epoch+1,低32位重置为0
    • 当老的leader重新接入,新的leader会让其将所有拥有旧的epoch,未被commit的老的proposal清除

leader选举

  • 服务器启动时的leader选举
    • 每个server发出一个投票给其他机器
      • 都是先将自己作为leader进行投票
      • 投票内容带上自己的myid、zxid
    • 接收各个服务器投票
      • 检查是否是本轮投票,是否来自looking状态服务器
    • 处理投票
      • pk本人投票和接收到的其他投票
      • 优先比较zxid,取大的;再比较myid,取大的
      • 将pk出的新的投票信息发出去
    • 统计投票
      • 每次投票后,都会统计投票结果,判断是否有过半数相同的投票信息
    • 改变服务器状态
      • 如果是Follower 状态改为following
      • leader 改为leading
  • 运行过程中的leader选举
    • 变更状态
      • 非observer 都改状态为looking,开启leader选举
    • 每个server发出投票
    • 处理投票
    • 统计投票
    • 修改状态

leader选举源码分析

  • 在3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法
  • 当一台机器进入Leader选举时,当前集群可能会处于以下两种状态:
    • 集群中已经存在Leader

    • 集群中不存在Leader

  • 对于集群中已经存在Leader而言,此种情况一般都是某台机器启动得较晚,在其启动之前,集群已经在正常工作,对这种情况,该机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可。

深入分析ZooKeeper的实现原理-LMLPHP

Leader选举实现细节

  • 服务器具有四种状态,分别是LOOKING、FOLLOWING、LEADING、OBSERVING。
    • LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
    • FOLLOWING:跟随者状态。表明当前服务器角色是Follower。
    • LEADING:领导者状态。表明当前服务器角色是Leader。
    • OBSERVING:观察者状态。表明当前服务器角色是Observer。
  • 投票数据结构
    • 每个投票中包含了两个最基本的信息,所推举服务器的SIDZXID,投票(Vote)在Zookeeper中包含字段如下:
      • id:被推举的Leader的SID。

      • zxid:被推举的Leader事务ID。

      • electionEpoch:逻辑时钟,用来判断多个投票是否在同一轮选举周期中

        • 该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作。

      • peerEpoch:被推举的Leader的epoch。

      • state:当前服务器的状态。

  • QuorumCnxManager:网络I/O

    • 每台服务器在启动的过程中,会启动一个QuorumPeerManager,

    • 负责各台服务器之间的底层Leader选举过程中的网络通信

    •  (1)消息队列

      • QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合

      • 如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰。

        • · recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息。

          · queueSendMap:消息发送队列,用于保存那些待发送的消息,按照SID进行分组。

          · senderWorkerMap:发送器集合,每个SenderWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,也按照SID进行分组。

          · lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息。

    • (2) 建立连接

      • 为了能够相互投票,Zookeeper集群中的所有机器都需要两两建立起网络连接。

      • QuorumCnxManager在启动时会创建一个ServerSocket来监听Leader选举的通信端口(默认为3888)。

      • 为了避免两台机器之间重复地创建TCP连接,Zookeeper只允许SID大的服务器主动和其他机器建立连接,否则断开连接。

      • 一旦连接建立,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动。

    • (3) 消息接收与发送

      • 消息接收:由消息接收器RecvWorker负责,由于Zookeeper为每个远程服务器都分配一个单独的RecvWorker,

        • 因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中。

      • 消息发送:由于Zookeeper为每个远程服务器都分配一个单独的SendWorker,

        • 因此,每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息发送即可,同时将这个消息放入lastMessageSent中。

  • FastLeaderElection:选举算法核心:

    •   · 外部投票:特指其他服务器发来的投票。

        · 内部投票:服务器自身当前的投票。

        · 选举轮次:Zookeeper服务器Leader选举的轮次,即logicalclock。

        · PK:对内部投票和外部投票进行对比来确定是否需要变更内部投票。

    • (1)选票管理

      •       · sendqueue:选票发送队列,用于保存待发送的选票。

          · recvqueue:选票接收队列,用于保存接收到的外部投票。

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

          · WorkerSender:选票发送器,不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。

    • (2) 算法核心

      • FastLeaderElection模块是如何与底层网络I/O进行交互

      • 深入分析ZooKeeper的实现原理-LMLPHP

      • Leader选举的基本流程如下:

        • 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。

      • 以上10个步骤就是FastLeaderElection的核心,其中步骤4-9会经过几轮循环,直到有Leader选举产生

11-21 08:39