共识算法 basic paxos -> multi paxos -> raft -> zab

736 阅读53分钟

共识算法

浅谈分布式一致性协议tech.bytedance.net/course-vide…

共识算法,可以理解为是为了实现分布式一致性协议而产生的一系列流程与规则。当分布在不同地域的节点都按照这套规则进行协商交互之后,最终总能就某个/某些问题得到一致的决策,从而实现分布式系统中不同节点的一致性。

以下内容建议按照这个顺序阅读:

basic paxos -> multi paxos -> raft -> zab

Raft算法

elearning.feishu.cn/student/cou…

juejin.cn/post/690715…

在分布式系统中,为了消除单点提高系统可用性,通常会使用副本来进行容错,但这会带来另一个问题,即如何保证多个副本之间的一致性?

这里我们只讨论强一致性,即线性一致性。弱一致性涵盖的范围较广,涉及根据实际场景进行诸多取舍,不在 Raft 系列的讨论目标范围内。

所谓的强一致性(线性一致性)并不是指集群中所有节点在任一时刻的状态必须完全一致,而是指一个目标,即让一个分布式系统看起来只有一个数据副本,并且读写操作都是原子的,这样应用层就可以忽略系统底层多个数据副本间的同步问题。也就是说,我们可以将一个强一致性分布式系统当成一个整体,一旦某个客户端成功的执行了写操作,那么所有客户端都一定能读出刚刚写入的值。即使发生网络分区故障,或者少部分节点发生异常,整个集群依然能够像单机一样提供服务。

共识算法(Consensus Algorithm)就是用来做这个事情的,它保证即使在小部分(≤ (N-1)/2)节点故障的情况下,系统仍然能正常对外提供服务。共识算法通常基于状态复制机(Replicated State Machine)模型,也就是所有节点从同一个 state 出发,经过同样的操作 log,最终达到一致的 state。

共识算法是构建强一致性分布式系统的基石,Paxos 是共识算法的代表,而 Raft 则是其作者在博士期间研究 Paxos 时提出的一个变种,主要优点是容易理解、易于实现,甚至关键的部分都在论文中给出了伪代码实现。

首先,Raft 集群必须存在一个主节点(leader),我们作为客户端向集群发起的所有操作都必须经由主节点处理。所以 Raft 核心算法中的第一部分就是选主Leader election)——没有主节点集群就无法工作,先票选出一个主节点,再考虑其它事情。

其次,主节点需要承载什么工作呢?它会负责接收客户端发过来的操作请求,将操作包装为日志同步给其它节点,在保证大部分节点都同步了本次操作后,就可以安全地给客户端回应响应了。这一部分工作在 Raft 核心算法中叫日志复制Log replication)。

然后,因为主节点的责任是如此之大,所以节点们在选主的时候一定要谨慎,只有符合条件的节点才可以当选主节点。此外主节点在处理操作日志的时候也一定要谨慎,为了保证集群对外展现的一致性,不可以覆盖或删除前任主节点已经处理成功的操作日志。所谓的“谨慎处理”,其实就是在选主和提交日志的时候进行一些限制,这一部分在 Raft 核心算法中叫安全性Safety)。

Raft 核心算法其实就是由这三个子问题组成的:选主(Leader election)、日志复制(Log replication)、安全性(Safety)。这三部分共同实现了 Raft 核心的共识和 容错 机制。

基本概念

假设:如果,环境不出现问题,每个节点应该按照预期的方式运行。(非拜占庭) 即没有叛徒,没有欺骗,相互信任。 分布式系统中,每个服务器/网络都随时可能故障。

问题:

问题Raft
如何在主从上同步数据(容灾)日志复制
如何在异常中选择新的主节点(自治)领导选举
如何保证异常状态中的数据安全数据安全

节点 状态机

Raft中节点有三种身份

  • 领导者(Leader):发号施令,处理所有的数据操作。
  • 跟随者(Follower):执行Leader的指令,进行投票。出现问题时转变为候选人。
  • 候选人(Candidate):发现Leader出现问题时的挺身而出者。

需要你注意的是,Raft 算法是强领导者模型,集群中只能有一个“霸道总裁”。

任期Term

  • raft的时间被切分为多个任期
  • 当切换leader时,首先会进行选举,同时也开启一个新的任期
  • raft保证每个任期只能产生一名leader
  • 每一个节点都会保存当前leader的最大任期

领导选举

  • 任期(term):每一个节点中都保存了当前主节点的最大任期。
  • 任期(term)代表当前主节点是第几届Leader
  • 本文中任期简写为T1,T2...Tn

图中B节点可能发生了网络隔离,不知道A成为了新的leader,因为认可的leader的最大任期T1。

一次正常的请求处理流程

raft不是读写分离的,所有读写请求都在主节点上。如果有请求打到从节点上,会转发给主节点。

  • 主节点收到请求,追加日志后(此时日志未提交),将数据同步给所有从节点。

  • 从节点收到数据后,返回ack给主节点。

  • 主节点收到超过1/2以上的节点ack后,确认数据安全,提交数据。

  • Raft保证只要数据提交了,那么半数以上的节点都会有一份数据备份。

  • Raft保证集群中只要有半数以上的节点有效,则整个集群都可以正常提供服务。

  • 这里提交有顺序,比如先提交给B,D还未收到提交的信息,就会出现严重的不一致。怎么解决后面说。

Raft 集群通常只有奇数个节点

偶数个节点,和比起少一个的奇数个节点相比,可用率是相同的。因此经常是奇数个节点。

问题1: 我们如何检测服务是否可用?

  • 从节点会监控主节点的心跳是否超时。(主节点发送心跳给从节点)
  • 任何节点只要发现主节点的心跳超时,就可以认为主节点已经失效。

