分布式一致性协议--2raft协议

在Raft中,问题可以分解为:领导选取、日志复制、安全和成员变化。每一个步骤都是一个比较清晰的待解决问题, 便于理解, 接下里我们先来了解一下Raft协议中所涉及的几个基本概念。
为了让一致性协议变得简单可理解,Raft协议主要使用了两种策略:
1 将复杂问题进行分解,在Raft协议中,一致性问题被分解为:leader election、log replication、safety三个简单问题;
2减少状态空间中的状态数目。

0 基本概念

日志:每台机器保存一份日志,日志来自于客户端的请求,包含一系列的命令

状态机:状态机会按顺序执行这些命令

一致性模型:分布式环境下,保证多机的日志是一致的,这样回放到状态机中的状态是一致的

一致性算法作用于一致性模型,一般有以下特性:

safety:在非拜占庭问题下(网络延时,网络分区,丢包,重复发包以及包乱序等),结果是正确的

availability:在半数以上机器能正常工作时,则系统可用

timing-unindependent:不依赖于时钟来保证日志一致性,错误的时钟以及极端的消息时延最多会造成可用性问题

注意:真实的实现中,建议状态机的每个命令操作都采用幂等的,这样一致性的保证会更容易。

1 复制状态机

一个分布式的复制状态机系统由多个复制单元组成,每个复制单元均是一个状态机,它的状态保存在一组状态变量中,状态机的变量只能通过外部命令来改变。简单理解的话,可以想象成是一组服务器,每个服务器是一个状态机,服务器的运行状态只能通过一行行的命令来改变。每一个状态机存储一个包含一系列指令的日志,严格按照顺序逐条执行日志中的指令,如果所有的状态机都能按照相同的日志执行指令,那么它们最终将达到相同的状态。因此,在复制状态机模型下,只要保证了操作日志的一致性,我们就能保证该分布式系统状态的一致性。

img

在上图中,服务器中的一致性模块(Consensus Modle)接受来自客户端的指令,并写入到自己的日志中,然后通过一致性模块和其他服务器交互,确保每一条日志都能以相同顺序写入到其他服务器的日志中,即便服务器宕机了一段时间。一旦日志命令都被正确的复制,每一台服务器就会顺序的处理命令,并向客户端返回结果。

3 Raft一致性算法

在Raft体系中,有一个强leader,由它全权负责接收客户端的请求命令,并将命令作为日志条目复制给其他服务器,在确认安全的时候,将日志命令提交执行。当leader故障时,会选举产生一个新的leader。在强leader的帮助下,Raft将一致性问题分解为了三个子问题:

  1. leader选举:当已有的leader故障时必须选出一个新的leader。
  2. 日志复制:leader接受来自客户端的命令,记录为日志,并复制给集群中的其他服务器,并强制其他节点的日志与leader保持一致。
  3. 安全safety措施:通过一些措施确保系统的安全性,如确保所有状态机按照相同顺序执行相同命令的措施。

一个Raft集群拥有多个服务器,典型值是5,这样可以容忍两台服务器出现故障。服务器可能会处于如下三种角色:leader、candidate、follower,正常运行的情况下,会有一个leader,其他全为follower,follower只会响应leader和candidate的请求,而客户端的请求则全部由leader处理,即使有客户端请求了一个follower也会将请求重定向到leader。candidate代表候选人,出现在选举leader阶段,选举成功后candidate将会成为新的leader。可能出现的状态转换关系如下图:

img

从状态转换关系图中可以看出,集群刚启动时,所有节点都是follower,之后在time out信号的驱使(选举超时定时器,每个节点都有,leader发心跳,刷新该值)下,follower会转变成candidate去拉取选票,获得大多数选票后就会成为leader,这时候如果其他候选人发现了新的leader已经诞生,就会自动转变为follower;而如果另一个time out信号发出时,还没有选举出leader,将会重新开始一次新的选举。可见,time out信号是促使角色转换得关键因素<.font> 。

3.1 任期(term)

Raft 算法将时间划分成为任意不同长度的任期(term)。任期用连续的数字进行表示。每一个任期的开始都是一次选举(election),一个或多个候选人会试图成为leader。如果一个候选人赢得了选举,它就会在该任期的剩余时间担任领导人。在某些情况下,选票会被瓜分,有可能没有选出领导人,那么,将会开始另一个任期,并且立刻开始下一次选举。Raft 算法保证在给定的一个任期最多只有一个leader。

