Paxos算法、Raft算法、ZAB原子广播协议、gossip协议的描述与对比

这里我写一篇记录这几个分布式系统选举或传播的算法和协议的简单对比,用以帮助记忆。因为后面的几个或多或少的都从分布式选举算法的始祖Paxos算法而来。由于分布式选举算法非常难理解,作者本人也并非CS专业,理解难免有偏差,如果要详细的了解建议查看其他文章[1-5]。这篇文章引用了很多其他文章,并在最后总结一下几个算法和协议的不同。我们先简单总结一下Paxos算法。

一、Paxos算法

1,basic paxos算法和multi paxos思想

paxos算法非常难以理解,具体了解paxos算法,建议看这篇文章[1]。

paxos算法是1990年一个叫兰伯特的大佬提出的,据说当时能看懂的人也不是很多,paxos算法分为basic paxos算法和multi paxos思想。basic paxos算法描述的是多节点之间如何就某个值(提案 Value)达成共识,multi paxos思想描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。Multi-Paxos 说白了就是执行多次 Basic Paxos ,核心还是 Basic Paxos [1]。muti paxos思想兰伯特自己也没有实现,仅仅停留在思想层面。

paxos算法的前提是没有拜占庭将军问题[2][注1],即不存在恶意投票的节点。

2,basic paxos算法

Basic Paxos 中存在 3 个重要的角色:提议者(Proposer):也可以叫做协调者(coordinator),提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID)提议的值 (Value)接受者(Acceptor):也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史;学习者(Learner):如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。

Paxos 里面对这两个阶段分别命名为准备(Prepare)提交(Commit)。准备阶段通过锁来解决对哪个提案内容进行确认的问题,提交阶段解决大多数确认最终值的问题[3]。

准备阶段

提案者向多个接受者发送计划提交的提案编号 n,试探是否可以锁定多数接受者的支持;
接受者 i 收到提案编号 n,检查回复过的提案的最大编号 M_i。如果 n > M_i,则向提案者返回准备接受(accept)提交的最大编号的提案 P_i(如果还未接受过任何提案,则为空),并不再接受小于 n 的提案,同时更新 M_i = n。这一步是让接受者筛选出它收到的最大编号的提案,接下来只接受其后续提交。

提交阶段

某个提案者如果收到大多数接受者的回复(表示大部分人收到了 n),则准备发出带有 n 的提交消息。如果收到的回复中带有提案 P_i(说明自己看到的信息过期),则替换选编号最大的 P_i 的值为提案值;否则指定一个新提案值。如果没收到大多数回复,则再次发出请求;
接受者 i 收到序号为 n 的提交消息,如果发现 n >= P_i 的序号,则接受提案,并更新 P_i 序号为 n。

这里需要注意的是,paxos算法解决的是多个提案者+多个接受者的情况,即有多个节点都有提出选举的权利,也有多个节点对提案做出投票。

注1:
拜占庭帝国(Byzantine Empire)军队的几个师驻扎在敌城外, 每个师都由各自的将军指挥. 将军们只能通过信使相互沟通. 在观察敌情之后, 他们必须制定一个共同的行动计划, 如进攻(Attack)或者撤退(Retreat), 且只有当半数以上的将军共同发起进攻时才能取得胜利. 然而, 其中一些将军可能是叛徒, 试图阻止忠诚的将军达成一致的行动计划. 更糟糕的是, 负责消息传递的信使也可能是叛徒, 他们可能篡改或伪造消息, 也可能使得消息丢失

二,Raft算法

Paxos 算法虽然给出了共识设计,但并没有讨论太多实现细节,也并不重视工程上的优化,因此后来在学术界和工程界出现了一些改进工作,包括 Fast PaxosMulti-PaxosZookeeper Atomic Broadcast(ZAB)Raft 等。这些算法重点在于改进执行效率和可实现性。

其中,Raft 算法由斯坦福大学的 Diego Ongaro 和 John Ousterhout 于 2014 年在论文《In Search of an Understandable Consensus Algorithm》中提出,基于 Multi-Paxos 算法进行重新简化设计和实现,提高了工程实践性。Raft 算法的主要设计思想与 ZAB 类似,通过先选出领导节点来简化流程和提高效率。实现上解耦了领导者选举、日志复制和安全方面的需求,并通过约束减少了不确定性的状态空间。

算法包括三种角色:领导者(Leader)候选者(Candidate)跟随者(Follower),每个任期内选举一个全局的领导者。领导者角色十分关键,决定日志(log)的提交。每个日志都会路由到领导者,并且只能由领导者向跟随者单向复制。

典型的过程包括两个主要阶段:

领导者选举:开始所有节点都是跟随者,在随机超时发生后未收到来自领导者或候选者消息,则转变角色为候选者(中间状态),提出选举请求。最近选举阶段(Term)中得票超过一半者被选为领导者;如果未选出,随机超时后进入新的阶段重试。领导者负责从客户端接收请求,并分发到其他节点;

同步日志:领导者会决定系统中最新的日志记录,并强制所有的跟随者来刷新到这个记录,数据的同步是单向的,确保所有节点看到的视图一致。
此外,领导者会定期向所有跟随者发送心跳消息,跟随者如果发现心跳消息超时未收到,则可以认为领导者已经下线,尝试发起新的选举过程[3]。

三,ZAB原子广播协议

ZAB协议全称 ZooKeeper Atomic Broadcast 原子消息广播协议

1,消息广播
Zookeeper集群中,存在以下三种角色的节点:

  • Leader:Zookeeper集群的核心角色,在集群启动或崩溃恢复中通过Follower参与选举产生,为客户端提供读写服务,并对事务请求进行处理。
  • Follower:Zookeeper集群的核心角色,在集群启动或崩溃恢复中参加选举,没有被选上就是这个角色,为客户端提供读取服务,也就是处理非事务请求,Follower不能处理事务请求,对于收到的事务请求会转发给Leader。
  • Observer:观察者角色,不参加选举,为客户端提供读取服务,处理非事务请求,对于收到的事务请求会转发给Leader。使用Observer的目的是为了扩展系统,提高读取性能。

1,Leader 接收到消息请求后,将消息赋予一个全局唯一的 64 位自增 id,叫做:zxid,通过 zxid 的大小比较即可实现因果有序这一特性。

2,Leader 通过先进先出队列(通过 TCP 协议来实现,以此实现了全局有序这一特性)将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。

3,当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK。

4,当 leader 接收到合法数量的 ACKs 后,leader 就向所有 follower 发送 COMMIT 命令,同时会在本地执行该消息。

5,当 follower 收到消息的 COMMIT 命令时,就会执行该消息。

在这里插入图片描述
相比于完整的二阶段提交,Zab 协议最大的区别就是不能终止事务,follower 要么回 ACK 给 leader,要么抛弃 leader,在某一时刻,leader 的状态与 follower 的状态很可能不一致,因此它不能处理 leader 挂掉的情况,所以 Zab 协议引入了恢复模式来处理这一问题。

从另一角度看,正因为 Zab 的广播过程不需要终止事务,也就是说不需要所有 follower 都返回 ACK 才能进行 COMMIT,而是只需要合法数量(2n+1 台服务器中的 n+1 台) 的follower,也提升了整体的性能。

Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。

2,崩溃恢复

崩溃恢复的主要任务就是选举Leader(Leader Election),Leader选举分两个场景:
Zookeeper服务器启动时Leader选举。
Zookeeper集群运行过程中Leader崩溃后的Leader选举。

2.1,选举参数
在介绍选举流程之前,需要介绍几个参数,

myid: 服务器ID,这个是在安装Zookeeper时配置的,myid越大,该服务器在选举中被选为Leader的优先级会越大。ZAB算法中通过myid来规避了多个节点可能有相同zxid问题,注意可以对比之前的Raft算法,Raft算法中通过随机的timeout来规避多个节点可能同时成为Leader的问题。

zxid: 事务ID,这个是由Zookeeper集群中的Leader节点进行Proposal时生成的全局唯一的事务ID,由于只有Leader才能进行Proposal,所以这个zxid很容易做到全局唯一且自增。因为Follower没有生成zxid的权限。zxid越大,表示当前节点上提交成功了最新的事务,这也是为什么在崩溃恢复的时候,需要优先考虑zxid的原因。

epoch: 投票轮次,每完成一次Leader选举的投票,当前Leader节点的epoch会增加一次。在没有Leader时,本轮此的epoch会保持不变。

另外在选举的过程中,每个节点的当前状态会在以下几种状态之中进行转变。

LOOKING: 竞选状态。
FOLLOWING: 随从状态,同步Leader 状态,参与Leader选举的投票过程。
OBSERVING: 观察状态,同步Leader 状态,不参与Leader选举的投票过程。
LEADING: 领导者状态。

2.2,选举流程

选举的流程如下:

  • 每个Server会发出一个投票,第一次都是投自己。投票信息:(myid,ZXID)
  • 收集来自各个服务器的投票
  • 处理投票并重新投票,处理逻辑:优先比较ZXID,然后比较myid
  • 统计投票,只要超过半数的机器接收到同样的投票信息,就可以确定leader
  • 改变服务器状态,进入正常的消息广播流程。

在这里插入图片描述

2.3,ZAB算法需要解决的两大问题

2.3.1,二阶段Leader宕机,被过半Follower处理的消息不能丢