问题2: 如何选择出新的主节点?

  • 某个节点发现心跳超时的时候,会将自己的任期(Term)加一,并发起一轮选举。

  • 任意一个节点接收到一轮新任期的选举时,都会进行投票。

    • 当然投票有一些条件(有的节点会同意,有的节点不会同意)

  • 当一个候选人收到半数以上的选票时,赢得此次任期。
  • 新的主节点开始向所有的节点发送心跳,以宣示主权。

节点任期号什么时候更新?

  1. 收到任期号更大领导节点的心跳时。
  2. 投票给任期号更大的候选者节点时。

问题2-1: 从节点应该如何投票呢?

任意一个节点接收到一轮新任期的选举时,都会进行投票。

  1. 选举的任期比当前的任期大
  2. 一生只会给一个任期投一次选票
  3. 候选人的数据必须比自己新 (如果在同一索引有不同任期号,那么任期号大的比较新,后面会讲这个怎么判断 )

问题2-2: 如何确保主节点的数据是有效的?

  • 数据被提交前,至少需要超过半数的ack,即一半以上节点有已提交的数据
  • 3说明了,如果想赢得选举,要比半数以上节点的数据新(相同)
  • 以上两点取交集:赢得选举的节点,必然包括最新的已提交数据。

问题3: 有没有可能出现脑裂?

问题3-1: 有没有可能同时出现多个主节点?

问题3-2: 多个主节点是否可能同时提供服务?

  • 新的任期开始后,所有节点会屏蔽掉比当前任期小的请求和心跳。
  • 由于超过半数的节点已经进入新一轮的任期,旧leader不再可能获得半数以上的ack。
  • 旧leader一旦收到term更高的心跳,则直接降级为从节点。

领导选举 – 半数投票选举

问题3-3: 当多个节点同时参选,会不会发生脑裂?

  • 半数投票选举:由于一个任期需要有半数以上的节点投票同意,因此不会出现脑裂。

领导选举 – 分票导致选举失败

问题4: 有没有可能出现无法选出合适的主节点?

  • 分票导致选举失败:但是当多个候选人同时竞选时,有可能导致每个候选人都得不到超过半数的票,这个任期则会流产。
  • 随机超时:通过随机的超时时间,避免下一次选举冲突。
  • 当某个候选人再次超时后,会把自己的任期再+1,然后发起一轮新的选举。
  • 赢者通吃:任何节点收到较高任期(Term)的心跳后,都会退化为从节点。

领导选举 – 预选举

这是一个优化手段,从一致性来讲,并不是必要的手段。

  • 当网络发生异常,但节点没有发生异常时,可能会导致某些节点的任期无限增加。
  • Raft采取预选举(preVote)的方式避免。
  • 节点在发起选举前,会先发起一轮预选举。Candidate首先要确认自己要能赢得集群中大多数节点的投票,这样才会把自己的term增加,然后发起正式的选举,如果预选举不通过,则该节点的term不会增加。

选举流程总结

选举要花多久时间(了解就行)

问题5-1: 假设不会发生分票,服务不可用的时间?

问题5-2: 发生分票的概率?

成员变更

  • 在生产环境中,有时需要改变集群配置,比如更换坏掉的节点、增加冗余。
  • 需要在保证安全性性的前提下完成变更。不能在同一 term 有多个 leader。
  • 同时也希望升级不停机,能对外提供服务。
  • 如果贸然直接加入多个节点,势必会导致多个Leader节点的情况

图中本来3个节点,贸然加入2个新节点,然后发生了网络隔离。右上边两个节点以为集群中共有3个节点,获得投票数为2就可以产生新的ld。左下3个节点知道集群中共有5个节点,因此需要3票可以选举出新的leader。这样就会脑裂,选出两个主节点。

那么怎么办?

成员变更 – 一次只变化一个节点

比如,(a)图集群本来有4个节点。一次增加一个节点,不会造成脑裂。

  • 添加节点也是一种日志,按照多数通过的方式添加日志。
  • 超过半数节点同意后,新节点即加入集群。
  • 之后便可以开启新的一轮添加节点。

  • 新增节点由于没有任何日志,无法直接参与新日志的追加(这是因为新节点要从leader节点处补足之前缺失的日志,才可以追加新的日志),会导致新集群可用性变差。
  • 集群可用性变差的原因是,原来3个节点,可以挂一个,因为3/2集群至少需要2个节点。现在4个节点,4/2集群至少需要3个节点,但是新节点不可用,所以现在一个节点都不能挂。因此集群可用性变差。
  • 可以引入Learner身份,在没有投票权的情况下先从Leader节点中获取一段时间的日志。

成员变更 – 多个节点添加 – 先问是不是

使用日志机制同步节点配置(比如图中同步新增节点配置)后,真的会导致脑裂吗?

回顾一下节点处理请求,提交日志的流程:

假设下图中的B和上图中的C一样处于未被告知主节点提交的状态。

那么问题来了:像B这样的节点,当“我的”成员变化日志处于未提交时,我是否能使用未提交的配置?

  1. 不能使用未提交配置,B认为集群节点有3个。这样显然会造成脑裂!
  2. 可以使用未提交配置,B认为集群节点有5个,认可新成员会有问题么?(也会有脑裂,往下看)

我们讨论的节点数量太少了。下图主节点A未提交。如果此时,D拉拢所有4个新节点,可以成为leader,B拉拢CE也可以成为leader。这就出现了脑裂。“未提交节点”就可以带着新势力革命!

怎么解决这个问题?

Raft论文提供了两段提交(2-pahse approach) 的方式解决。

  • 先提交一次 ‘C_old->C_new’的日志,节点收到后无需Commit立即生效。 但是要求选举必须同时得到新集群和旧集群的大多数选票。(上面例子里面,新集群拿到5张票,旧集群拿到3张票)

  • 待‘old->new’被committed后,再发起一次’C_new’的日志,旧集群配置彻底失效。

