ZAB 协议详解(Zookeeper Atomic Broadcast)

重点了解:两种基本模式三种角色四种状态选主的四个阶段四种同步策略

区别了解:各协议的区别

ZAB 协议是为 分布式协调服务 Zookeeper 专门设计的一种 支持崩溃恢复的 原子广播协议,实现分布式数据一致性
采用了 主备模式 的系统架构来保持集群中各副本之间数据的一致性
整体上(读操作+写操作)实现了顺序一致性,但写操作实现了线性一致性

所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器称为 Leader 服务器
即:所有客户端的请求都由 Leader 服务器接收
然后由 Leader 同步到其他节点,称为 Follower

Leader 服务器将客户端事务请求转化成一个事务 Prososal(提议),并将该 Proposal 分发给集群中所有的 Follower 服务器
当 Leader 服务器接收了正确的反馈后,就会再次向所有的 Follower 服务器分发 Commit 消息,要求将前一个 Proposal 提交

1. ZXID(事务 ID)

1.1 ZXID 的组成

Leader 服务器在接收到事务请求后,会为每个事务请求生成对应的 Proposal 来进行广播
而在广播事务 Proposal 之前,Leader 服务器会首先为这个事务 Proposal 分配一个全局单调递增的唯一 ID ,我们称之为事务 ID(即 ZXID)

ZXID(Zookeeper Transaction ID) 是 ZAB 协议的一个事务编号(即事务 ID),是一个 64 位的数字
其中高 32 位代表 Leader 周期年代的编号(Epoch),标识了每一代 Leader 的唯一性
其中低 32 位是一个递增计数器,针对客户端每一个事务请求,计数器加 1,标识了每代 Leader 的事务的唯一性

Epoch 是当前集群所处的年代(或纪元或周期),即现在是哪一代 Leader
每当有一个新的 Leader 被选举时,就会从这个 Leader 服务器中取出本地日志中的最大事务编号(ZXID)
然后取出其中的 Epoch 值,加 1 后作为新周期的 ID,并将低 32 位的计数器清零

1.2 Epoch 的作用

每当选举产生一个新的 Leader 服务器时生成一个新的 Epoch 值
而服务运行过程中触发选举 Leader 的条件是: Leader 服务器的出现网络中断、崩溃退出、重启等异常情况,或者当集群中半数的服务器与该 Leader 服务器无法通信时

这说明在整个 Zookeeper 集群处于一个异常的情况下,异常前消息广播进行到哪一步骤我们根本不知道

而集群中的其他 Follower 节点从这种崩溃恢复状态重新选举出 Leader 后
如果老 Leader 又恢复了连接进入集群,那么它的的 Epoch 肯定会小于新 Leader 的 Epoch
这时就将老 Leader 就变成 Follower,对新的 Leader 进行数据同步

即便这时老 Leader 对其他的 Follower 节点发送了请求,Follower 节点也会比较 ZXID 的值
因为高 32 位加 1 了,即 Follower 的 Epoch 大于老 Leader 的 Epoch,所以 Follower 会忽略这个请求。

这像改朝换代一样,前朝的剑不能斩本朝的官。

2. 三种角色

除了 Leader 和 Follower,还有一个角色是 Observer

  • Leader:负责整个 Zookeeper 集群工作机制中的核心,主要工作有以下两个

    • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
    • 集群内部各服务器的调度者
  • Follower:它是 Leader 的追随者,其主要工作有三个

    • 处理客户端的非事务请求,转发事务请求给 Leader 服务器
    • 参与事务请求 Proposal 的投票
    • 参与 Leader 选举投票
  • Observer:是 zookeeper 自 3.3.0 开始引入的一个角色

    它不参与事务请求 Proposal 的投票,也不参与 Leader 选举投票
    只提供非事务的服务(查询),通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力

3. 四种状态

在 ZAB 协议中是通过自身的状态来区分自己的角色的

  • LOOKING :处在这个状态时,会进入 Leader 选举状态
  • FOLLOWING :Follower 服务器和 Leader 服务器保持同步时的状态
  • LEADING :Leader 服务器作为主进程领导者的状态
  • **OBSERVING:**服务器的角色为observer,此种服务器不参与投票,只是同步状态

