在《ZooKeeper的作用、应用场景和替代品》中已对 ZooKeeper 进行了介绍,知道了 ZooKeeper 是通过 主从模式 + ZAB 协议 解决单点问题,其中 ZAB 协议是保证分布式一致性的关键。本文将进一步讨论 ZAB 协议和一些问题的思考。
ZAB 协议
ZAB(ZooKeeper Atomic Broadcast,ZooKeeper 原子消息广播协议),其作用在于保证主从节点的一致性。那首先看下 ZooKeeper 的架构图:
从图中我们可以看出在多个 Server 中,只有一台是 Leader,而其他 Server 则担任的角色为 Follower 或 Observer。为了叙述方便,本文我们只讨论 Leader 和 Follower 这两种角色的情况。那么第一个问题来了, 启动集群时,是如何指定一台 Server 当 Leader 的。假设有 3 台 Server A、B、C, Leader 的选举过程如下:
-
每台 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)。
-
接收来自各服务器的投票
-
处理投票
对于接收到的投票,将会依次与自己的投票进行比较,比较的规则是依次比较 ZXID、id 的大小,这里由于 ZXID 都是 0,无法决定哪个投票更大,因此会比较 id。当接收到的投票大于自身投票时,则会更新投票内容,否则不更新,再向集群中的其他 Server 发出投票。
-
统计投票
每次投票后,会统计所有的投票,如果有过半的投票推举了某台 Server,则认为已经选出了 Leader,将不再发送投票。
-
改变服务器状态
确定 Leader 后,如果自身是 Leader,将状态更改为 LEADING,如果是 Follower,则状态变更为 FOLLOWING。
通过上面的流程便能够从集群中选举出 Leader。下面看下 ZAB 协议是如何在主从模式下工作的,主要有两个阶段:消息广播和崩溃恢复。在消息广播阶段,会同步主从节点的数据,而当 Leader 失效时,则会进入崩溃恢复阶段,重新选举 Leader 并进行数据的同步,避免单点故障。
首先看下消息广播阶段。这里我们关注客户端的两种请求,事务请求和非事务请求。所谓的事务请求,我们可以简单认为是一个写操作,会改变数据,而非事务请求,则是读操作。
-
非事务请求:Leader、Follower 会根据本地数据返回结果;
-
事务请求:统一由 Leader 处理,当 Follower 收到时,会转发给 Leader。
当 Leader 收到事务请求时,便将会进行消息广播:
- Leade 将事务请求转换成一个事务 Proposal(提议),广播给所有的 Follower。
- 当 Follower 收到 Proposal 时,会将事务操作写到事务日志中,然后会返回给 Leader 一个 Ack 响应。
- 当 Leader 接收到了半数以上的 Ack 响应时,则认为该事务可提交,则提交事务,并再广播一个 Commit 消息。
- 当 Follower 收到 Commit 消息时,则提交本地事务。
通过消息广播,可以实现主从节点的数据一致性。而如果 Leader 宕机或者由于网络原因与过半的 Follower 失去了联系,为保证一致性,将进入崩溃恢复阶段。
崩溃恢复阶段,会通过 Leader 选举选出新的 Leader,然后进行数据同步。这里的 Leader 选举的过程仍和上文叙述的相同。而这时候 ZXID 就能起到作用了。ZXID 是一个 64 位的数字,前 32 位为 epochID,后 32 位为事务ID。
- 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 保证同步后再读。
参考
-
《从Paxos到ZooKeeper》