这一情况会出现在以下场景:当 leader 收到合法数量 follower 的 ACKs 后,就向各个 follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。但是如果在各个 follower 在收到 COMMIT 命令前 leader 就挂了,导致剩下的服务器并没有执行都这条消息。

为了实现已经被处理的消息不能丢这个目的,Zab 的恢复模式使用了以下的策略:

选举拥有 proposal 最大值(即 zxid 最大) 的节点作为新的 leader:由于所有提案被 COMMIT 之前必须有合法数量的 follower ACK,即必须有合法数量的服务器的事务日志上有该提案的 proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被 COMMIT 的 proposal。 而在选举Leader的过程中,会比较zxid,因此选举出来的Leader必然会包含所有被COMMIT的proposal。
新的 leader 将自己事务日志中 proposal 但未 COMMIT 的消息处理。
新的 leader 与 follower 建立先进先出的队列, 先将自身有而 follower 没有的 proposal 发送给 follower,再将这些 proposal 的 COMMIT 命令发送给 follower,以保证所有的 follower 都保存了所有的 proposal、所有的 follower 都处理了所有的消息。

2.3.2,一阶段Leader宕机,被旧Leader丢弃的消息不能再次出现

这一情况会出现在以下场景:当 leader 接收到消息请求生成 proposal 后就挂了,其他 follower 并没有收到此 proposal,因此经过恢复模式重新选了 leader 后,这条消息是被跳过的。 此时,之前挂了的 leader 重新启动并注册成了 follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。

在这里插入图片描述

Zab 通过巧妙的设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。这样设计的好处是旧的 leader 挂了后重启,它不会被选举为 leader,因为此时它的 zxid 肯定小于当前的新 leader。当旧的 leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。

Zab 协议设计的优秀之处有两点,一是简化二阶段提交,提升了在正常工作情况下的性能;二是巧妙地利用率自增序列,简化了异常恢复的逻辑,也很好地保证了顺序处理这一特性[5]。

四,Gossip协议

一种比较简单粗暴的方法就是 集中式发散消息,简单来说就是一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。这种方法的缺陷也很明显,节点多的时候不光同步消息的效率低,还太依赖与中心节点,存在单点风险问题。

于是,分散式发散消息Gossip 协议 就诞生了。

Gossip 协议介绍Gossip 直译过来就是闲话、流言蜚语的意思。流言蜚语有什么特点呢?容易被传播且传播速度还快,你传我我传他,然后大家都知道了。

Gossip 协议 也叫 Epidemic 协议(流行病协议)或者 Epidemic propagation 算法(疫情传播算法),别名很多。不过,这些名字的特点都具有 随机传播特性 (联想一下病毒传播、癌细胞扩散等生活中常见的情景),这也正是 Gossip 协议最主要的特点。Gossip 协议最早是在 ACM 上的一篇 1987 年发表的论文 《Epidemic Algorithms for Replicated Database Maintenance》中被提出的。根据论文标题,我们大概就能知道 Gossip 协议当时提出的主要应用是在分布式数据库系统中各个副本节点同步数据。正如 Gossip 协议其名一样,这是一种随机且带有传染性的方式将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。在 Gossip 协议下,没有所谓的中心节点,每个节点周期性地随机找一个节点互相同步彼此的信息,理论上来说,各个节点的状态最终会保持一致。下面我们来对 Gossip 协议的定义做一个总结:Gossip 协议是一种允许在分布式系统中共享状态的去中心化通信协议,通过这种通信协议,我们可以将信息传播给网络或集群中的所有成员。

NoSQL 数据库 Redis 和 Apache Cassandra、服务网格解决方案 Consul 等知名项目都用到了 Gossip 协议,学习 Gossip 协议有助于我们搞清很多技术的底层原理。我们这里以 Redis Cluster 为例说明 Gossip 协议的实际应用。我们经常使用的分布式缓存 Redis 的官方集群解决方案(3.0 版本引入) Redis Cluster 就是基于 Gossip 协议来实现集群中各个节点数据的最终一致性。

在这里插入图片描述

Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要遵循一致的通信协议,Redis Cluster 中的各个节点基于 Gossip 协议 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。Redis Cluster 的节点之间会相互发送多种 Gossip 消息:

MEET:在 Redis Cluster 中的某个 Redis 节点上执行 CLUSTER MEET ip port 命令,可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。

PING/PONG:Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。

FAIL:Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。

……

下图就是主从架构的 Redis Cluster 的示意图,图中的虚线代表的就是各个节点之间使用 Gossip 进行通信 ,实线表示主从复制。

在这里插入图片描述

有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换[6]。

五,总结与比较

1,paxos算法

