在分布式系统的复杂架构中,协调者扮演着无可替代的核心角色。想象一下,如果缺少协调者,整个系统就如同失去了指挥中枢,机器之间的交互关系将无奈退回至中心化模式,或是局限于指定 IP 地址调用的原始状态,这无疑会严重制约系统的性能与扩展性。
ZAB(ZooKeeper Atomic Broadcast)协议,作为卓越的协调者一致性协议,是分布式领域里的关键技术。无论你是深耕分布式系统设计的专业人士,还是渴望投身该领域的求职者,熟练掌握 ZAB 协议都是开启成功大门的关键钥匙。ZAB 堪称 ZooKeeper 一致性的灵魂所在,它精妙地维持着系统的有序与稳定。
为了帮助你透彻理解 ZAB 协议,我们将采用循序渐进的方式。首先,我会带你深入剖析 ZooKeeper 的基础模型,为理解 ZAB 筑牢根基。待你熟悉基础模型后,再一同探究 ZAB 协议在正常运行与异常状况下,分别是如何巧妙达成一致性的,一步步揭开 ZAB 协议的神秘面纱。
ZooKeeper 基础模型
简而言之,在 ZooKeeper 集群里,由一台 Leader 服务器专门处理外部客户端的写请求,其余服务器均为 Follower。Leader 服务器会把客户端发起的写操作数据,同步至所有 Follower 节点,以此确保 Leader 与 Follower 上的数据完全一致。如此一来,你的微服务无论从哪个节点获取数据,得到的结果都毫无二致。
在当今的微服务架构体系中,许多微服务框架都选用 ZooKeeper 作为注册中心。这是因为 ZooKeeper 能够确保,任何一个微服务在任意一台 ZooKeeper 节点上读取到的数据都保持一致。它就像一位贴心的幕后管家,巧妙地将底层复杂的设计细节隐藏起来,让开发者无需为数据一致性的难题费心,能够轻松享受数据一致性带来的便捷与高效,专注于业务逻辑的开发 。
正常情况下的 ZAB 协议
知道了 ZooKeeper,我们再来学习它保持一致性的秘密:ZAB 协议。
我们从字面上分析下 ZAB,它的全称是 ZooKeeper Atomic Broadcast。这里的重点是 Atomic Broadcast,也就是原子广播。
其实,我不喜欢将 ZAB 称作“协议”,因为“协议”是一个名词,就像 HTTP 协议的 header 和 body 约束,给人的感觉更像是一种格式规范,而实际上 ZAB 本质是一系列“动作”。很明显,就像 Java 并发包中的 atomic 类一样,ZAB 是不可分的一系列操作,至于广播其实就是上面提到的 Leader 向所有 Follower 传递数据的过程。
既然是原子,就代表无论集群是否正常,我都要保证广播是可以进行下去的,下面我会从正常和异常两个角度解释 ZAB 的过程。
正常的情况叫做消息广播模式,如果你之前了解过两阶段提交,理解起来就简单多了,消息广播就是一个两阶段提交过程,我们来看一下这个过程:
如图,Leader 和每个 Follower 之间会使用一个队列作为信息交换解耦,我们分步来看看这个过程:
第一步,Leader 将客户端发来的写请求发给所有的队列;
第二步,每个 Follower 从队列取出消息;
第三步,若 Follower 觉得自身可以提交这个请求变更,则 Follower 发送一个确认回复给消息队列;
第四步,Leader 从队列消费消息,如果 Leader 发现收到半数以上的确认消息,则开始第五步;
第五步,Leader 向所有的队列发送提交消息;
第六步,Follower 消费队列的消息,分别在本地提交变更。
通俗地说,就是我是老大,我要像大家传递旨意,但是传达之前我要看看大家是不是支持我,如果多数人支持我,我再决定执行。
如果不考虑异常情况,ZAB 协议我们就讲完了。
异常情况下的 ZAB 协议
以上是比较理想的情况,然而在现实中,我们不可能不考虑异常情况,我们接下来会讲述两种异常的情况,通过讲述,你就会感觉到,ZAB 的精髓其实是异常情况下的处理。
Master 挂了的情况
显而易见,若一台Follower服务器出现故障,并不会对整体系统运行产生显著影响,毕竟Follower的主要职责是作为数据副本存在。然而,要是Master(在ZooKeeper中通常称为Leader)服务器挂掉了,又该如何应对呢?
在此,我们先补充一个关键知识点。在ZooKeeper的运行机制里,每一个客户端发起的写请求,都会被分配一个独一无二的标识:zxid。这个zxid可不是随意生成的,它是由Leader节点的id与一个不断递增的请求id组合而成。特别留意这个id的生成规则,在后续理解ZooKeeper的故障恢复与一致性保障机制时,它可是起着举足轻重的作用 。
设想存在3台机器,分别为Server1、Server2和Server3 ,其中Server2担任着Leader的角色。然而,在运行过程中,Server2突然发生故障而停止工作。此时,Server1和Server3都有成为新Leader的诉求。于是,剩余的Server1和Server3便开启了投票选举流程。
需要注意的是,此次投票所使用的选票,其格式涵盖了服务器的id以及前文提及的zxid 。其中,服务器的id仅作为一种标识,我们设定Server1、Server2、Server3所对应的服务器id分别为1、2、3 。假设在这次投票中,Server1为自己投出的选票内容为(1,101899) ,这个选票数据将在后续的选举进程中,对最终的Leader产生结果发挥关键作用。
紧接着,Server3也为自己投出一票,其选票内容为(3, 101895)。在这场角逐中,Server1和Server3都难免带有“私心”,它们都渴望自己能成为新的领导者,于是各自将选票投给自己,并向其他节点进行广播,告知自己的投票结果。但此刻整个系统中仅有两台存活的节点,所以Server1把自己的投票结果传达给了Server3,Server3也同样把自己的投票结果告知了Server1。
Server1在接收到Server3的投票信息(3, 101895)后,对zxid进行比较,发现自身的zxid 101899更大。基于此判断,Server1认定自己才是最适合担任新领导者的节点。而Server3在得知Server1的zxid更大后,尽管起初有自己的“小算盘”,但最终还是顾全大局,将选票改投给了Server1。如此一来,Server1成功获得两票,顺利成为了新的Leader。
可能有同学会产生疑问:要是zxid相同,又该如何判定呢?ZooKeeper对此给出的解决方案是,当zxid一致时,进一步比较服务器id的大小来确定领导者。
在上述例子里,zxid之所以不同,与之前提到的两阶段提交机制用于保证原子性有关。实际上,由于队列的存在,不同节点处理请求的速度有差异,所以本身各个节点的zxid就是不一样的。并且,如果强制要求各个节点时刻保持串行一致性,那整个系统的性能表现将会大打折扣。
另外,单纯实现两阶段提交,和保证原子性并没有直接紧密的联系,它更多的是提高了操作的成功率。即便Leader收到了半数以上节点的确认信息,一旦此时网络突发故障,依然无法确保请求能够被顺利执行。
不过,这并不意味着两阶段提交毫无价值。在实际生产环境中,两阶段提交确实提高了操作的成功率。后续为了进一步优化两阶段提交的流程,又出现了三阶段提交等机制,但这些内容并不在本节课的讨论范围之内,所以在此就不深入展开了。
ZooKeeper 集群启动
那么,除了 Master 挂了的异常情况以外,还有一个重要的场景,就是当我们启动 ZooKeeper 集群的时候,是一台一台启动的。这种情况也需要选主,假设我们启动了 Server1,它首先给自己投票 (1,101000),并且广播自己的投票数据,但此时只有 Server1 是启动状态,所以并没人收到消息。
随后,Server2完成启动,并为自己投出一票,选票信息为(2, 101000) 。由于此次是初始启动,其zxid与其他相关初始值相同。紧接着,Server2将自己的投票信息(2, 101000)进行广播。当遇到zxid相同的情况时,便会依据服务器id进行比较。Server1在接收到Server2的投票信息后,经过比较判断,认为Server2更具备担任Leader的条件,于是果断将自己的选票改投给Server2。最终,Server2凭借获得的两票,成功当选为初始化Leader,就如同封建王朝中登基的“秦始皇”一般,开启全新的领导篇章。
新的Leader顺利选出后,一个棘手的问题接踵而至:倘若老的Leader恢复运行,又该如何应对呢?此时,系统中便会出现两个看似“领导”的角色,即刚刚恢复的Server2和新当选的Server1。不妨设身处地感受一下Server3的处境,它必定会感到困惑不已,仿佛大脑都要“裂开”了。这种情况便是典型的脑裂问题。在选主过程中,脑裂问题具体是指,由于主节点切换过程不够彻底,或者受到其他因素干扰,致使Follower节点产生错觉,误认为存在两个Leader,进而导致整个集群陷入混乱无序的状态,严重影响系统的正常运行。
大家应该还记得,我们之前提到过zxid的组成部分中包含Leader的节点id。此刻,请求的zxid已然变为Server1的id,系统不再认可Server3之前基于旧规则的状态。zxid中的节点id在英文里被称作epoch ,若将其直译为“年代”,或许不够形象生动;我更倾向于将它翻译为“皇帝的年号”,如此一来,便能更加贴切地体现其在系统中的独特标识与阶段性特征,就像不同皇帝在位期间拥有不同的年号,代表着不同的统治时期与系统状态。
最后,我们来深入探讨一下ZooKeeper设置奇数个节点的重要意义。以我们当前所讨论的场景为例,假设有3个节点,当其中1个节点出现故障时,剩余的2个节点依然能够维持系统的正常运转,因为此时存活节点数超过了总节点数的一半,足以进行投票等关键操作;但倘若有2个节点同时故障,存活节点数便不足半数,系统将无法进行投票等操作,进而无法正常工作。再设想一下,如果我们将节点数量增加到4个,当有1个节点故障时,系统尚可正常运行;然而,一旦有2个节点出现故障,存活节点数同样不足半数,系统同样无法正常工作,这与仅有3个节点时出现2个故障节点的情况相同。但3个节点相较于4个节点,数量上减少了1个,却能达成相同的系统效益。因此,从资源利用与系统稳定性的综合考量出发,ZooKeeper推荐设置奇数个Server节点。
新的Leader成功上任后,首要任务便是妥善处理前任留下的遗留问题,也就是常说的“收拾烂摊子”。但具体该如何操作呢?这里并没有一个放之四海而皆准的标准答案。不过,ZAB协议提供了一个明确的标准:新Leader必须确保前任Leader已经提交的提案,最终能够被所有的Follower节点成功提交;同时,坚决丢弃上一任Leader尚未最终提交的请求。简单来说,在正常的消息广播过程中,若第五步的请求还未发出,便需要将其丢弃。如此一来,新Leader才能有条不紊地带领系统迈向新的阶段,保障系统的稳定与高效运行。
我们再回顾一下,加深印象:
第一,投票过程先比较 zxid,zxid 相同就比较节点 id;
第二,zxid 通过年代编号来阻止脑裂问题;
第三,ZooKeeper 最好设置为奇数个节点,因为多出来的那一个没什么用处;
第四,新官上任三把火,新的 Leader 需要按照一定规则处理前任的烂摊子。
总结
在今天的学习旅程中,我们一同深入探索了ZAB协议。ZAB协议并非孤立的单一规则,而是一系列严密操作的有机集合。在正常运行状态下,原子广播依托两阶段提交机制来确保数据的一致性与完整性,每一个步骤都紧密相扣,不容有失。
当面对异常情况时,整个系统的稳健性便受到了考验。此时,如何精准高效地进行选主成为关键环节,这关乎着系统能否迅速恢复正常运转。而新的领导者上位后,又需妥善规划并执行一系列操作,稳定系统秩序,保障服务的连续性。
此外,“同时操作N个节点获取过半节点的肯定”这一方法论,堪称ZAB协议乃至整个分布式系统的核心要义之一。它犹如一把精准的标尺,衡量着系统决策的科学性与可靠性,为系统在复杂多变的环境中稳健前行提供了坚实保障,深刻影响着系统的性能与稳定性。