ZAB协议和一些思考

 

《ZooKeeper的作用、应用场景和替代品》中已对 ZooKeeper 进行了介绍,知道了 ZooKeeper 是通过 主从模式 + ZAB 协议 解决单点问题,其中 ZAB 协议是保证分布式一致性的关键。本文将进一步讨论 ZAB 协议和一些问题的思考。

ZAB 协议

ZAB(ZooKeeper Atomic Broadcast,ZooKeeper 原子消息广播协议),其作用在于保证主从节点的一致性。那首先看下 ZooKeeper 的架构图:

ZooKeeper架构

从图中我们可以看出在多个 Server 中,只有一台是 Leader,而其他 Server 则担任的角色为 Follower 或 Observer。为了叙述方便,本文我们只讨论 Leader 和 Follower 这两种角色的情况。那么第一个问题来了, 启动集群时,是如何指定一台 Server 当 Leader 的。假设有 3 台 Server A、B、C, Leader 的选举过程如下:

  1. 每台 Server 会发出一个投票

    Server 启动时,状态为 LOOKING,将发出投票选举 Leader,投票的格式为(id, ZXID)

    • id:被推举的 Leader 的 SID 值(每台 Server 的唯一 ID,由配置文件配置)
    • ZXID:被推举的 Leader 的事务 ID

    ZXID,先不细说,后面会介绍,这里由于是集群启动,所以各 Server 的 ZXID 均为0。

    由于每台 Server 不知道其他机器的情况,唯一能确定的是自身是在运行的,因此,第一票都将投给自己。这里假设 Server A、B、C 的 SID 是 1、2、3,则发出的投票分别为 (1,0)、(2,0)、(3,0)。

  2. 接收来自各服务器的投票

  3. 处理投票

    对于接收到的投票,将会依次与自己的投票进行比较,比较的规则是依次比较 ZXID、id 的大小,这里由于 ZXID 都是 0,无法决定哪个投票更大,因此会比较 id。当接收到的投票大于自身投票时,则会更新投票内容,否则不更新,再向集群中的其他 Server 发出投票。

  4. 统计投票

    每次投票后,会统计所有的投票,如果有过半的投票推举了某台 Server,则认为已经选出了 Leader,将不再发送投票。

  5. 改变服务器状态

    确定 Leader 后,如果自身是 Leader,将状态更改为 LEADING,如果是 Follower,则状态变更为 FOLLOWING。

服务器启动时的Leader选举

通过上面的流程便能够从集群中选举出 Leader。下面看下 ZAB 协议是如何在主从模式下工作的,主要有两个阶段:消息广播崩溃恢复。在消息广播阶段,会同步主从节点的数据,而当 Leader 失效时,则会进入崩溃恢复阶段,重新选举 Leader 并进行数据的同步,避免单点故障。

首先看下消息广播阶段。这里我们关注客户端的两种请求,事务请求和非事务请求。所谓的事务请求,我们可以简单认为是一个写操作,会改变数据,而非事务请求,则是读操作。

  • 非事务请求:Leader、Follower 会根据本地数据返回结果;

  • 事务请求:统一由 Leader 处理,当 Follower 收到时,会转发给 Leader。

当 Leader 收到事务请求时,便将会进行消息广播:

  1. Leade 将事务请求转换成一个事务 Proposal(提议),广播给所有的 Follower。
  2. 当 Follower 收到 Proposal 时,会将事务操作写到事务日志中,然后会返回给 Leader 一个 Ack 响应。
  3. 当 Leader 接收到了半数以上的 Ack 响应时,则认为该事务可提交,则提交事务,并再广播一个 Commit 消息。
  4. 当 Follower 收到 Commit 消息时,则提交本地事务。

消息广播

通过消息广播,可以实现主从节点的数据一致性。而如果 Leader 宕机或者由于网络原因与过半的 Follower 失去了联系,为保证一致性,将进入崩溃恢复阶段。

崩溃恢复阶段,会通过 Leader 选举选出新的 Leader,然后进行数据同步。这里的 Leader 选举的过程仍和上文叙述的相同。而这时候 ZXID 就能起到作用了。ZXID 是一个 64 位的数字,前 32 位为 epochID,后 32 位为事务ID。

ZXID

  • epochID:集群启动时,初始为0,每当选举一个新的 Leader 时,则认为进入了一个新的 Leader 周期,加1;
  • 事务ID:集群启动时,初始为0,每当为一个事务请求产生一个 Proposal 提议时,加1;

epochID 的设计为了避免事务 ID 的冲突。例如作为 Leader 的 Server A 上有一个 事务ID 3 的提议,还没来得及广播,就宕机了,而 Follower 上的 事务ID 还是 2,当 Follower 选举出了一个新 Leader,Server C,当处理一个新的事务请求时,事务ID 2 + 1 = 3,而这时 Server A 重启了,加入到集群中,两个事务请求不同,但却有同样的ID,便会产生冲突。通过引入 epochID,则避免了这种情况,例如 Server A 当 Leader 时,epochID 为 0,而 Server C 当 Leader 时,epochID 为 1,当 Server A 与 Server C 对比,发现自己提议记录到的是<0, 1> <0, 2><0,3>,而 Leader 上的是 <0, 1><0, 2> <1, 3>,则会将提议回退到 <0, 2>,重新同步 <1, 3>提议,保持和 Leader 的一致。

事务ID 参与投票的比较,保证了选举出的 Leader 具有最大的事务 ID,即数据是最新的,那么其他 Server 只要和 Leader 进行数据同步,也便保证了整个集群数据的一致性。当崩溃恢复阶段完成,集群便再次进入消息广播阶段。

思考

这部分是笔者在学习 ZAB 协议中思考的几个问题。

(1)在广播消息阶段,为什么是要求收到半数以上的 ACK 响应则可提交?

在 2PC 中,是要求收到全部的 ACK 响应,才可进行提交,这样条件过于苛刻,当有一台 Follower 宕机时,便会影响事务处理。而 ZAB 中,则将事务处理受 Follower 的影响下降了,只要求半数以上即可,而为什么要求半数以上,则是为了防止脑裂。 《脑裂是什么?Zookeeper是如何解决的?》这篇文章讲的很清楚了。

(2)如果 Leader 先后广播了两个提议 p1、p2,Follower A正常先后收到了 p1、p2,而 Follower B 由于网络问题没收到 p1,只收到了 p2,或者先收到了 p2 再收到了 p1,收到的顺序错误。那在 ZooKeeper 中是如何保证消息的可靠性和顺序性?

在 Leader 中,会为每个 Follower 维护一个消息队列,并且使用 TCP 协议发送消息从而保证消息的可靠性和顺序性。所以即使 Follower A 已经收到了 p1、p2,但 Leader 仍能为没收到 p1 的 Follower B 重发 p1,确认 p1 发送成功后才开始发送 p2。 消息队列

(3)Client 读到的一定是最新的数据吗?

由于 ZAB 的过程,我们可以知道不一定是最新的。例如 Follower B 由于网络原因还没有接收到某个事务的 Commit 请求,数据版本将晚于其他 Server,这时读到的数据就不是集群中最新的了。虽然 ZAB 不能保证强一致性,但能保证顺序一致性。

顺序一致性

顺序一致性:客户端发送的更新命令,服务端会按它们发送的顺序执行

如果一定要保证读取到的是最新的数据,则在读之前,先通过 Sync 保证同步后再读。

参考

  1. 《从Paxos到ZooKeeper》

  2. Apache ZooKeeper: How do writes work