目录

一、分布式一致性原理

二、ZooKeeper架构

        2.1 ZAB 协议操作顺序性

        2.2 领导者选举

        成员身份

        成员状态

        领导者选举

三、总结

        在分布式系统里的多台服务器要对数据状态达成一致,其实是一件很有难度和挑战的事情,因为服务器集群环境的软硬件故障随时会发生,多台服务器对一个数据的记录保持一致,需要一些技巧和设计。

一、分布式一致性原理

        关于分布式一致性,相信你肯定听过著名的 CAP 原理。CAP 原理认为,一个提供数据服务的分布式系统无法同时满足数据一致性(Consistency)、可用性(Availibility)、分区容错性(Patition Tolerance)这三个条件,如下图所示。

        一致性指的是,每次读取的数据都应该是最近写入的数据或者返回一个错误(Every read receives the most recent write or an error),而不是过期数据,也就是说,数据是一致的。

        可用性指的是,每次请求都应该得到一个响应,而不是返回一个错误或者失去响应,不过这个响应不需要保证数据是最近写入的(Every request receives a (non-error) response, without the guarantee that it contains the most recent write),也就是说系统需要一直都是可以正常使用的,不会引起调用者的异常,但是并不保证响应的数据是最新的。

        分区容错性指的是,即使因为网络原因,部分服务器节点之间消息丢失或者延迟了,系统依然应该是可以操作的。

        当网络分区失效发生的时候,我们要么取消操作,这样数据就是一致的,但是系统却不可用;要么我们继续写入数据,但是数据的一致性就得不到保证。

        对于一个分布式系统而言,网络失效一定会发生,也就是说,分区容错性是必须要保证的,那么在可用性和一致性上就必须二选一。当网络分区失效,也就是网络不可用的时候,如果选择了一致性,系统就可能返回一个错误码或者干脆超时,即系统不可用。如果选择了可用性,那么系统总是可以返回一个数据,但是并不能保证这个数据是最新的。

        所以,关于CAP原理,更准确的说法是,在分布式系统必须要满足分区耐受性的前提下,可用性和一致性无法同时满足。