日志复制

基本概念

  • Raft数据包含日志序列和数据状态机。
  • 日志本质上就是一个数组,内部存了一条条操作。
  • 任意一个节点按序执行日志里的操作可以都可以还原相同状态机结果。
  • Leader产生日志,同步到Follower节点中。Follower按序追加到自己的日志队列中执行。

由于一个Term只会有一个Leader, 一个Leader只会在一个位置放一次日志。

因此使用 索引 + 任期 可以唯一标识一条日志。

问题0: 一次正常的日志同步流程。

  • 主节点在收到新的请求后,先将日志追加到自己的日志中。这时日志还未提交(uncommit)
  • Master将日志同步给所有的从节点。从节点收到日志后也保存到未提交(uncommit)队列中
  • 当Master确认半数以上节点获取到日志后,则将日志提交

日志复制 – 缺少日志

问题1: 如何处理日志缺失。

  • Master节点中维护了所有从节点的下一个预期日志(next Index)
  • 子节点只会接收当前Max Index后的下一个日志,其余下标的日志全部拒绝
  • 子节点总会向Master汇报自己当前的max index

日志复制 – 日志冲突 – 未提交直接覆盖

问题2: 如何处理日志冲突? 问题2-1: 从节点如何处理?

  • 当主节点宕机时没有来得及把日志同步给半数以上的节点,就会出现数据冲突。
  • 从节点收到日志请求时,会判断未提交的日志是否发生冲突,如果发生冲突则直接截断覆盖。

当A出现网络隔离,图中E节点,通过拉拢CD可以成为leader。

日志复制 – 安全性 - 日志冲突

问题3 : 如何快速确定主从节点的日志是否冲突(/相同)呢?

日志复制 – 安全性 – 日志匹配特性

怎么证明上面2点?

证明1: 如果在不同的节点中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令

证明2: 如果在不同的节点中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。

领导选举

问题4: 如何判断数据的新旧。

  • 如果在同一索引有不同任期号,那么任期号大的比较新。

日志复制 – 主节点未提交日志处理 + 主节点追加原则

问题5: 新任Leader如何处理前任(前任指的是,不是ld时)未提交数据?

  • Leader节点只会追加自己的日志,不会删除或覆盖自己的日志(无论是否已被Commit)。

举个例子:在下图中第二个阶段,A已经向客户端提交了日志2。所以后面这个日志不应该被删除。

问题6: 新任Leader如何提交前任(前任指的是,不是ld时)未提交数据?

答案:

  • 不主动提交非自己任期的日志。
  • 只在新日志请求到来后顺便提交。

假设不知道答案,先看下面的假设:

  1. A: 由于我的数据肯定是最新的,上任后立刻全部提交。
  2. B: 先尝试同步数据,收到半数ACK后再提交。
  3. C: 前任的事情我不管,不到万不得已不提交。

假设A,会出现问题:

怎么解决这个问题?leader在提交自己任期的日志时顺便提交旧日志,E就不会当选。

日志压缩

  • 不定期将日志合并为一张快照,可以缩短日志长度,节约空间。
  • 快照保存了当时的状态机,同时也保存了最后一条日志的提交任期(Term)和索引(index)

安全性

  • 选举安全原则(Election Safety):一个任期(term)内最多允许有一个领导人被选上

  • 领导人只增加原则(Leader Append-Only):领导人永远不会覆盖或者删除自己的日志,它只会增加条目

  • 日志匹配原则(Log Matching): 如果两个日志在相同的索引位置上的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间的条目完全相同

  • 领导人完全原则(Leader Completeness): 如果一个日志条目在一个给定任期内被提交,那么这个条目一定会出现在所有任期号更大的领导人中

  • 状态机安全原则(State Machine Safety): 如果一个服务器已经将给定索引位置的日志条目应用到状态机中,则所有其他服务器不会在该索引位置应用不同的条目

Basic Paxos算法

juejin.cn/post/711160…

Basic Paxos 算法,描述的是多节点之间如何就单个值(提案 Value)达成共识。