在组成 ZAB 协议的所有进程启动的时候,初始化状态都是 LOOKING 状态
此时进程组中不存在 Leader,选举之后才有,在进行选举成功后,就进入消息广播模式
此时 Zookeeper 集群中的角色状态就不再是 LOOKING 状态

4. 两种基本模式

1.1 消息广播

Zookeeper 用一个单一的主进程来接收并处理客户端的所有事务请求
并采用 ZAB 的原子广播协议将服务器数据状态以事务 Proposal 的形式广播到所有的副本进程中去

这样的模式就保证了:在同一时刻只有一个主进程来广播服务器的状态更变
因此能够很好地处理客户端大量的并发请求,这在 ZAB 协议中叫:消息广播

同时,除消息广播外还需要解决事务的顺序性

在分布式环境中事物的执行顺序也会存在一定的先后关系
比如:事务 C 的写入需要依赖事务 B 的写入,而事务 B 写入需要依赖事务 A 写入
这种前后依赖的顺序也对 ZAB 协议提出了一个要求:ZAB 协议需要保证如果一个状态的变更被处理了,那么所有其依赖的状态变更都已经被提前处理了。也就是需要顺序执行。

消息广播的具体细节:

  • Leader 服务器接收到请求后在进行广播事务 Proposal 之前会为这个事务分配一个 ZXID,再进行广播

  • Leader 服务器会为每个 Follower 服务器都各自分配一个单独的队列
    然后将需要广播的事务 Proposal 依次放入这些队列中去,并根据 FIFO 策略进行消息的发送

  • 每个 Follower 服务器在接收到后都会将其以事务日志的形式写入到本地磁盘中
    并且在成功写入后返回 Leader 服务器一个 ACK 响应

  • 当有超过半数的服务器 ACK 响应后,Leader 就会广播一个 Commit 消息给所有的 Follower 服务器
    Follower 接收到后就完成对事务的提交操作

  • 展开图示

1.2 崩溃恢复

除了能正常广播消息、消息的顺序执行,主进程也可能随时会因为断电、机器宕机等异常情况无法提供服务
因此,ZAB 协议还需要做到在当前主进程出现上述异常情况的时候依然能够正常工作,这在 ZAB 协议中叫:崩溃恢复。

崩溃恢复要做的就是:重新选举出新的 Leader 服务器,选举完成后 Follower 服务器在再去同步 Leader 的数据。

  • 不同情况下崩溃做出的处理:

    运行中的服务再次进行重新选举,一定是出现某种异常
    而出现异常情况之前 Leader 的消息广播可能会处在任何一个阶段
    有可能客户端的请求只是在 Leader 服务器上提出并未被提交,也可能请求已经被 Leader 服务器提交。

    ZAB 协议对于不同阶段的出现的数据不一致的情况做了兼容,保证:

    • 已经在 Leader 服务器上提交的事务,最终会被所有服务器都提交
    • 只在 Leader 服务器上提出的事务,要丢弃

    针对以上的两个要求,在进行 Leader 选举时,只需要选举出集群中 ZXID 最大的事务 Proposal 即可
    这样就可以省去 Leader 服务器检查 Proposal 的提交和丢弃工作了
    因为 Leader 服务器的事务是最大的,一切以 Leader 服务器的数据为标准即可

  • 可能重复的 ZXID:

    ZXID 在集群中其实并不是唯一的,所以也有可能出现多 Follower 服务器 ZXID 相同的情况
    这时候就需要比较 Zookeeper 的 SID 值
    什么是 SID?SID 是一个数字,和 zookeeper 的 myid 一致

重新选举出 Leader 服务器后,会进入消息广播模式 ,开始接收处理客户端的请求

5. 选主的四个阶段

5.1 阶段一:选举(Leader election)

一般有两种情况会发生选举:

  • 当服务器启动时期会进行 Leader 选举。
  • 当服务器运行期 Leader 服务器的出现网络中断、奔溃退出、重启等异常情况
    或者当集群中半数的服务器与该 Leader 服务器无法通信时,进入崩溃恢复模式,开始 Leader 选举。