img

server之间的交流是通过RPC进行的。只需要实现两种RPC就能构建一个基本的Raft集群:

1 RequestVote RPC:它由选举过程中的candidate发起,用于拉取选票
2 AppendEntries RPC:它由leader发起,用于复制日志或者发送心跳信号。

它们的定义如下图所示:

img

img

3.2 leader选举过程

Raft通过心跳机制发起leader选举。节点都是从follower状态开始的,如果收到了来自leader或candidate的RPC,那它就保持follower状态,避免争抢成为candidate。Leader会发送空的AppendEntries RPC作为心跳信号来确立自己的地位,如果follower一段时间(election timeout)没有收到心跳,它就会认为leader已经挂了,发起新的一轮选举

选举发起后,一个follower会增加自己的当前term编号并转变为candidate。它会首先投自己一票,然后向其他所有节点并行发起RequestVote RPC,之后candidate状态将可能发生如下三种变化:

1 赢得选举,成为leader: 如果它在一个term内收到了大多数的选票,将会在接下的剩余term时间内称为leader,然后就可以通过发送心跳确立自己的地位。(每一个server在一个term内只能投一张选票,并且按照先到先得的原则投出)
2 其他server成为leader:在等待投票时,可能会收到其他server发出AppendEntries RPC心跳信号,说明其他leader已经产生了。这时通过比较自己的term编号和RPC过来的term编号,如果比对方大,说明leader的term过期了,就会拒绝该RPC,并继续保持候选人身份; 如果对方编号不比自己小,则承认对方的地位,转为follower.
3 选票被瓜分,选举失败: 如果没有candidate获取大多数选票, 则没有leader产生, candidate们等待超时后发起另一轮选举. 为了防止下一次选票还被瓜分,必须采取一些额外的措施, raft采用随机election timeout的机制防止选票被持续瓜分。通过将timeout随机设为一段区间上的某个值, 因此很大概率会有某个candidate率先超时然后赢得大部分选票**

3.3 日志复制的安全性

3.3.1日志复制过程

一旦leader被选举成功,就可以对客户端提供服务了。客户端提交每一条命令都会被按顺序记录到leader的日志中,每一条命令都包含term编号和顺序索引,然后向其他节点并行发送AppendEntries RPC用以复制命令(如果命令丢失会不断重发),当复制成功也就是大多数节点成功复制后,leader就会提交命令,即执行该命令并且将执行结果返回客户端,raft保证已经提交的命令最终也会被其他节点成功执行。leader会保存有当前已经提交的最高日志编号。顺序性确保了相同日志索引处的命令是相同的,而且之前的命令也是相同的**。当发送AppendEntries RPC时,会包含leader上一条刚处理过的命令,接收节点如果发现上一条命令不匹配,就会拒绝执行**。

接受命令的过程:
  1. 领导者接受客户端请求;

  2. 领导者把指令追加到日志;

  3. 发送AppendEntries RPC到追随者;

  4. 领导者收到大多数追随者的确认后,领导者Commit该日志,把日志在状态机中执行,并返回结果给客户端;

提交过程:
  1. 在下一个心跳阶段,领导者再次发送AppendEntries RPC给追随者,日志已经commited;

  2. 追随者收到Commited日志后,将日志在状态机中执行。

在这个过程中可能会出现一种特殊故障:如果leader崩溃了,它所记录的日志没有完全被复制,会造成日志不一致的情况,follower相比于当前的leader可能会丢失几条日志,也可能会额外多出几条日志,这种情况可能会持续几个term。如下图所示:

img

在上图中,框内的数字是term编号,a、b丢失了一些命令,c、d多出来了一些命令,e、f既有丢失也有增多,这些情况都有可能发生。比如f可能发生在这样的情况下:f节点在term2时是leader,在此期间写入了几条命令,然后在提交之前崩溃了,在之后的term3中它很快重启并再次成为leader,又写入了几条日志,在提交之前又崩溃了,等他苏醒过来时新的leader来了,就形成了上图情形。

如何解决日志不一致?