这里简单总结下,gossip协议是所有分布式共识算法(用于达成一致,也就是满足一致性)的祖宗,但是gossip协议比较复杂,它设想的是有多个节点都可以提出提案,也有多个节点都可以对提案进行投票。每个节点在发出提案时,都会有一个提案ID分配给该提案,其他节点收到提案后,会和自身记录的已接受过的上次的提案ID比较,如果这次的提案ID比上次提案更大,意味着是新的提案,对其进行投票;如果这次提案ID没上次大,意味着是旧的提案,不用管。

当发出提案的节点收到了过半节点的投票(响应里用提案ID最大的当本次提案的ID,因为别的节点可能已接受的提案ID更大, 所以它们把提案ID发过来,让发出提案节点改一下提案ID,这样它们才能正常提交),它就会发出提交请求,对数据进行提交。

这样paxos完成了基本的分布式共识的达成。

2,raft算法

而正常的工程中,分为两种情况,一种是有主的集群架构,也就是master-slave这种,一种是无主的,如Redis Cluster这种。Raft算法对paxos算法做出了简化,明确的将场景分为崩溃选举和正常工作两种场景,选举的时候没什么好说的,我们可以理解为和paxos没太大区别,本质上就是调用了一次basic paxos完成了选举相关的一次或若干次提案;而正常工作时,则不使用paxos的分布式共识算法,只通过leader来完成消息的分发和同步。

如Redis的Sentinel集群正是使用了Raft算法,Redis的Sentinel集群中的leader在选举时,会向其他sentinel发起提案,提案中runId为当前sentinel节点的runId,表示希望自己成为leader。而sentinel想要对节点进行客观下线时,也就是某个sentinel节点发现redis数据节点心跳过期(主观下线)后,询问其他节点是否也对该节点进行了主观下线,过半sentinel都主观下线,则对该节点进行客观下线,此时提案中runId为*表示该sentinel希望交换对节点下线的判定。

3,zab协议

而zab协议则是zookeeper使用的协议,也是将场景分为了崩溃选举和正常工作两种。但是相比raft算法,它能够保证消息在分布式情况下的原子性,zk中的原子性,指的是要么就完成过半节点的落盘,要么就放弃落盘,同时满足最终一致性,即返回的结果和最后过半节点的落盘结果一样。

zab协议在满足原子性的路上,有哪些挑战呢,这里我们就需要看zab 2.3部分的两个问题,即 二阶段Leader宕机,被过半Follower处理的消息不能丢一阶段Leader宕机,被旧Leader丢弃的消息不能再次出现

第一个问题靠选出事务id最大的节点为leader解决,即事务已经被过半follower处理,开始进行commit和leader的本地消息持久化,属于zk处理消息的第二阶段,有了过半的follower已经将消息持久化并返回ack,一定有follower节点持有这个未被leader持久化的消息,只需要将事务id最大的follower节点选为leader,将消息重新发给其他节点持久化,即可保证leader在二阶段宕机消息不丢失。

第二个问题,靠事务id的高32位进行纪元epoch的区分来辨识脏数据。即leader还没有收到过半follower的ack,消息还没有被过半follower处理完成,处于leader处理消息的第一阶段,此时leader宕机。新选出leader后,旧leader节点重启并注册为一个follower,此时这个新加入的follower(旧leader节点)中仍然有上个纪元中的事务,怎么识别并清除这个事务呢?依靠的是事务id。事务id共64位,高32位是纪元,低32位是自增的序列号,当选出新的leader来到新的纪元时,新的纪元会加1,新加入的follower持有的事务的id的高32位小于新的纪元,很明显,属于旧纪元的事务,且这个事务的状态是未被提交,这种事务会被清除。已提交的旧纪元的事务则不会被清除。

这样,zab就通过这两点保证了原子性,且不管是选举还是正常工作,都保证了最终的一致性。其实保证原子性有点类似于mysql用redo log和undo log共同保证了原子性一样,不管是提交时用redo log还是回滚时基于MVCC读取undo log,都有可以保证记录原子性,和zab也要考虑过半节点已提交和过半节点未提交有点类似。

4,gossip协议

至于gossip协议则没什么好说的,它本身是基于无主的架构的一种分布式情况下的传播消息的方案,只需要考虑传播消息。保证最终一致性即可,因为无主,所以也不用考虑主节点挂掉后选举的问题和主节点同步数据到一半挂掉进而保证原子性的问题。如redis cluster的节点之间传播消息就是用的gossip协议。

参考文章:
[1],Paxos 算法详解
[2],拜占庭将军问题 (The Byzantine Generals Problem)
[3],Paxos 算法与 Raft 算法
[4],ZooKeeper原理深入之ZAB原子消息广播协议
[5],一文彻底搞懂ZAB算法,看这篇就够了!!!
[6],Gossip 协议详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值