只要一个节点得到了超过半数节点的票数,它就可以当选准 Leader
只有到了第三阶段(也就是同步阶段),这个准 Leader 才会真正成为 Leader

  • 成为 Leader 的条件:

    Epoch 最大
    Epoch 相同则 ZXID 最大
    Epoch 和 ZXID 相同,则 server_id(即 myid)最大

  • 选举流程:

    当节点处于 LOOKING 状态时,就会发起选举(无论其他节点处于什么状态)
    该结点会向其他结点发送(myid,zxid)信息
    如果是处于刚启动的状态,zxid 为 0,就会发送(myid,0)

    其他结点收到后,将该信息和自己的信息比较,判断对方是否有成为 Leader 的条件
    如果是则给它投一票,同时将投票后的最新投票情况扩散出去
    如果拒绝投票给它,则不需要扩散消息

    每次接收完其他节点的扩散消息后
    就判断是否有半数以上的机器同意某节点成为 Leader
    不是的话则进行新一轮的投票直到选出

    是的话,则该半数以上支持的节点成为 Leader,而其他节点就都变成 Follower

  • 选举示例流程

    目前有 5 台服务器,每台服务器均没有数据,它们的编号分别是 1,2,3,4,5 按编号依次启动,它们的选择举过程如下:

    1. 服务器 1 启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器 1 的状态一直属于 Looking;
    2. 服务器 2 启动,给自己投票,同时与之前启动的服务器 1 交换结果,由于服务器 2 的编号大所以服务器 2 胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是 Looking;
    3. 服务器 3 启动,给自己投票,同时与之前启动的服务器 1,2 交换信息,由于服务器 3 的编号最大所以服务器 3 胜出,此时投票数正好大于半数,所以服务器 3 成为领导者,服务器1,2 成为小弟;
    4. 服务器 4 启动,给自己投票,同时与之前启动的服务器 1,2,3 交换信息,尽管服务器 4 的编号大,但之前服务器 3 已经胜出,所以服务器 4 只能成为小弟;
    5. 服务器 5 启动,后面的逻辑同服务器 4 成为小弟。

5.2 阶段二:发现(Discovery)

该阶段主要进行了:接收提议 Proposal、生成 Epoch、接收 Epoch
主要目的是发现当前大多数节点接收的最新提议
然后由准 Leader 生成新的 Epoch 并让其他 Follower 接受,更新它们的 acceptedEpoch

Follower 和上一轮选举出来的准 Leader 进行通信,同步 Followers最近接收的事务提议
所有 Follower 只会连接到同一个 Leader,如果有一个 Follower 被其他 Follower 认为是 Leader
那将被这个 Follower 拒绝并重新进入选举阶段

具体流程如下:

  1. Follower 将自己最后接受的事务 Proposal 的 Epoch 值发送给准 Leader

  2. 当前接收到过半 Follower 的 Epoch 值后,准 Leader 就生产新的 Epoch值(在原基础+1),然后同步给这些过半的follower。

  3. 当 Follower 接收到来自准 Leader 的新 Epoch 值之后,如果发现当前自己的 Epoch 值小于接收到的新的 Epoch值,
    那么就更新自己的 Epoch值,同时向准 Leader 反馈一个 ack,在反馈的结果中,包含当前 Follower 的 Epoch 值和该 Follower
    的历史事务 Proposal 集合

  4. 当准 Leader 接收到过半的 Follower 的确认消息 ack 之后,就会从这过半服务器都选取一个 Follower
    将其作为初始化事务集合,这个被选取的follower要符合以下要求之一:

    1. 该 Follower 最后接收的事务 Epoch 值要大于接收到 ack 的 Follower 集群中的任意一台(即周期最新)
    2. 如果周期相同,那么就是取 ZXID 最大的。

5.3 阶段三:同步(Synchronization)

同步阶段主要是利用 Leader 前一阶段获得的最新提议历史,同步集群中所有的副本
只有当大多数节点都同步完成,准 Leader 才会成为真正的 Leader
Follower 只会接收 Zxid 比自己的 lastZxid 大的提议

具体流程如下:

  1. 准 Leader 将在发现阶段选取的初始化事务集合发送给过半 Follower
    该消息也叫 newLeader 消息,用于确认自己的 Leader地位
  2. 当 Follower 接收到来自准 Leader 的 newLeader 消息之后,如果发现自己自己的 Epoch 值和发送过来的 Epoch 值不相同
    那么直接进入下一轮循环,因此此时 Follower 发现自己还在上一轮,或者更上轮,无法参与本轮的同步
    如果 Epoch 值相同,那么就执行事务应用操作,最后反馈给 Leader ack 表明自己已经同步了所有历史事务
  3. 当 Leader 接收到这些 Follower 针对 newleader 消息的反馈,就会向所有 Follower 发送 commit 消息,
    Follower 收到 commit消息之后,就将之前的事务提交
    此时,同步阶段完成,超过半数的节点都已经同步到了最新的 Proposal 历史,此时集群就可以开始对外工作了