1 在Raft中,leader通过强制follower复制自己的日志来解决上述日志不一致的情形,那么冲突的日志将会被重写。为了让日志一致,先找到最新的一致的那条日志(如f中索引为3的日志条目),然后把follower之后的日志全部删除,leader再把自己在那之后的日志一股脑推送给follower,这样就实现了一致。而寻找该条日志,可以通过AppendEntries RPC,该RPC中包含着下一次要执行的命令索引,如果能和follower的当前索引对上,那就执行,否则拒绝,然后leader将会逐次递减索引,直到找到相同的那条日志。

2 然而这样也还是会有问题,比如某个follower在leader提交时宕机了,也就是少了几条命令,然后它又经过选举成了新的leader,这样它就会强制其他follower跟自己一样,使得其他节点上刚刚提交的命令被删除,导致客户端提交的一些命令被丢失了。Raft通过为选举过程添加一个限制条件,该限制确保leader包含之前term已经提交过的所有命令Raft通过投票过程确保只有拥有全部已提交日志的candidate能成为leader。由于candidate为了拉选票需要通过RequestVote RPC联系其他节点**,而之前提交的命令至少会存在于其中某一个节点上,因此只要candidate的日志至少和其他大部分节点的一样新就可以了**, follower如果收到了不如自己新的candidate的RPC,就会将其丢弃,自己最新的话成为新的cindidate.

3 还可能会出现另外一个问题, 如果命令已经被复制到了大部分节点上,但是还没来的及提交就崩溃了,这样后来的leader应该完成之前term未完成的提交. Raft通过让leader统计当前term内还未提交的命令已经被复制的数量是否半数以上, 然后进行提交。.

领导人通过强制追随者们复制它的日志来处理日志的不一致,追随者上的冲突日志会被领导者的日志覆盖。(下面几节会证明为什么这么做是安全的)

为了使得追随者的日志同自己的一致,领导人需要找到追随者同它的日志一致的地方,然后删除追随者在该位置之后的条目,然后将自己在该位置之后的条目发送给追随者。这些操作都在 AppendEntries RPC 进行一致性检查时完成。

领导者永远不会覆盖自己已经存在的日志条目

日志永远只有一个流向:从领导者到追随者

3.3.2 选举约束(什么样的节点能成为leader)

两个约束:

1 master选举最终安全性目标: 被选举出来的leader必须要包含所有已经提交的entries
如leader针对复制过半的entry提交了,但是某些follower可能还没有这些entry,当leader挂了,该follower如果被选举成leader的时候,就可能会覆盖掉了上述的entry了,造成不一致的问题,所以新选出来的leader必须要满足上述约束。
2 raft简单实现:只要当前server的log比半数server的log都新就可以,具体到每一个node的比对就是上述说的“谁的lastLog的term越大谁越新,如果term相同,谁的lastLog的index越大谁越新

抱歉这个约束还不能保证安全性,如下面3.3.3中另外一个问题。

我们先来看看到现在为止整个系统是安全的么?

异常场景:leader挂掉时机:

a、 尚未打log: 完全无影响

b、 自己写了log, 但还未发requestRPC :不会对client返回处理成功, 但是会在自己服务器中保留一份log, 未来新选的master会刷掉这份log(也就是3.3.1中设置的条件)

c、 发了requestRPC 尚未commit : 收到消息的follow都会保留一份log, 但因为还未commit 所以不会对client返回处理成功, 但是此处是存有疑问的。

d、 commit 之后: 已经commit了, 也就是说client认为已经处理成功了, 此时挂了的话剩下的机器中有一多半都已经保存了数据log, 他们竞争master时也会保留已经提交的数据(参见3.3…2)。

最终会剩下一个问题, 也就是c中新接手的master怎么处理之前尚未commit的数据, 再来看之前的选举限制:

*“lastLog的term越大谁越新,如果term相同,谁的lastLog的index越大谁越新”*

在这种情况下我们选举master其实只是关注了log新旧, 并没有关注commit与否, 在3.3.3 中例子可以发现现在的约束是无法实现master选举的最终安全性目标的

3.3.3 如何处理任期之前的日志条目(未commit的log)

在Raft算法中,当一个日志被安全的复制到绝大多数的机器上面,即AppendEntries RPC在绝大多数服务器正确返回了,那么这个日志就是被提交了,然后领导者会更新commit index。

img