三个角色

  • 提议者(Proposer) :提议一个值,用于投票表决。为了方便演示,你可以把图 1 中的客户端 1 和 2 看作是提议者。但在绝大多数场景中,集群中收到客户端请求的节点,才是提议者(图 1 这个架构,是为了方便演示算法原理)。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑,就可以像使用数据库一样访问后端的数据。
  • 接受者(Acceptor):对每个提议的值进行投票,并存储接受的值,比如 A、B、C 三个节点。 一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
  • 学习者( Learner :被告知投票的结果,接受达成共识的值,存储保存,不参与投票的过程。一般来说,学习者是数据备份节点,比如“Master-Slave”模型中的 Slave,被动地接受数据,容灾备份。

提议者代表的是接入和协调功能,收到客户端请求后,发起二阶段提交,进行共识协商;

接受者代表投票协商和存储数据,对提议的值进行投票,并接受达成共识的值,存储保存;

学习者代表存储数据,不参与共识协商,只接受达成共识的值,存储保存。

原理

我使用[n, v]表示一个提案,其中 n 为提案编号,v 为提议值。

Paxos 算法包括“准备(Prepare)”和“批准(Accept)”两个阶段。

准备阶段

第一阶段“准备”(Prepare)就相当于抢占锁的过程。如果某个提案节点准备发起提案,必须先向所有的决策节点广播一个许可申请(称为 Prepare 请求)。提案节点的 Prepare 请求中会附带一个全局唯一的数字 n 作为提案 ID。

决策节点收到后,会给提案节点两个承诺一个应答:

两个承诺:

  1. 承诺不会再接受提案 ID 小于或等于 n 的 Prepare 请求;
  2. 承诺不会再接受提案 ID 小于 n 的 Accept 请求。

一个应答:

  1. 在不违背以前作出的承诺的前提下,回复已经批准过的提案中 ID 最大的那个提案所设定的值和提案 ID,如果该值从来没有被任何提案设定过,则返回空值。如果违反此前做出的承诺,也就是说收到的提案 ID 并不是决策节点收到过的最大的,那就可以直接不理会这个 Prepare 请求。

批准阶段

当提案节点收到了多数派决策节点的应答(称为 Promise 应答)后,可以开始第二阶段“批准”(Accept)过程。这时有两种可能的结果:

  • 如果提案节点发现所有响应的决策节点此前都没有批准过这个值(即为空),就说明它是第一个设置值的节点,可以随意地决定要设定的值;并将自己选定的值与提案 ID,构成一个二元组 (id, value),再次广播给全部的决策节点(称为 Accept 请求)。
  • 如果提案节点发现响应的决策节点中,已经有至少一个节点的应答中包含有值了,那它就不能够随意取值了,必须无条件地从应答中找出提案 ID 最大的那个值并接受,构成一个二元组 (id, maxAcceptValue),然后再次广播给全部的决策节点(称为 Accept 请求)。

当每一个决策节点收到 Accept 请求时,都会在不违背以前作出的承诺的前提下,接收并持久化当前提案 ID 和提案附带的值。如果违反此前做出的承诺,即收到的提案 ID 并不是决策节点收到过的最大的,那允许直接对此 Accept 请求不予理会。

当提案节点收到了多数派决策节点的应答(称为 Accepted 应答)后,协商结束,共识决议形成,将形成的决议发送给所有记录节点进行学习。整个过程的时序图如下所示

例子

在这个例子中,我们只讨论正常通讯的场景,不会涉及网络分区。

假设一个分布式系统有五个节点,分别是 S1、S2、S3、S4 和 S5;全部节点都同时扮演着提案节点和决策节点的角色。此时,有两个并发的请求希望将同一个值分别设定为 X(由 S1 作为提案节点提出)和 Y(由 S5 作为提案节点提出);我们用 P 代表准备阶段、用 A 代表批准阶段,这时候可能发生下面四种情况。

情况一

比如,S1 选定的提案 ID 是 3.1(全局唯一 ID 加上节点编号),先取得了多数派决策节点的 Promise 和 Accepted 应答;此时 S5 选定的提案 ID 是 4.5,发起 Prepare 请求,收到的多数派应答中至少会包含 1 个此前应答过 S1 的决策节点,假设是 S3。

那么,S3 提供的 Promise 中,必将包含 S1 已设定好的值 X,S5 就必须无条件地用 X 代替 Y 作为自己提案的值。由此,整个系统对“取值为 X”这个事实达成了一致。如下图所示:

情况二

事实上,对于情况一,X 被选定为最终值是必然结果。但从图中可以看出,X 被选定为最终值并不是一定要多数派的共同批准,而只取决于 S5 提案时 Promise 应答中是否已经包含了批准过 X 的决策节点。

比如下图所示,S5 发起提案的 Prepare 请求时,X 并未获得多数派批准,但由于 S3 已经批准的关系,最终共识的结果仍然是 X。

情况三

当然,另外一种可能的结果是,S5 提案时 Promise 应答中并未包含批准过 X 的决策节点。

比如,应答 S5 提案时,节点 S1 已经批准了 X,节点 S2、S3 未批准但返回了 Promise 应答,此时 S5 以更大的提案 ID 获得了 S3、S4 和 S5 的 Promise。这三个节点均未批准过任何值,那么 S3 将不会再接受来自 S1 的 Accept 请求,因为它的提案 ID 已经不是最大的了。所以,这三个节点将批准 Y 的取值,整个系统最终会对“取值为 Y”达成一致。

情况四

从情况三可以推导出另一种极端的情况,如果两个提案节点交替使用更大的提案 ID 使得准备阶段成功,但是批准阶段失败的话,这个过程理论上可以无限持续下去,形成活锁(Live Lock)。在算法实现中,会引入随机超时时间来避免活锁的产生。

优点&缺点

优点:

  • Paxos算法有严格的数学证明,系统设计精妙。

缺点:

一般不会直接用于实践:

  • Basic Paxos 只能对单个值形成决议。
  • 决议的形成至少需要2 轮 RPC 通讯(准备阶段prepare和批准阶段accept)往返消息多、耗性能、延迟大,高并发情况下将产生较大的网络开销。
  • 如果多个提议者同时提交提案,就可能发生因为没有提议者接收大多数响应,而准备失败,需要重新协商。
  • 极端情况 (两个提案节点交替使用更大的提案 ID ) 下甚至可能形成活锁。

Multi Paxos算法

兰伯特提到的 Multi-Paxos 是一种思想,不是算法。而 Multi-Paxos 算法是一个统称,它是指基于 Multi-Paxos 思想,通过多个 Basic Paxos 实例实现一系列值的共识的算法(比如 Chubby 的 Multi-Paxos 实现、Raft 算法等)。

兰伯特关于 Multi-Paxos 的思考

回顾 Basic paxos 算法:

而如果我们直接通过多次执行 Basic Paxos 实例,来实现一系列值的共识,就会存在这样几个问题:

  1. 如果多个提议者同时提交提案,可能出现因为提案编号冲突,在准备阶段没有提议者接收到大多数准备响应,协商失败,需要重新协商。你想象一下,一个 5 节点的集群,如果 3 个节点作为提议者同时提案,就可能发生因为没有提议者接收大多数响应(比如 1 个提议者接收到 1 个准备响应,另外 2 个提议者分别接收到 2 个准备响应)而准备失败,需要重新协商。
  2. 2 轮 RPC 通讯(准备阶段和接受阶段)往返消息多、耗性能、延迟大。你要知道,分布式系统的运行是建立在 RPC 通讯的基础之上的,因此,延迟一直是分布式系统的痛点,是需要我们在开发分布式系统时认真考虑和优化的。

那么如何解决上面的 2 个问题呢?可以通过引入领导者和优化 Basic Paxos 执行来解决。

领导者

我们可以通过引入领导者节点,也就是说,领导者节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了:

在这里,我补充一点:在论文中,兰伯特没有说如何选举领导者,需要我们在实现 Multi-Paxos 算法的时候自己实现。 比如在 Chubby 中,主节点(也就是领导者节点)是通过执行 Basic Paxos 算法,进行投票选举产生的。

优化 Basic Paxos 执行

我们可以采用“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制,优化 Basic Paxos 执行。也就是说,领导者节点上,序列中的命令是最新的,不再需要通过准备请求来发现之前被大多数节点通过的提案,领导者可以独立指定提案中的值。这时,领导者在提交命令时,可以省掉准备阶段,直接进入到接受阶段:

Chubby 的 Multi-Paxos 实现

既然兰伯特只是大概的介绍了 Multi-Paxos 思想,那么 Chubby 是如何补充细节,实现 Multi-Paxos 算法的呢?

  1. 首先,它通过引入主节点,实现了兰伯特提到的领导者(Leader)节点的特性。也就是说,主节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了。
  2. 另外,在 Chubby 中,主节点是通过执行 Basic Paxos 算法,进行投票选举产生的,并且在运行过程中,主节点会通过不断续租的方式来延长租期(Lease)。比如在实际场景中,几天内都是同一个节点作为主节点。如果主节点故障了,那么其他的节点又会投票选举出新的主节点,也就是说主节点是一直存在的,而且是唯一的。
  3. 其次,在 Chubby 中实现了兰伯特提到的,“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制。
  4. 最后,在 Chubby 中,实现了成员变更(Group membership),以此保证节点变更的时候集群的平稳运行。
  5. 最后,我想补充一点:在 Chubby 中,为了实现了强一致性,读操作也只能在主节点上执行。 也就是说,只要数据写入成功,之后所有的客户端读到的数据都是一致的。具体的过程,就是下面的样子:
  • 所有的读请求和写请求都由主节点来处理。当主节点从客户端接收到写请求后,作为提议者,执行 Basic Paxos 实例,将数据发送给所有的节点,并且在大多数的服务器接受了这个写请求之后,再响应给客户端成功:

  • 当主节点接收到读请求后,处理就比较简单了,主节点只需要查询本地数据,然后返回给客户端就可以了:

总结

  1. 兰伯特提到的 Multi-Paxos 是一种思想,不是算法,而且还缺少算法过程的细节和编程所必须的细节,比如如何选举领导者等,这也就导致了每个人实现的 Multi-Paxos 都不一样。而 Multi-Paxos 算法是一个统称,它是指基于 Multi-Paxos 思想,通过多个 Basic Paxos 实例实现一系列数据的共识的算法(比如 Chubby 的 Multi-Paxos 实现、Raft 算法等)。
  2. Chubby 实现了主节点(也就是兰伯特提到的领导者),也实现了兰伯特提到的 “当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段” 这个优化机制,省掉 Basic Paxos 的准备阶段,提升了数据的提交效率,但是所有写请求都在主节点处理,限制了集群处理写请求的并发能力,约等于单机。
  3. 因为在 Chubby 的 Multi-Paxos 实现中,也约定了“大多数原则”,也就是说,只要大多数节点正常运行时,集群就能正常工作,所以 Chubby 能容错(n - 1)/2 个节点的故障。
  4. 本质上而言,“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制,是通过减少非必须的协商步骤来提升性能的。这种方法非常常用,也很有效。比如,Google 设计的 QUIC 协议,是通过减少 TCP、TLS 的协商步骤,优化 HTTPS 性能。我希望你能掌握这种性能优化思路,后续在需要时,可以通过减少非必须的步骤,优化系统性能。

但我想强调,Basic Paxos 是经过证明的,而 Multi-Paxos 是一种思想,缺失实现算法的必须编程细节,这就导致,Multi-Paxos 的最终算法实现,是建立在一个未经证明的基础之上的,正确性是个问号。

优点&缺点

优点:

  • 引入领导者,领导者节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了(解决base paxos这个问题)。领导者是通过执行 Basic Paxos 算法,进行投票选举产生的。
  • 领导者在提交命令时,可以省掉准备阶段,直接进入到接受阶段(解决base paxos 两轮rpc问题)。领导者节点上,序列中的命令是最新的,不再需要通过准备请求来发现之前被大多数节点通过的提案,领导者可以独立指定提案中的值。

缺点:

  • 省掉 Basic Paxos 的准备阶段,提升了数据的提交效率,但是所有写请求都在主节点处理,限制了集群处理写请求的并发能力,约等于单机。

  • Multi-Paxos 是一种思想(raft zab都是其中一种),缺失实现算法的必须编程细节,这就导致,Multi-Paxos 的最终算法实现,是建立在一个未经证明的基础之上的,正确性是个问号。

  • 兰伯特的 Multi-Paxos,虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么,也无法保证各值(也就是操作)的顺序性。

ZAB算法

实现操作的顺序性(顺序一致性)

为什么zookeeper使用zab算法?

兰伯特的 Multi-Paxos,虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么,也无法保证各值(也就是操作)的顺序性。而这就是 Zookeeper 没有采用 Multi-Paxos 的原因,又是 ZAB 协议着力解决的,也是你理解 ZAB 协议的关键。

ZAB 协议的最核心设计目标:如何实现操作的顺序性。

假如节点 A、B、C 组成的一个分布式集群,我们要设计一个算法,来保证指令(比如 X、Y)执行的顺序性,比如,指令 X 在指令 Y 之前执行,那么我们该如何设计这个算法呢?

为什么 Multi-Paxos 无法保证操作顺序性?

我们假设当前所有节点上的被选定指令,最大序号都为 100,那么新提议的指令对应的序号就会是 101。

首先节点 A 是领导者,提案编号为 1,提议了指令 X、Y,对应的序号分别为 101 和 102(一个提案包含两个指令),但是因为网络故障,指令只成功复制到了节点 A。

图中b没有回复批准请求:

假设这时节点 A 故障了,新当选的领导者为节点 B。节点 B 当选领导者后,需要先作为学习者了解目前已被选定的指令。节点 B 学习之后,发现当前被选定指令的最大序号为 100(因为节点 A 故障了,它被选定指令的最大序号 102,无法被节点 B 发现),那么它可以从序号 101 开始提议新的指令。这时它接收到客户端请求,并提议了指令 Z,指令 Z 被成功复制到节点 B、C。

假设这时节点 B 故障了,节点 A 恢复了,选举出领导者 C 后,节点 B 故障也恢复了。节点 C 当选领导者后,需要先作为学习者了解目前已被选定的指令,这时它执行 Basic Paxos 的准备阶段,就会发现之前选定的值(比如 Z、Y),然后发送接受请求,最终在序号 101、102 处达成共识的指令是 Z、Y。就像下图的样子。

在这里,你可以看到,原本预期的指令是 X、Y,最后变成了 Z、Y。

这个过程,其实很明显的验证了“Multi-Paxos 虽然能保证达成共识后的值不再改变,但它不关心达成共识的值是什么。”

假设在 ZooKeeper 中直接使用了兰伯特的 Multi-Paxos,这时创建节点"/geekbang"和"/geekbang/time",那么就可能出现,系统先创建了节点"/geekbang/time",这样肯定就出错了,因为创建节点"/geekbang/time"时,找不到节点"/geekbang",所以就会创建失败:

[zk: 192.168.0.10:2181(CONNECTED) 0] create /geekbang/time 456
Node does not exist: /geekbang/time

ZAB 是如何实现操作的顺序性的?

ZAB 是通过“一切以领导者为准”的强领导者模型和严格按照顺序处理(基于事务标识符唯一单增性)、提交提案,来实现操作的顺序性的。

ZAB 协议到底是什么:能保证操作顺序性的,基于主备模式的原子广播协议。

首先,需要你注意的是,在 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。

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

这里有一些自己的思考:为什么事务标识符可以保证顺序? multi paxos 的提案编号 -> zab的事务标识符

在multi paxos里面,XY的提案编号是一样的,但是在zab里面XY的事务标识符是不会一样的,且Y一定比X的大。因X的提案编号比Z的小,序号和Z一样,因此会被Z覆盖。但是XYZ的事务标识符是单增的,因此是不会被覆盖的。

在创建完提案之后,主节点会基于 TCP 协议(如果 ZAB 采用的是 UDP 协议,无法保证消息接收的顺序性,主要是因为 TCP 协议本身支持按序确认,而 UCP 只能支持尽最大可能交付),并按照顺序将提案广播到其他节点。这样就能保证先发送的消息,会先被收到,保证了消息接收的顺序性。

你看这张图,X 一定在 Y 之前到达节点 B、C:

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

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

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

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

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

领导者选举

快速领导者选举(Fast Leader Election)。

ZAB 如何选举领导者?

ZAB 支持 3 种成员身份(领导者、跟随者、观察者)。

  • 领导者(Leader): 作为主(Primary)节点,在同一时间集群只会有一个领导者。需要你注意的是,所有的写请求都必须在领导者节点上执行。
  • 跟随者(Follower):作为备份(Backup)节点, 集群可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票。需要你注意的是,跟随者可以直接处理并响应来自客户端的读请求,但对于写请求,跟随者需要将它转发给领导者处理。
  • 观察者(Observer):作为备份(Backup)节点,类似跟随者,但是没有投票权,也就是说,观察者不参与领导者选举和提案提交的投票。你可以对比着 Paxos 中的学习者来理解。

定义了 4 种成员状态。

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

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

如何选举?

假设投票信息的格式是<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 而言,它提议的领导者是 B,选票(<3, 1, 102, C>)提议的领导者是 C,因为节点 C 的任期编号与节点 B 相同,但节点 C 的事务标识符的最大值比节点 B 的大,那么,按照约定的规则,相比节点 B,节点 C 应该作为领导者,所以,节点 B 除了接收和保存选票信息,还会更新自己的选票为 <3, 1, 102, B>,也就是推荐 C 作为领导者,并将选票重新发送给节点 B、C:

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

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

在这里,我想强调的是,领导者选举的目标,是从大多数节点中选举出数据最完整的节点,也就是大多数节点中,事务标识符值最大的节点。另外,ZAB 本质上是通过“见贤思齐,相互推荐”的方式来选举领导者的。也就说,根据领导者 PK,节点会重新推荐更合适的领导者,最终选举出了大多数节点中数据最完整的节点。

故障恢复

我们上一讲提到了 ZAB 的领导者选举,在我看来,它只是选举了一个适合当领导者的节点,然后把这个节点的状态设置成 LEADING 状态。此时,这个节点还不能作为主节点处理写请求,也不能使用领导职能(比如,它没办法阻止其他“领导者”广播提案)。

集群怎么从故障中恢复过来?成员发现 + 数据同步 会解决这个问题。

成员发现和数据同步不仅让新领导者正式成为领导者,确立了它的领导关系,还解决了各副本的数据冲突,实现了数据副本的一致性。这样一来,集群就能正常处理写请求了。在这句话里:

  • 确立领导关系,也就是在成员发现(DISCOVERY)阶段,领导者和大多数跟随者建立连接,并再次确认各节点对自己当选领导者没有异议,确立自己的领导关系;
  • 处理冲突数据,也就是在数据同步(SYNCHRONIZATION)阶段,领导者以自己的数据为准,解决各节点数据副本的不一致。

成员发现阶段:如何确立领导关系?

在当选后,领导者会递增自己的任期编号,并基于任期编号值的大小,来和跟随者协商,最终建立领导关系。具体说的话,就是跟随者会选择任期编号值最大的节点,作为自己的领导者,而被大多数节点认同的领导者,将成为真正的领导者。

假设一个 ZooKeeper 集群,由节点 A、B、C 组成。其中,领导者 A 已经宕机,C 是新选出来的领导者,B 是新的跟随者(为了方便演示,假设 B、C 已提交提案的事务标识符最大值分别是 <1, 10> 和 <1, 11>,其中 1 是任期编号,10、11 是事务标识符中的计数器值,A 宕机前的任期编号也是 1)。那么 B、C 如何协商建立领导关系呢?

首先,B、C 会把自己的 ZAB 状态设置为成员发现(DISCOVERY),这就表明,选举(ELECTION)阶段结束了,进入了下一个阶段:

在这里,我想补充一下,ZAB 定义了 4 种状态,来标识节点的运行状态。

  • ELECTION(选举状态):表明节点在进行领导者选举;
  • DISCOVERY(成员发现状态):表明节点在协商沟通领导者的合法性;
  • SYNCHRONIZATION(数据同步状态):表明集群的各节点以领导者的数据为准,修复数据副本的一致性;
  • BROADCAST(广播状态):表明集群各节点在正常处理写请求。

只有当集群大多数节点处于广播状态的时候,集群才能提交提案。

接下来,B 会主动联系 C,发送给它包含自己接收过的领导者任期编号最大值(也就是前领导者 A 的任期编号,1)的 FOLLOWINFO 消息。

当 C 接收来自 B 的信息时,它会将包含自己事务标识符最大值的 LEADINFO 消息发给跟随者。

你要注意,领导者进入到成员发现阶段后,会对任期编号加 1,创建新的任期编号,然后基于新任期编号,创建新的事务标识符(也就是 <2, 0>)。

当接收到领导者的响应后,跟随者会判断领导者的任期编号是否最新,如果不是,就发起新的选举;如果是,跟随者返回 ACKEPOCH 消息给领导者。在这里,C 的任期编号(也就是 2)大于 B 接受过的其他领导任期编号(也就是旧领导者 A 的任期编号,1),所以 B 返回确认响应给 C,并设置 ZAB 状态为数据同步。

最后,当领导者接收到来自大多数节点的 ACKEPOCH 消息时,就设置 ZAB 状态为数据同步。在这里,C 接收到了 B 的消息,再加上 C 自己,就是大多数了,所以,在接收到来自 B 的消息后,C 设置 ZAB 状态为数据同步。

现在,ZAB 在成员发现阶段确立了领导者的领导关系,之后领导者就可以行使领导职能了。而这时它首先要解决的就是数据冲突,实现各节点数据的一致性,那么它是怎么做的呢?

数据同步阶段:如何处理冲突数据?

当进入到数据同步状态后,领导者会根据跟随者的事务标识符最大值,判断以哪种方式处理不一致数据(有 DIFF、TRUNC、SNAP 这 3 种方式,后面我会具体说一说)。

因为 C 已提交提案的事务标识符最大值(也就是 <1, 11>)大于 B 已提交提案的事务标识符最大值(也就是 <1, 10>),所以 C 会用 DIFF 的方式修复数据副本的不一致,并返回差异数据(也就是事务标识符为 <1, 11> 的提案)和 NEWLEADER 消息给 B。

在这里,我想强调一点:B 已提交提案的最大值,也是它最新提案的最大值。因为在 ZooKeeper 实现中,节点退出跟随者状态时(也就是在进入选举前),所有未提交的提案都会被提交。这是 ZooKeeper 的设计,你知道有这么个事就可以了。

然后,B 修复不一致数据,返回 NEWLEADER 消息的确认响应给领导者。

接着,当领导者接收到来自大多数节点的 NEWLEADER 消息的确认响应,将设置 ZAB 状态为广播。在这里,C 接收到 B 的确认响应,加上 C 自己,就是大多数确认了。所以,在接收到来自 B 的确认响应后,C 设置自己的 ZAB 状态为广播,并发送 UPTODATE 消息给所有跟随者,通知它们数据同步已经完成了。

最后当 B 接收到 UPTODATE 消息时,它就知道数据同步完成了,就设置 ZAB 状态为广播。

这个时候,集群就可以正常处理写请求了。

在这里,需要你了解领导者向跟随者同步数据的三种方式(TRUNC、DIFF、SNAP),它们是什么含义呢?要想了解这部分内容,你首先要了解一下 syncFollower() 中,3 个关键变量的含义

  • peerLastZxid:跟随者节点上,提案的事务标识符最大值。
  • maxCommittedLog、minCommittedLog:领导者节点内存队列中,已提交提案的事务标识符最大值和最小值。需要你注意的是,maxCommittedLog、minCommittedLog 与 ZooKeeper 的设计有关。在 ZooKeeper 中,为了更高效地复制提案到跟随者上,领导者会将一定数量(默认值为 500)的已提交提案放在内存队列里,而 maxCommittedLog、minCommittedLog 分别标识的是内存队列中,已提交提案的事务标识符最大值和最小值。

说完 3 个变量的含义,我来说说 3 种同步方式。

  • TRUNC:当 peerLastZxid 大于 maxCommittedLog 时,领导者会通知跟随者丢弃超出的那部分提案。比如,如果跟随者的 peerLastZxid 为 11,领导者的 maxCommittedLog 为 10,那么领导者将通知跟随者丢弃事务标识符值为 11 的提案。
  • DIFF:当 peerLastZxid 小于 maxCommittedLog,但 peerLastZxid 大于 minCommittedLog 时,领导者会同步给跟随者缺失的已提交的提案,比如,如果跟随者的 peerLastZxid 为 9,领导者的 maxCommittedLog 为 10,minCommittedLog 为 9,那么领导者将同步事务标识符值为 10 的提案,给跟随者。
  • SNAP:当 peerLastZxid 小于 minCommittedLog 时,也就是说,跟随者缺失的提案比较多,那么,领导者同步快照数据给跟随者,并直接覆盖跟随者本地的数据。

领导者以自己的数据为准,实现各节点数据副本的一致的。

stackoverflow.com/questions/6…

在 ZooKeeper 中,一个提案进入提交(Committed)状态,有两种方式:

  • 被复制到大多数节点上,被领导者提交或接收到来自领导者的提交消息(leader.COMMIT)而被提交。在这种状态下,提交的提案是不会改变的。
  • 另外,在 ZooKeeper 的设计中,在节点退出跟随者状态时(在 follower.shutdown() 函数中),会将所有本地未提交的提案都提交。需要你注意的是,此时提交的提案,可能并未被复制到大多数节点上,而且这种设计,就会导致 ZooKeeper 中出现,处于“提交”状态的提案可能会被删除(也就是接收到领导者的 TRUNC 消息而删除的提案)。

在 ZooKeeper 中,被复制到大多数节点上的提案,最终会被提交,并不会再改变;而只在少数节点存在的提案,可能会被提交和不再改变,也可能会被删除。例子:

  • 如果写请求对应的提案“SET X = 1”已经复制到大多数节点上,那么它是最终会被提交,之后也不会再改变。也就是说,在没有新的 X 赋值操作的前提下,不管节点怎么崩溃、领导者如何变更,你查询到的 X 的值都为 1。
  • 如果写请求对应的提案“SET X = 1”未被复制到大多数节点上,比如在领导者广播消息过程中,领导者崩溃了,那么,提案“SET X = 1”,可能被复制到大多数节点上,并提交和之后就不再改变,也可能会被删除。这个行为是未确定的,取决于新的领导者是否包含该提案。

这里有个问题啊:zk的读官网上说是顺序一致性。

zk这个commit 的特性(退出跟随者时commit),会不会影响顺序一致性?如果这个时候commit的提案被读了,后面又被删了?那这样就是最终一致性了。

到底是什么?

处理读写请求

ZooKeeper 处理读写请求的原理

在 ZooKeeper 中,与领导者“失联”的节点,是不能处理读写请求的。比如,如果一个跟随者与领导者的连接发生了读超时,设置了自己的状态为 LOOKING,那么此时它既不能转发写请求给领导者处理,也不能处理读请求,只有当它“找到”领导者后,才能处理读写请求。

举个例子:当发生分区故障了,C 与 A(领导者)、B 网络不通了,那么 C 将设置自己的状态为 LOOKING,此时在 C 节点上既不能执行读操作,也不能执行写操作。

其次,当大多数节点进入到广播阶段的时候,领导者才能提交提案,因为提案提交,需要来自大多数节点的确认。

最后,写请求只能在领导者节点上处理,所以 ZooKeeper 集群写性能约等于单机。而读请求是可以在所有的节点上处理的,所以,读性能是能水平扩展的。也就是说,你可以通过分集群的方式来突破写性能的限制,并通过增加更多节点,来扩展集群的读性能。

如何实现写操作?

如何实现读操作?

相比写操作,读操作的处理要简单很多,因为接收到读请求的节点,只需要查询本地数据,然后响应数据给客户端就可以了。

ZAB与Raft的对比

  • 领导者选举:ZAB 采用的“见贤思齐、相互推荐”的快速领导者选举(Fast Leader Election),Raft 采用的是“一张选票、先到先得”的自定义算法。在我看来,Raft 的领导者选举,需要通讯的消息数更少,选举也更快。
  • 日志复制:Raft 和 ZAB 相同,都是以领导者的日志为准来实现日志一致,而且日志必须是连续的,也必须按照顺序提交。
  • 读操作和一致性:ZAB 的设计目标是操作的顺序性,在 ZooKeeper 中默认实现的是最终一致性,读操作可以在任何节点上执行;而 Raft 的设计目标是强一致性(也就是线性一致性),所以 Raft 更灵活,Raft 系统既可以提供强一致性,也可以提供最终一致性。
  • 写操作:Raft 和 ZAB 相同,写操作都必须在领导者节点上处理。
  • 成员变更:Raft 和 ZAB 都支持成员变更,其中 ZAB 以动态配置(dynamic configuration)的方式实现的。那么当你在节点变更时,不需要重启机器,集群是一直运行的,服务也不会中断。
  • 其他:相比 ZAB,Raft 的设计更为简洁,比如 Raft 没有引入类似 ZAB 的成员发现和数据同步阶段,而是当节点发起选举时,递增任期编号,在选举结束后,广播心跳,直接建立领导者关系,然后向各节点同步日志,来实现数据副本的一致性。在我看来,ZAB 的成员发现,可以和领导者选举合到一起,类似 Raft,在领导者选举结束后,直接建立领导者关系,而不是再引入一个新的阶段;数据同步阶段,是一个冗余的设计,可以去除的,因为 ZAB 不是必须要先实现数据副本的一致性,才可以处理写请求,而且这个设计是没有额外的意义和价值的。