5.4 阶段四:广播(Broadcast)

到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 Leader 可以进行消息广播
同时如果有新的节点加入,还需要对新节点进行同步

ZAB 提交事务并不像 2PC 一样需要全部 Follower 都 ACK,只需要得到超过半数的节点的 ACK 就可以了

在广播阶段,Leader 会以队列的形式为每一个与自己保持同步的 Follower 创建一个操作队列
同一时刻一个 Follower 只能和一个 Leader 保持同步,Leader 与 Follower 之间通过心跳机制来感知彼此的状况

如果指定的时间内,Leader 无法从过半 Follower 进程中接收到心跳检测
那么 Leader 会终止对当前周期的领导,切换到 LOOKING 状态
同时 Follower 也会放弃这个 Leader,同时切换到 LOOKING 状态
然后开始新一轮的 Leader 选举

具体流程如下:

  1. Leader 接收到客户端新的请求之后,会产生对应事务的 Proposal,并根据 ZXID 的顺序向所有 Follower 发送提案
  2. Follower 根据接收到的消息先后次序处理这些来自 Leader 的事务 Proposal
    并将它们追加到 Hostory 中去,并反馈给 ack 给 Leader
  3. 当 Leader 接收到来自过半的 Follower 机器的 ack 之后,就会发送 commit 消息
  4. Follower 收到 commit 消息之后,就会提交该事务

广播阶段,只要收到过半的 ack 消息就可以发送 commit
那么如果其中有台 Follower 因为网络原因没有收到这个事务,此时它的 ZXID 就小了 1
在 Leader 崩溃之后,重新选举 Leader 时的同步阶段,能同步回去,所以不影响
那么如果 Follower 中途挂了的话,重启之后会加入到恢复阶段,去和 Leader 同步数据,同步完才能进入可用 Follower 列表中,因此也不影响
最后,只有 Leader 可以和客户端通信,因此,follower的数据暂时不一致不影响

6. 四种同步策略

在数据同步之前,Leader 服务器会进行数据同步的初始化
首先会从 Zookeeper 的内存数据库中提取出事务前期对应的提议缓存队列,同时会初始化三个 ZXID 的值:

  • peerLastZxid:这是 Follower 的最后处理 ZXID
  • minCommittedLog:Leader 服务器的提议缓存队列中最小的 ZXID
  • maxCommittedLog:Leader 服务器的提议缓存队列中最大的 ZXID

根据这三个参数,就可以确定四种同步方式,分别为:

  • 直接差异化同步

    场景:当 minCommittedLog < peerLastZxid < maxCommittedLog 时

  • 先回滚在差异化同步

    场景:假如集群有 A、B、C 三台机器,此时 A 是 Leader 但是 A 挂了,在挂之前 A 生成了一个提议假设是:03
    然后集群有重新选举 B 为新的 Leader,此时生成的的提议缓存队列为:01~02
    B 和 C 进行同步之后,生成新的纪元,ZXID 从 10 开始计数,集群进入广播模式处理了部分请求

    假设现在 ZXID 执行到 15 这个值,此时 A 恢复了加入集群
    这时候就比较 A 最后提交的 ZXID:peerLastZxid 与 minCommittedLog、maxCommittedLog 的关系
    此时虽然符合直接差异化同步:minCommittedLog < peerLastZxid < maxCommittedLog 这样的关系
    但是提议缓存队列中却没有这个 ZXID ,这时候就需要先回滚,再进行同步。

  • 仅回滚同步

    场景:这里和先回滚在差异化同步类似,直接回滚就可以。

  • 全量同步

    场景:peerLastZxid < minCommittedLog,当远远落后 Leader 的数据时,直接全量同步。

这就是四种同步策略,这几种同步方式也解决了上面提出的问题:
只在 Leader 服务器上提出的事务,要丢弃 (这个问题会在同步时,会进行回滚,使得只在 Leader 服务器上提出的事务丢弃)