详细解释如下:

a场景:s1是leader,此时处于term2,并且将index为2的entry复制到s2上

b场景:s1挂了,s5利用s3,s4,s5当选为leader,处于term3,s5在index为2的位置上接收到了新的entry

c场景:s5挂了,s1当选为leader,处于term4,s1将index为2,term为2的entry复制到了s3上,此时已经满足过半数了

*重点就在这里:此时处于term4,但是之前处于term2的entry达到过半数了,s1是提交该entry呢还是不提交呢?*

  • 假如s1提交的话,则index为2,term为2的entry就被应用到状态机中了,是不可改变了,此时s1如果挂了,来到term5,s5是可以被选为leader的,因为按照之前的log比对策略来说,s5的最后一个log的term是3比s2 s3 s4的最后一个log的term都大。一旦s5被选举为leader,即d场景,s5会复制index为2,term为3的entry到上述机器上,这时候就会造成之前s1已经提交的index为2的位置被重新覆盖,因此违背了一致性。
  • 假如s1不提交,而是等到term4中有过半的entry了,然后再将之前的term的entry一起提交(这就是所谓的间接提交,即使满足过半,但是必须要等到当前term中有过半的entry才能跟着一起提交),即处于e场景,s1此时挂的话,s5就不能被选为leader了,因为s2 s3的最后一个log的term为4比s5的3大,所以s5获取不到投票,进而s5就不可能去覆盖上述的提交
第三个约束:

当前term的leader不能“直接”提交之前term的entries 必须要等到当前term有entry过半了,才顺便一起将之前term的entries进行提交

所以raft靠着这2个约束来进一步保证一致性问题。

再来仔细分析这个案例,其问题就是出在:*上述leader选举上,s1如果在c场景下将index为2、term为2的entry提交了,此时s5也就不包含所有的commitLog了,但是s5按照log最新的比较方法还是能当选leader。*

那就是说log最新的比较方法并不能保证2中的选举约束即
*被选举出来的leader必须要包含所有已经提交的entries*
所以可以理解为:正是由于上述选举约束实现上的简单实现并不靠谱, 才导致又加了这么一个不能直接提交之前term的entries的约束。

3.3.4 安全性论证讨论
Leader Completeness: 如果一个entry被提交了,那么在之后的leader中,必然存在该entry。

经过上述2个约束,就能得出Leader Completeness结论。正是由于上述***“不能直接提交之前term的entries”***的约束,所以任何一个entry的提交必然存在当前term下的entry的提交。那么此时所有的server中有过半的server都含有当前term(也是当前最大的term)的entry,假设serverA将来会成为leader,此时serverA的lastlog的term必然是不大于当前term的,它要想成为leader,即和其他server pk 谁的log最新,必然是需要满足log的index比他们大的,所以必然含有已提交的entry。

client端

在client看来如果client发送一个请求,leader返回ok响应,那么client认为这次请求成功执行了,那么这个请求就需要被真实的落地,不能丢。

如果leader没有返回ok,那么client可以认为这次请求没有成功执行,之后可以通过重试方式来继续请求。

异常场景 1: leader挂

一旦你给客户端回复OK的话,然后挂了,那么这个请求对应的entry必须要保证被应用到状态机,即需要别的leader来继续完成这个应用到状态机。

一旦leader在给客户端答复之前挂了,那么这个请求对应的entry就不能被应用到状态机了,如果被应用到状态机就造成客户端认为执行失败,但是服务器端缺持久化了这个请求结果,这就有点不一致了。

这个原则同消息队列也是一致的。再来说说什么叫消息队列的消息丢失(很多人还没真正搞明白这个问题):client向服务器端发送消息,服务器端回复OK了,之后因为服务器端自己的内部机制的原因导致该消息丢失了,这种情况才叫消息队列的消息丢失。如果服务器端没有给你回复OK,那么这种情况就不属于消息队列丢失消息的范畴。

