一致性协议(Consensus Protocol)是分布式系统中用于解决多个节点之间达成一致的算法或协议。它们的主要作用是确保在节点之间保持数据的一致性。
需要说明的是一致性协议和拜占庭将军问题没有直接的关联。拜占庭将军的核心问题是在缺少可信任的中央节点和可信任的通道的情况下,分布在不同地方的各个节点如何达成共识。因此拜占庭将军问题强调安全性方面,而一致性协议则更强调系统的容错性。
Paxos
Basic Paxos
Paxos算法是基于消息传递且具有高度容错特性的一致性算法。
其解决的问题就是在分布式系统中如何就某个值(决议)达成一致。
角色
提议者(Proposer)
接收客户端请求,将其封装成提案(proposal)。并将提案发给所有的接受者(acceptor)。根据接受者(acceptor)的返回情况,控制是否需要提交该提案(proposal),即是否要保存该提案。
决策者(Acceptor)
参与对提案(proposal)的投票,接收和处理Paxos的两个阶段的请求。
学习者(Learner)
不参与提案和投票,只被动接收结果。
阶段:prepare阶段、accept阶段、learn阶段
prepare阶段
由proposer向acceptor发送prepare请求,acceptor根据约定决定是否需要响应该请求。
accept阶段
proposer在prepare阶段接收到大多数响应后,由proposer向acceptor发送accept请求。
learn阶段
learn阶段,在某一个提案通过paxos达成共识后,由acceptor通知learner学习提案结果。
Raft
提案:将客户端对集群的操作称之为提案
Peer set: 所有的server节点组成一个peer set(consul有server和client模式)
Quorum: 指一个peer set里面的大多数成员,即(N/2) + 1
节点角色
Raft集群中有三个角色:
Leader: 所有请求的处理者,接收客户端发起的请求,写入本地日志后同步给其他节点。
Follower: 请求的被动更新者,从leader接收更新请求,写入本地文件。如果客户端的操作请求首先发送给了follower,会首先由follower重定向给leader。
candidate: 如果follower在一定时间内没有收到leader的心跳,则判断leader可能已经故障,此时启动leader election过程,本节点切换为candidate直到选主结束。
定时器
Raft中有两个定时器:
1. follower等待leader心跳的随机超时时间
2. candidate等待投票的随机超时时间,超时选举无效,开启新一轮选举
任期
每开始一次新的选举,称为一个任期(term),每个任期term都有一个严格递增的整数与之关联。
如果一个candidate在一个任期内赢得选举,他将在本term中担任leader的角色。
但并不是每个term都一定对应一个leader,有时候某个term内会由于选举超时导致选不出leader,这时candidate会递增term号并开始新一轮选举。
节点间通过RPC来通信,主要有两类RPC请求:
RequestVote RPCs: 用于candidate选举
AppendEntries RPCs: 用于leader向其他节点复制日志以及同步心跳
角色转换
系统启动初始状态
集群中每个节点刚启动时都是follower身份,leader会周期性的向所有节点发送心跳包来维持自己的身份。如果一个follower在一段时间内没有收到任何心跳,也就是选举超时,它就会认为系统中没有可用的leader,并发起新的选举。
每个follower都有一个随机化的定时器,让每个节点的”超时时间“在一定范围内随机生成,这样就降低了多个节点同时发起选举的可能性。
follower状态转换
1. 当candicate从整个集群的大多数(N/2 +1)节点获得针对同一任期(term)的选票时,它就赢得了这次选举,立刻将自己的身份转变为leader并开始向其他节点发送心跳来维持自己的身份。
2. 如果有多个follower同时成为candidate,没有任何一个candiate能得到大多数节点的支持,那么所有candidate都会超时。每个candidate的超时时间不同,如果candidate的随机超时时间到了,那么candidate会增加自己的term,发起新一轮选举。
3. 如果candidate在等待投票回复的时候,收到其他自称是leader的节点发送的心跳包,如果这个心跳包携带的term值不小于candidate当前的term值,那么candidate会承认这个leader,并将身份切换为follower。
leader状态转换
如果leader节点发生宕机或者网络重连,那么其他follower收不到leader的心跳,首个触发超时的节点变为candidate并开始拉票,由于该candidate的term大于原leader的term,因此所有follower都会投票给他,这名candidate成为新的leader。
一段时间后原leader恢复了,收到了新的leader的心跳包,发现心跳包中的term值大于自己的term值,此时该节点会立即切换为follower并跟随新的leader。
日志复制
共识算法保证即使在小部分(<= (N-1)/2)节点故障的情况下,系统仍然能正常对外提供服务。共识算法通常基于状态复制机(Replicated State Machine)模型,也就是所有节点从同一个state出发,经过同样的操作log,最终达到一致的state。
日志复制流程
leader接收客户端请求,把该请求封装为新的日志附加到自身的日志集合,然后向其他节点发起附加条目请求(AppendEntries RPC),来要求它们将这条日志附加到各自本地的日志集合。
当这条日志被大多数节点复制后,leader会将该日志apply到它本地的状态机中,然后把操作成功的结果返回给客户端。
接收请求 -》 添加日志 -》 日志同步 -》 提交到状态机 -》 返回客户端
在 Raft 算法中,随机超时时间是有 2 种含义的,这里是很多同学容易理解出错的地方,需要你注意一下:
1. 跟随者等待领导者心跳信息超时的时间间隔,是随机的;
2. 如果候选人在一个随机时间间隔内,没有赢得过半票数,那么选举无效了,然后候选人发起新一轮的选举,也就是说,等待选举超时的时间间隔,是随机的。
如何实现日志的一致?
在 Raft 算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致日志。也就是说,Raft 是通过以领导者的日志为准,来实现各节点日志的一致的。具体有 2 个步骤。
首先,领导者通过日志复制 RPC 的一致性检查,找到跟随者节点上,与自己相同日志项的最大索引值。也就是说,这个索引值之前的日志,领导者和跟随者是一致的,之后的日志是不一致的了。
然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。
日志复制的优化
首先,领导者进入第一阶段,通过日志复制(AppendEntries)RPC 消息,将日志项复制到集群其他节点上。
接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。
领导者不直接发送消息通知其他节点应用指定日志项。因为领导者的日志复制 RPC 消息或心跳消息,包含了当前最大的,将会被提交(Commit)的日志项索引值。所以通过日志复制 RPC 消息或心跳消息,跟随者就可以知道领导者的日志提交位置信息。
因此,当其他节点接受领导者的心跳消息,或者新的日志复制 RPC 消息后,就会将这条日志项应用到它的状态机。而这个优化,降低了处理客户端请求的延迟,将二阶段提交优化为了一段提交,降低了一半的消息延迟。
集群成员变更
联合共识
Joint Consensus 通过一个中间阶段保证每一步变更的 quorum 比如存在交集。
Raft使用一种两阶段方法平滑切换集群成员配置来避免脑裂的发生。
阶段一
1. 客户端将C-new发送给leader, leader将c-old与c-new取并集并立即apply,我们表示为c-old,new
2. leader将c-old,new包装为日志同步给其他节点
3. follower收到c-old.new后立即apply,当0old,new的大多数节点都切换后,Leader将该日志commit
阶段二
1. leader接着将c-new包装为日志同步给其他节点
2. follower收到c-new后立即apply,如果此时发现自己不在c-new列表,则主动退出集群
3. leader确认c-new的大多数节点都切换成功后,给客户端发送执行成功的响应。
https://juejin.cn/post/6907151199141625870#heading-27
单步成员变更
Join consensus 有些复杂,Raft 作者搞了个单步成员变更,就是每次只向集群中添加或移除一个节点。比如说以前集群中存在三个节点,现在需要将集群拓展为五个节点,那么就需要一个一个节点的添加,而不是一次添加两个节点。
单步成员变更能保证每一次的变更必然存在 quorum 交集。
变更流程如下:
1. Leader 提交一个成员变更请求Cnew,请求的内容为服务节点的是添加还是移除一个节点。
2. Leader 在收到请求以后,向日志中追加Cnew的 ConfChange 日志,后续这些日志会随着 AppendEntries 同步到所有的 Followers 节点中。
3. 当 ConfChange 的日志被添加到日志中是立即生效的。
4. 当 ConfChange 的日志被复制到Cnew的大多数服务器上时,那么就可以对其进行 commit。
https://www.inlighting.org/archives/raft-membership-change
ZAB
ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。它保证zookeeper集群数据的一致性和命令的全局有序性。
集群角色
Leader: 同一时间集群中只允许有一个leader,提供对客户端的读写功能,负责将数据同步给各个节点;
Follower: 提供对客户端读功能,写请求则转发给leader处理,当leader崩溃后参与leader选举;
Observer: 和follower一样,但是不同的是不参与leader选举
节点状态
following:当前节点是跟随者,服务leader节点的命令
leading: 当前节点是leader,负责协调事务
election/looking: 节点处于选举状态
代码实现中多了一种observing状态,这是zookeeper引入Observer之后加入的,Observer不参与选举是只读节点,跟ZAB协议没有关系。
ZAB协议包括两种基本模式:崩溃恢复和消息广播。
当整个服务框架在启动过程中,或是Leader服务器出现崩溃、网络中断、重启或者集群中不存在过半的服务器与leader服务器保持正常的通信等异常情况,ZAB就会进入崩溃恢复模式并选举产生新的Leader服务器。
当选举产生新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步(数据同步)之后,ZAB协议就会退出恢复模式。
之后就进入了消息广播模式。如果此时一个同样遵循ZAB协议的加入到集群时,那么如果此时已经存在一个Leader在负责消息广播,那么该服务器就会自觉进入数据恢复模式,在完成之后加入到消息广播流程中。
选举投票规则
1. 先比较epoch,epoch大的获胜
2. epoch相同则比较zxid,zxid大的获胜
3. zxid相同则比较myId,myId大的获胜。
注意:这里的zxid是机器上所保存数据的最大zxid。
Leader周期epoch,可以理解为当前集群所处的年代或者周期,每当有一个新的Leader选举出现时,就会从这个Leader服务器上去除其本地日志中最大事务的Zxid,并从中读取epoch值,然后加1,以此作为新的周期ID,高32位代表了每代Leader的唯一性,低32位代表了每代Leader中事务的唯一性。
zxid: Zab协议的一个事务编号,Zxid是一个64位的数字,其中低32位是一个简单的单调递增计数器,针对客户端每一个事务请求,计数器加1;而高32位代表Leader周期年代的编号。
接收请求写数据流程
1. leader接收客户端写请求,将写请求以提案的形式发给所有的follower并等待ack
2. follower收到leader的proposal后返回ack
3. leader得到过半数的ack后向所有的follower和observer发送commit
4. leader将处理结果返回给客户端
Gossip协议
Gossip protocol也叫Epidemic protocol(流行病协议),是基于流行病传播方式的节点或进程间信息交换的协议。
Gossip协议是基于六度分隔理论,简单来说就是一个人通过6个中间人可以认识世界上任何人。
Gossip协议在计算机系统通常以随机的”对等选择“形式实现:以给定的频率,每台计算机随机选择另一台计算机,并共享任何消息。
传播方式
Gossip协议的消息传播方式有两种:Anti-Entropy(反熵传播)和Rumor-Mongering(谣言传播)
反熵传播:每个节点周期性地随机选择其他节点,然后相互交换所有的数据来消除两者之间的差异
谣言传播:每个节点周期性选择其他其他节点,然后发送自己的增量数据
不管是Anti-Entropy还是Rumor-Mongering都涉及到节点间的数据交互方式,Gossip网络中两个节点之间存在三种通信方式:push, pull和push&pull。
push: 发起信息交换的节点A随机选择节点B,向其发送自己的信息,节点B在收到信息后更新比自己新的数据,一般拥有新信息的节点参会作为发起节点。
pull: 发起信息交换的节点A随机选择节点B,并从对方获取信息,一般无新信息的节点才会作为发起节点
push&pull: 发起信息交换的节点A向选择的节点B发送消息,同时从对方获取数据,用于更新自己的本地数据。
优点
可扩展性、容错、健壮性、最终一致性、简单
缺点
消息延迟、消息冗余、拜占庭问题
Redis Cluster
Redis Cluster是在3.0版本引入的功能。为了让集群中每个实例都知道其他所有实例的状态信息,Redis集群规定各个实例之间按照Gossip协议来通信传递消息。
Redis Cluster的节点间会相互发送多种消息,较为重要的如下:
MEET: 通过cluster meet ip port命令,已有集群的节点会向新的节点发送邀请,加入现有集群,然后新节点就会开始与其他节点进行通信
PING: 节点按照配置的时间间隔向集群中其他节点发送ping消息,消息中带有自己的状态,还有自己维护的集群元数据,和部分其他节点的元数据
PONG: 节点用于回应PING和MEET消息,结构和PING消息类似,也包含自己的状态和其他信息,也可以用于信息广播和更新
FAIL: 节点PING不通某节点后,会向集群所有节点广播该节点挂掉的消息。
参考:
【超详细】分布式一致性协议 - Paxos-腾讯云开发者社区-腾讯云
https://juejin.cn/post/6907151199141625870#heading-27
https://www.inlighting.org/archives/raft-membership-change
http://www.tcs.hut.fi/Studies/T-79.5001/reports/2012-deSouzaMedeiros.pdf
https://marcoserafini.github.io/papers/zab.pdf
https://juejin.cn/post/6844903672128733198
https://juejin.cn/post/6945395113132556325
面试官:说一下Zookeeper的ZAB协议?敖丙:不好意思我肚子疼!-腾讯云开发者社区-腾讯云
https://juejin.cn/post/6902004920543952909