今天去智一面面试高级运维开发的岗位,面试官上来就问 ZAB 协议,瑟瑟发抖…
我把做的题分享给大家:高级运维工程师在线评测
Zookeeper 是通过 ZAB 一致性协议来实现分布式事务的最终一致性。
ZAB 协议介绍
ZAB 全称为 Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)
ZAB 协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议。基于该协议,ZooKeeper 实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB的消息广播过程使用的是原子广播协议,类似于二阶段提交。针对客户端的请求,Leader服务器生成对应的事务提议,并将其发送给集群中所有的 Follower 服务器。然后收集各自的选票,最后进行事务提交。如图:
在 ZAB 协议中二阶段提交,移除了中断逻辑。所有的 Follower 服务器要么正常反馈 Leader 提出的事务提议,要么就抛弃 Leader 服务器。同时,我们可以在过半的 Follower 服务器已经反馈 ACK 后,就开始提交事务提议了。
Leader 服务器会为事务提议分配一个全局单调递增的 ID,称为事务 ID(ZXID)。由于 ZAB 协议需要保证每一个消息严格的因果关系,因此需要将每一个事务提议按照其 ZXID 的先后顺序进行处理。
在消息广播过程中,Leader 服务器会为每一个 Follower 服务器分配一个队列,然后将事务提议依次放入到这些队列中去,并且根据 FIFO 的策略进行消息发送。
每一个 Follower 服务器接收到这个事务提议后,会把该事务提议以事务日志的形式写入到本地磁盘中,并且写入成功后,反馈给 Leader 服务器 ACK。
当 Leader 服务器收到过半 Follower 服务器的 ACK,就发送一个 COMMIT 消息,同时 Leader 自身完成事务提交,Follower 服务器接收到 COMMIT 消息后,也进行事务提交。
之所以采用原子广播协议协议,是为了保证分布式数据一致性。过半的节点数据保存一致性。
消息广播
你可以认为消息广播机制是简化版的 2PC协议,就是通过如下的机制保证事务的顺序一致性的。
客户端提交事务请求时 Leader 节点为每一个请求生成一个事务 Proposal,将其发送给集群中所有的 Follower 节点,收到过半 Follower的反馈后开始对事务进行提交,ZAB 协议使用了原子广播协议;在 ZAB 协议中只需要得到过半的 Follower 节点反馈 Ack 就可以对事务进行提交,这也导致了 Leader 节点崩溃后可能会出现数据不一致的情况,ZAB 使用了崩溃恢复来处理数字不一致问题;消息广播使用了TCP 协议进行通讯所有保证了接受和发送事务的顺序性。广播消息时 Leader 节点为每个事务 Proposal分配一个全局递增的 ZXID(事务ID),每个事务 Proposal 都按照 ZXID 顺序来处理;
Leader 节点为每一个 Follower 节点分配一个队列按事务 ZXID 顺序放入到队列中,且根据队列的规则 FIFO 来进行事务的发送。Follower节点收到事务 Proposal 后会将该事务以事务日志方式写入到本地磁盘中,成功后反馈 Ack 消息给 Leader 节点,Leader 在接收到过半Follower 节点的 Ack 反馈后就会进行事务的提交,以此同时向所有的 Follower 节点广播 Commit 消息,Follower 节点收到 Commit 后开始对事务进行提交;
崩溃恢复
消息广播过程中,Leader 崩溃了还能保证数据一致吗?当 Leader 崩溃会进入崩溃恢复模式。其实主要是对如下两种情况的处理。
-
Leader 在复制数据给所有 Follwer 之后崩溃,怎么处理?
-
Leader 在收到 Ack 并提交了自己,同时发送了部分 commit 出去之后崩溃,怎么处理?
针对此问题,ZAB 定义了 2 个原则:
-
ZAB 协议确保
执行
那些已经在 Leader 提交的事务最终会被所有服务器提交。 -
ZAB 协议确保
丢弃
那些只在 Leader 提出/复制,但没有提交的事务。
至于如何实现确保提交已经被 Leader 提交的事务,同时丢弃已经被跳过的事务呢?核心是通过 ZXID 来进行处理。在崩溃过后进行恢复的时候会选择最大的 zxid 作为恢复的快照。这样的好处是: 可以省略事务提交的检查和事务的丢弃工作以提升效率
数据同步
完成Leader选举之后,在正式开始工作之前,Leader服务器会去确认事务日志中所有事务提议(指已经提交的事务提议)是否都已经被过半的机器提交了,即是否完成数据同步。下面是ZAB协议的 数据同步过程。
Leader服务器为每一个Follower服务器准备一个队列,将那些没有被Follower服务器同步的事务以事务提议的形式逐个发送给Follower服务器,并在每一个事务提议消息后面发送一个commit消息,表示该事务已被提交。
等到Follower服务器将所有其未同步的事务提议都从Leader服务器上面同步过来,并且应用到本地数据库后,Leader服务器就会将该Follower服务器加入到真正可用的Follower列表中。
ZXID 的设计
ZXID 是一个64位的数字, 如下图所示。
其中低 32 位是一个简单的单调递增的计数器,Leader 服务器产生一个新的事务提议的时候,都会对该计数器 +1。
高 32 位,用来区分不同的 Leader 服务器。具体做法是,每选举产生一个新的 Leader 服务器,就会从 Leader 服务器的本地日志中取出一个最大的 ZXID,生成对应的 epoch 值,然后再进行加1操作,之后就会以该值作为新的 epoch。并将低 32 位从 0 开始生成 ZXID。(我理解这里的 epoch 代表的就是一个 Leader 服务器的标志,每次选举 Leader 服务器,那么 epoch 值就会更新,代表是这段时期由这个新的 Leader 服务器进行事务请求的处理)。
ZAB 协议中通过 epoch 编号来区分 Leader 周期变化,能够有效避免不同 Leader 服务器使用相同的 ZXID。
下面是我 Leader 节点的 zxid 生成核心代码大家可以看一下。
// Leader.java
void lead() throws IOException, InterruptedException {
// ....
long epoch = getEpochToPropose(self.getId(), self.getAcceptedEpoch());
zk.setZxid(ZxidUtils.makeZxid(epoch, 0));
// ....
}
//
public long getEpochToPropose(long sid, long lastAcceptedEpoch) throws InterruptedException, IOException {
synchronized (connectingFollowers) {
// ....
if (isParticipant(sid)) {
// 将自己加入连接队伍中,方便后面判断 lead 是否有效
connectingFollowers.add(sid);
}
QuorumVerifier verifier = self.getQuorumVerifier();
// 如果有足够多的 follower 进入, 选举有效,则无需等待,并通过其他等待的线程,类似 Barrier
if (connectingFollowers.contains(self.getId()) && verifier.containsQuorum(connectingFollowers)) {
waitingForNewEpoch = false;
self.setAcceptedEpoch(epoch);
connectingFollowers.notifyAll();
} else {
// ....
// followers 不够就进入等待, 超时时间为 initLimit
while (waitingForNewEpoch && cur < end && !quitWaitForEpoch) {
connectingFollowers.wait(end - cur);
cur = Time.currentElapsedTime();
}
// 超时退出,重新选举
if (waitingForNewEpoch) {
throw new InterruptedException("Timeout while waiting for epoch from quorum");
}
}
return epoch;
}
}
// ZxidUtils
public static long makeZxid(long epoch, long counter) {
return (epoch << 32L) | (counter & 0xffffffffL);
}
ZAB 协议实现
写数据的过程
下面我梳理了 zookeeper 源码中写数据的过程,如下图所示:
运维工程师QQ在线交流群:580175957