再来看看raft是否能满足这个原则:

  • leader在某个entry被过半复制了,认为可以提交了,就应用到状态机了,然后向客户端回复OK,之后leader挂了,是可以保证该entry在之后的leader中是存在的
  • leader在某个entry被过半复制了,然后就挂了,即没有向客户端回复OK,raft的机制下,后来的leader是可能会包含该entry并提交的,或可能直接就覆盖掉了该entry。如果是前者,则该entry是被应用到了状态机中,那么此时就出现一个问题:client没有收到OK回复,但是服务器端竟然可以成功保存了, 为了掩盖这种情况,就需要在客户端做一次手脚,即客户端对那么没有回复OK的都要进行重试,客户端的请求都带着一个唯一的请求id,重试的时候也是拿着之前的请求id去重试的服务器端发现该请求id已经存在提交log中了,那么直接回复OK,如果不在的话,那么再执行一次该请求。
异常场景 2: follower挂

follower挂了,只要leader还满足过半条件就一切正常。他们挂了又恢复之后,leader是会不断进行重试的,该follower仍然是能恢复正常的

follower在接收AppendEntries RPC的时候是幂等操作

3.4集群成员变更对安全性的影响

集群成员的变更和成员的宕机与重启不同,因为前者会修改成员个数进而影响到领导者的选取和决议过程,因为在分布式系统这对于majority这个集群中成员大多数的概念是极为重要的。

集群成员变的安全性问题: 两个不同的领导人在同一个任期里都可以被选举成功。一个是通过旧的配置,一个通过新的配置。

两阶段方法保证安全性:
1 在 Raft 中,集群先切换到一个过渡的配置,我们称之为共同一致***(joint consensus)***;
一旦共同一致已经被提交了,那么系统就切换到新的配置上。共同一致是老配置和新配置的结合。
2 共同一致允许独立的服务器在不影响安全性的前提下,在不同的时间进行配置转换过程。此外,共同一致可以让集群在配置转换的过程中依然响应服务器请求。

一个领导人接收到一个改变配置从 C-old 到 C-new 的请求,他会为了共同一致存储配置(图中的 C-old,new),以前面描述的日志条目和副本的形式。一旦一个服务器将新的配置日志条目增加到它的日志中,他就会用这个配置来做出未来所有的决定。领导人完全特性保证了只有拥有 C-old,new 日志条目的服务器才有可能被选举为领导人。当C-old,new日志条目被提交以后,领导人在使用相同的策略提交C-new,如下图所示,C-old 和 C-new 没有任何机会同时做出单方面的决定,这就保证了安全性。

img

一个配置切换的时间线。虚线表示已经被创建但是还没有被提交的条目,实线表示最后被提交的日志条目。领导人首先创建了 C-old,new 的配置条目在自己的日志中,并提交到 C-old,new 中(C-old,new 的大多数和 C-new 的大多数)。然后他创建 C-new 条目并提交到 C-new 中的大多数。这样就不存在 C-new 和 C-old 可以同时做出决定的时间点。

小结:领导人完全特性保证了只有拥有 C-old,new 日志条目的服务器才有可能被选举为领导人,保证了平滑切换。

3.5日志压缩

随着日志大小的增长,会占用更多的内存空间,处理起来也会耗费更多的时间,对系统的可用性造成影响,因此必须想办法压缩日志大小。Snapshotting是最简单的压缩方法,系统的全部状态会写入一个snapshot保存起来,然后丢弃截止到snapshot时间点之前的所有日志。Raft中的snapshot内容如下图所示:

img

每一个server都有自己的snapshot,它只保存当前状态,如上图中的当前状态为x=0,y=9,而last included index和last included term代表snapshot之前最新的命令,用于AppendEntries的状态检查。

虽然每一个server都保存有自己的snapshot,但是当follower严重落后于leader时,leader需要把自己的snapshot发送给follower加快同步,此时用到了一个新的RPC:InstallSnapshot RPC。follower收到snapshot时,需要决定如何处理自己的日志,如果收到的snapshot包含有更新的信息,它将丢弃自己已有的日志,按snapshot更新自己的状态,如果snapshot包含的信息更少,那么它会丢弃snapshot中的内容,但是自己之后的内容会保存下来。RPC的定义如下:

img

总结

1 选举leader的两个约束

1 tem越新,tem一样logindex越大
2 拥有所有已提交的enties

2 解决日志不一致,除了leader选举约束?

3未提交的log 必须满足半数以上节点的enties才能被提交。

参考

1 https://docs.qq.com/doc/DY0VxSkVGWHFYSlZJ

2 https://www.jianshu.com/p/ddbe4209be0f

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值