二、ZooKeeper架构

        ZooKeeper 主要提供数据的一致性服务,分布式系统共识算法起源于 Paxos 算法。这里假设你已经了解了 Paxos 算法的视线,不了解也没关系,关于 Paxos 算法具体内容请参考往期文章:探索分布式强一致性奥秘:Paxos共识算法的精妙之旅-CSDN博客

        那我们能用 Paxos 来实现 Zookeeper 各节点的数据一致性吗?答案是否定的,Paxos 算法虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么,也无法保证各值(也就是操作)的顺序性。而这就是 Zookeeper 没有采用 Paxos 的原因,又是 ZAB 协议着力解决的,也是理解 ZAB 协议的关键。

        首先看下 ZooKeeper 的 ZAB 协议是如何实现操作顺序的?

        2.1 ZAB 协议操作顺序性

        如果用一句话来解释 ZAB 协议到底是什么,应该是:能保证操作顺序性的,基于主备模式的原子广播协议。

        先来看一个例子:假如节点 A、B、C 组成的一个分布式集群,指令(比如 X、Y),我们来看下 ZAB 是如何保证实现顺序性的,假设 A 为主节点,B、C 为备份节点。

        首先,需要注意的是,在 ZAB 中,写操作必须在主节点(比如节点 A)上执行。如果客户端访问的节点是备份节点(比如节点 B),它会将写请求转发给主节点。如图所示:        

        接着,当主节点接收到写请求后,它会基于写请求中的指令(也就是 X,Y),来创建一个提案(Proposal),并使用一个唯一的 ID 来标识这个提案。这里的唯一 ID 就是指事务标识符(Transaction ID,也就是 zxid),如下图。

        从图中可以看到,X、Y 对应的事务标识符分别为 <1, 1> 和 <1, 2>,这两个标识符是什么含义呢?

        可以这么理解,事务标识符是 64 位的 long 型变量,有任期编号 epoch 和计数器 counter 两部分组成(为了形象和方便理解,我把 epoch 翻译成任期编号),格式为高 32 位为任期编号,低 32 位为计数器:

  • 任期编号,就是创建提案时领导者的任期编号,需要注意的是,当新领导者当选时,任期编号递增,计数器被设置为零。比如,前领导者的任期编号为 1,那么新领导者对应的任期编号将为 2。
  • 计数器,就是具体标识提案的整数,需要注意的是,每次领导者创建新的提案时,计数器将递增。比如,前一个提案对应的计数器值为 1,那么新的提案对应的计数器值将为 2。

        为什么要设计的这么复杂呢?因为事务标识符必须按照顺序、唯一标识一个提案,也就是说,事务标识符必须是唯一的、递增的。

        在创建完提案之后,主节点会基于 TCP 协议,并按照顺序将提案广播到其他节点。这样就能保证先发送的消息,会先被收到,保证了消息接收的顺序性。

        然后,当主节点接收到指定提案的“大多数”的确认响应后,该提案将处于提交状态(Committed),主节点会通知备份节点提交该提案。

        需要注意的是,主节点提交提案是有顺序性的。主节点根据事务标识符大小,按照顺序提交提案,如果前一个提案未提交,此时主节点是不会提交后一个提案的。也就是说,指令 X 一定会在指令 Y 之前提交。        

        最后,主节点返回执行成功的响应给节点 B,节点 B 再转发给客户端。你看,这样我们就实现了操作的顺序性,保证了指令 X 一定在指令 Y 之前执行。

        当写操作执行完后,接下来可能需要执行读操作了。需要注意,为了提升读并发能力,Zookeeper 提供的是最终一致性,也就是读操作可以在任何节点上执行,客户端会读到旧数据:        

        如果客户端必须要读到最新数据,怎么办呢?Zookeeper 提供了一个解决办法,那就是 sync 命令。可以在执行读操作前,先执行 sync 命令,这样客户端就能读到最新数据了。

        2.2 领导者选举

        系统在运行中,不可避免会出现各种各样的问题,比如进程崩溃了、服务器死机了,这些问题会导致很严重的后果,让系统没办法运行。在 ZAB 中,写请求是必须在主节点上处理的,而且提案的广播和提交,也是由主节点来完成的。既然主节点那么重要,如果它突然崩溃宕机了,该怎么办呢?答案是选举出新的领导者(也就是新的主节点)。

        成员身份

        既然要选举领导者,那就涉及成员身份变更,那么在 ZAB 中,支持哪些成员身份呢。ZAB 支持 3 种成员身份(领导者、跟随者、观察者)。

  • 领导者(Leader):作为主节点(Primary),在同一时间集群只会有一个领导者,所有的写请求必须在主节点上进行。
  • 跟随者(Follower):作为备份节点(Backup),集群中可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票。跟随者可以直接处理并相应客户端的读请求,但写请求必须转发给领导者。
  • 观察者(Observer):作为备份节点(BackUp),类似跟随者,但是没有投票权,不参与领导者选举和提案提交的投票。

        成员状态

        虽然 ZAB 支持 3 种成员身份,但是它定义了 4 种成员状态。

  • LOOKING:选举状态,该状态下的节点认为当前集群中没有领导者,会发起领导者选举。
  • FOLLOWING :跟随者状态,意味着当前节点是跟随者。
  • LEADING :领导者状态,意味着当前节点是领导者。
  • OBSERVING: 观察者状态,意味着当前节点是观察者。

        为什么多了一种成员状态呢?这是因为 ZAB 支持领导者选举,在选举过程中,涉及了一个过渡状态(也就是选举状态)。

        领导者选举

        为了帮你更好地理解 ZAB 的领导者选举,举个例子演示一下,为了演示方便和更容易理解(我们聚焦最核心的领导者 PK),假设投票信息的格式是 <proposedLeader, proposedEpoch, proposedLastZxid, node>,其中:

  • proposedLeader:领导者的集群 ID,也就是在集群配置(比如 myid 配置文件)时指定的 ID。
  • proposedEpoch:领导者的任期编号。
  • proposedLastZxid:领导者的事务标识符最大值(也就是最新提案的事务标识符)。
  • node:投票的节点,比如节点 B。

        假设一个 ZooKeeper 集群,由节点 A、B、C 组成,其中节点 A 是领导者,节点 B、C 是跟随者(为了方便演示,假设 epoch 分别是 1 和 1,lastZxid 分别是 101 和 102,集群 ID 分别为 2 和 3)。那么如果节点 A 宕机了,会如何选举呢?

        首先,当跟随者检测到连接领导者节点的读操作等待超时了,跟随者会变更节点状态,将自己的节点状态变更成 LOOKING,然后发起领导者选举(假设这时节点 B、C 都已经检测到了读操作超时):

        接着,每个节点会创建一张选票,这张选票是投给自己的,也就是说,节点 B、C 都“自告奋勇”推荐自己为领导者,并创建选票 <2, 1, 101, B> 和 <3, 1, 102, C>,然后各自将选票发送给集群中所有节点,也就是说,B 发送给 B、C,C 也发送给 B、C。

        一般而言,节点会先接收到自己发送给自己的选票(因为不需要跨节点通讯,传输更快),也就是说,B 会先收到来自 B 的选票,C 会先收到来自 C 的选票:

        集群的各节点收到选票后,为了选举出数据最完整的节点,对于每一张接收到选票,节点都需要进行领导者 PK,也就将选票提议的领导者和自己提议的领导者进行比较,找出更适合作为领导者的节点,约定的规则如下: 

  • 优先检查任期编号(Epoch),任期编号大的节点作为领导者;
  • 如果任期编号相同,比较事务标识符的最大值,值大的节点作为领导者;
  • 如果事务标识符的最大值相同,比较集群 ID,集群 ID 大的节点作为领导者。

        如果选票提议的领导者,比自己提议的领导者,更适合作为领导者,那么节点将调整选票内容,推荐选票提议的领导者作为领导者。

        当节点 B、C 接收到的选票后,因为选票提议的领导者与自己提议的领导者相同,所以,领导者 PK 的结果,是不需要调整选票信息,那么节点 B、C,正常接收和保存选票就可以了。

        接着节点 B、C 分别接收到来自对方的选票,比如 B 接收到来自 C 的选票,C 接收到来自 B 的选票。对于 C 而言,它提议的领导者是 C,而选票(<2, 1, 101, B>)提议的领导者是 B,因为节点 C 的任期编号与节点 B 相同,但节点 C 的事务标识符的最大值比节点 B 的大,那么,按照约定的规则,相比节点 B,节点 C 更适合作为领导者,也就是说,节点 C 不需要调整选票信息,正常接收和保存选票就可以了。

        接着,当节点 B、C 接收到来自节点 B,新的选票时,因为这张选票(<3, 1, 102, B>)提议的领导者,与他们提议的领导者是一样的,都是节点 C,所以,他们正常接收和存储这张选票,就可以。

        最后,因为此时节点 B、C 提议的领导者(节点 C)赢得大多数选票了(2 张选票),那么,节点 B、C 将根据投票结果,变更节点状态,并退出选举。比如,因为当选的领导者是节点 C,那么节点 B 将变更状态为 FOLLOWING,并退出选举,而节点 C 将变更状态为 LEADING,并退出选举。

        到此,领导者选举结束。

三、总结

        总之,ZAB 协议是 ZooKeeper 实现其作为分布式协调服务核心功能的关键所在,确保了在复杂网络环境和机器故障情况下,仍能提供强大而一致的数据服务,支撑起众多分布式系统的协同工作。

往期经典推荐

探索分布式强一致性奥秘:Paxos共识算法的精妙之旅-CSDN博客

Raft共识算法领导者选举流程揭秘-CSDN博客

Redis使用规范的最佳实践:打造高性能与稳定性的关键法则-CSDN博客

SpringBoot项目并发处理大揭秘,你知道它到底能应对多少请求洪峰?_springboot并发处理-CSDN博客

Kafka消息流转的挑战与对策:消息丢失与重复消费问题_kafka发送消息生产者关闭了-CSDN博客

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