6.824——实验二:Raft

1.介绍

这是构建KV存储系统一系列实验的第一个实验。而对于分布式数据库而言,首先要解决两个重大的问题:容错、一致性。所以这个实验当中,需要实现Raft协议(一个复制状态机协议)以解决这两个首要问题。

一个可靠性服务需要通过存储其状态副本(比如数据等)到多个副本服务器上来实现容错。即使一些服务器节点失败了,其它副本服务器仍可以运行该服务。但问题的关键是,失败的服务节点会造成数据不一致。
下一个实验会在基于Raft协议之上构建kv服务,然后可以在多机环境上共享kv服务,以求更高的性能。

2.Raft

为了解决上面的问题即一致性问题,于是出现了Raft,Raft能够管理所有服务节点的状态副本,能够让失败的服务节点恢复到正确的一致性状态上来,算法的目标就是让服务器节点按照少数服从多数的方式,最终达成一致意见。正如上面讲到的,Raft是一个复制状态机协议。https://yuerblog.cc/2018/07/28/understand-raft/——这篇博客不错,raft关键在于抽屉理论,二阶段提交,选举约束,一共3个部分。
【下面仔细说一下Raft的原理】
在GFS中我们说过一致性的实现原理——确定一个主实例,串行化所有写操作,然后在其他实例重放相同的操作序列,以保证多个实例数据的一致性。但是GFS中只有一个主实例master,一旦master崩溃,这会造成很大的影响,所以一般GFS都还会有些备用master,需要在这些备用master上同样同步保存配置信息和日志信息。

虽然大体上Raft协议也是确定一个主实例Leader,但不同的是其不需要额外的备份Leader,因为其他的所有节点都可以成为Leader。
下面描述一下Raft的大体流程,在一台Leader服务器上,一致性模块(这里就是Raft)接收到客户端的指令并把指令整理后写入到日志中,并与其他Follower服务器上的一致性模块(Raft实例)通信,以确保每一个日志最终包含一致的请求序列,即使有某些服务器宕机。一旦这些指令被正确的复制了,每一个服务器的状态机都会按同样的顺序去执行它们,然后将结果返回给客户端。另外,如果某些服务器宕机后又重新恢复了,其Raft会主动和Leader服务器通信,及时更新本地log并执行相应的命令序列,保持状态一致性。
http://thesecretlivesofdata.com/raft/ 这个链接超级棒,讲得很详细。总结一下Raft的三个主要子问题:
【1. 日志复制】
这里我们首先说一下每一个服务器上日志记录的形式:term+command。
在这里插入图片描述
对于整个系统而言,所有的改变都要经由Leader来实现。在一台Leader服务器上,一致性模块(这里就是Raft)接收到客户端的指令并把指令整理后写入到日志中,此时,该条日志记录还处于未提交状态,节点并不会执行具体操作;而是在提交之前首先与其他Follower服务器上的一致性模块(Raft实例)通信,以确保每一个日志最终包含一致的请求序列,再等待大多数节点写入log并响应Leader之后,Leader才commit执行具体操作,然后Leader通知其他节点commit。

【2. Leader选举】

  • 对于集群中的每个节点而言,有3种状态——Follower、Candidate、Leader。
  • 初始时都为follower状态,如果follower节点在一段时间内没有收到命令(即当前Leader宕机了),就会变为Candidate状态;然后Candidate节点会向其他节点发起投票请求,如果该Candidate节点得到了大多数的vote支持,就会变为Leader。
  • 超时设定:
    election timeout.——若超过这个时间还没有接到Leader的命令(心跳检测或log增加等RPC调用),则代表Leader可能挂掉了,于是自己就可以准备上位了

Raft的关键就在于Leader选举机制,因为Leader是保持一致性的根本,Leader的log条目必须是要与Client提交过的log条目一致才行,即要保证Leader的log条目一定是commit过最多的。
而抽屉理论恰好解决了这个问题,抽屉中有5个球,3黑2白,任意抽3个球,必然有一个是黑球。
借鉴这个理论,我们只需要添加几个约束条件即可:

  1. commit限制:我们在日志复制时,必须要复制超过半数成功返回给当前Leader,Leader才能commit并向Client承诺
  2. 投票(RequestVote)限制:follower只能向大于自己term的candidate投票,在相等的情况下,选择记录了最新log的
  3. 选举限制:如果当前Leader宕机,开始新一轮选举,则必须要保证接收到超过半数的选票才能成为新Leader,因为前面的commit限制,超过半数意味着其中至少有一个follower是最新状态(可能not commit,但是一定已经写在了其本地log中),另外由于投票限制,意味着当前Leader一定具有最新的已commit的log条目

【Term】
在这里插入图片描述
首先我们从宏观上考虑:我们可以将一致性看作是一段时间内的一致性,在这一段时间内必须得保证集群的一致性,而由于各种原因可能导致出现不同的Leader,于是天然的就将时间划分为了不同的任期,在一个Term中只能产生一个Leader;
其中Term的变化流程:
Raft开始时所有Follower的Term为1,其中一个Follower逻辑时钟到期后转换为Candidate,Term加1这时Term为2,然后开始选举,这时候有几种情况会使Term发生改变:
  1:如果当前Term为2的任期内没有选举出Leader或出现异常,则Term递增,开始新一任期选举
  2:当这轮Term为2的周期选举出Leader后,过后Leader宕掉了,然后其他Follower转为Candidate,Term递增,开始新一任期选举
  3:当Leader或Candidate发现自己的Term比别的Follower小时Leader或Candidate将转为Follower,Term递增
  4:当Follower的Term比别的Term小时Follower也将更新Term保持与其他Follower一致;
  
【3. 安全】
对于Raft而言安全的关键参数就是状态安全参数,如果一个服务器已经将某个log索引位置的entry用于其状态机(也就是commit执行了),则其他服务器不允许在同样的log索引位置增加不同的entry。commit是十分重要的一个概念,commited log 意味着这些日志已经被持久化的记录在了集群中的大多数服务器节点上,而且由于少数服从多数的原则,这保证了即使有一小部分节点因为网络等原因没有持久化记录,但是一定会在后面某个网络顺畅的时刻由Leader通知持久化记录并应用于其状态机。

2.1 具体实现——Raft算法的事件和状态转换

首先说一下,每一个服务器都可以视为一个Raft实例节点,集群的一致性就是通过各个服务器的Raft实例实现的。
https://www.cnblogs.com/wangbinquan/p/9061223.html——参考链接

2.1.1 【Raft集群启动 】

1、可以获取整个Raft集群的所有节点连接信息
2、currentTerm初始为0、votedFor初始为空
3、初始状态为Follower
4、如果是重新启动则有快照和日志序列,如果为新集群则全部为空
5、启动随机定时器,定时器超时时间在[m, n]范围内,保证请求传输时间 << [m, n] << 平均一个服务器两次出现宕机的时间间隔

2.1.2【状态】

1.存储于所有服务器上的持久化状态变量:

  • currentTerm——当前节点所能看到的最大的term值,该值单调增加
  • votedFor——当前term里将票投给的对象,如果尚未投票则为空
  • log[]——日志条目;每个条目都包含用于状态机的命令和Leader收到条目的term期限(第一个term为1)

2.存储于所有服务器上的可变状态变量(每次选举后都要重新初始化)

  • commitIndex——已知已提交的最后一个log条目索引(初始为0,单调增加)
  • lastApplied——已知已经执行的最后一个log条目索引(初始为0,单调增加),如果发现当前机器commitIndex > lastApplied则应该将本机log[]中序号为(lastApplied, commitIndex]的部分应用到状态机

3.存储于Leader上的可变状态变量

  • nextIndex[]——要发送给其它节点的下一条log条目索引(初始时为Leader的最后一个log条目索引+1)
  • matchIndex[]——所有其它节点已经复制好的最后一条log条目索引,即和Leader匹配的最大日志编号(初始为0,单调增加)

2.1.3【RPC方法】

1.AppendEntries RPC——Leader调用以向其它follower节点实现日志复制功能,也可以用作心跳检测
我们以下图来具体分析
在这里插入图片描述
我们知道,持久化存储的日志信息只有term+command(当然其log index肯定也知道),所以我们只能以term+log index来作为log 复制的判断依据。如上图,在某轮term下,log出现了上述不一致的情况,该如何实现日志复制功能呢?下面具体看一下Raft的AppendEntries 设计。起初leader并不知道其它节点的log状态,所以统一将其它节点nextIndex[]设置为其log index+1,意为假设follower和自己保持一致,所以接下来就是发送AppendEntries 进行一致性校验,若follower的log超过了自己或包含自己不匹配的(c、d、f),则follower删除不匹配的log(这部分删除由于前面的leader选举限制保证了这部分多余或不匹配的log肯定是前一个Leader没有commit的,即没有向用户承诺的数据,可以安全删除)返回false,若不足(a、b、e)则返回false,然后leader使其nextIndex[i]–,直到最终在某个点达成一致,然后follower便可以复制leader的新增日志,实现一致性。

参数:

  • term——leader任期
  • leaderID——不解释
  • prevLogIndex——当前即将发送的日志的前面一个日志的索引(等于nextIndex[]中该节点对应的值-1 )
  • prevLogTerm——当前即将发送的日志的前面一个日志的term值 (等于此Leader的log[]中prevLogIndex对应日志的term值 )
    //prevLogIndex…prevLogTerm就相当于Leader的匹配点,只有follower匹配了Leader的匹配点才表示follower接下来的日志复制操作才能与Leader保持log一致性
  • entries[]——即节点需要add的log条目(心跳检测时为空),如果prevLogIndex+1不是此Leader的log[]中最后一条日志,则 entries[]取log[]中prevLogIndex之后紧接着的部分日志
  • leaderCommit——Leader最后一次commit的索引

结果

  • term——接收日志节点的term值,即告知leader当前follower的任期,如果大于leader的term,则leader需要更新自己的currentTerm,并转换为Follower状态
  • success——如果接收日志节点的log[]结构中prevLogIndex索引处含有日志并且该日志的term等于prevLogTerm则返回true,否则false

任何状态下的节点接收到AppendEntries的返回结果逻辑:(条件依次判断,不满足上一条才会进入下一条)

  1. 如果发过来的term值小于当前term值,返回false,以及currentTerm——针对场景(d):可能旧Leader(term6)与当前follower由于网络原因,导致很长时间才接受到AppendEntries,但是在这段网络堵塞的时间内,已经推选出了另外一个新Leader(term7),而由于Leader选举算法的原因,这个新Leader总是满足一致性,所以当前follower的日志可能已处于最新term期,旧Leader没有AppendEntries完成的任务已经由新Leader代替完成了,此时term7完成。如果网络恢复,旧Leader发送过来的AppendEntries信息已经过时,直接拒绝即可。
  2. 如果term > currentTerm则设置currentTerm = term, voteFor = leaderId。转换当前节点为Follower状态,重置随机定时器,并进入下一步——针对场景(b,e)
  3. 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配(也包括prevLogIndex 不存在日志的情况),则返回 false,则设置voteFor = leaderId,并返回false——针对场景(a,b,e):可能旧Leader(Term6)还没有来得及完成对节点a的全部复制,就fail了或者节点a自己还没写完log就fail了,等到term8才恢复过来。
  4. 如果在当前索引位置上存在日志,但是term不一致,则删除这一条和之后所有的 (因为当前不一致,后面的肯定不一致),返回false——针对场景(f):在term2、term3时期,大多数follower都没有来得及完成log写入就fail掉了(Leader或Follower都有可能fail),只有一小部分follower写入了term2、term3时期leader的appendEntries日志。然后再紧接着这几轮term,f都一直处于fail状态,直到term8才恢复。
  5. 到这一步,意味着已经达成了一致,即prevLogIndex与prevLogTerm相匹配了,比如f,会不断地删除直到leader对应的nextIndex[f]=4,亦即prevLogIndex=3,prevLogTerm=3(表示Leader的匹配点),然后f发现与自己的匹配点相一致,于是可以执行日志存储功能

    日志存储:
    1、将prevLogIndex之后的日志全部删除,并将entries[]中的日志依次放入log[]中prevLogIndex之后的位置里。
    2、如果leaderCommit(参数里) > commitIndex(每个节点存储里)则设置commitIndex = leaderCommit并将(commitIndex,leaderCommit]区间的日志应用到状态机上(更新commitIndex后机器会自动应用该操作)。

在Leader状态下接收到AppendEntries的返回结果逻辑:(条件依次判断,不满足上一条才会进入下一条)
6. 返回消息中的term > currentTerm则设置currentTerm = term,并将自己转换为Follower状态,并重置随机定时器。——对应场景(d)
7. 返回消息中success为false则将该节点在nextIndex[]对应的值减1——对应场景(a,b,e,f)
8. 如果接收到大多数Follower的成功反馈,则可以提交该条AppendEntries所同步的所有日志,和此之前的所有日志。Leader只允许提交当前term的日志,不允许提交之前term的日志,但是可以通过提交当前term的日志达到间接提交之前term的日志的目的。
> 因为Leader不允许提交之前term的日志,因此在Leader被选举成功时可以发送一条无意义日志给其他机器,以更新日志列表中的最大term编号,当接收到大多数返回时提交该日志,以达到提交之前已被大多数节点接受的日志的目的

2.RequestVote RPC——candidate向follower拉票
参数:
在这里插入图片描述
返回结果:在这里插入图片描述
投票人的投票逻辑实现:

  1. 如果term < currentTerm返回 false ——选举限制,保证leader始终是commit过的term最新的
  2. 如果 votedFor 为空或者为 candidateId,并且候选人的日志至少和自己一样新(即candidate的最后一个日志的term和index要分别大于等于自己节点的最后一个日志的term和index),那么就投票给他——在同样commit的情况下(即1),尽量选举not commit但是已经写入log最多的。

2.1.4 【Raft实例规则】

1.所有节点:

  • 如果commitIndex > lastApplied,那么就 lastApplied 加一,并把log[lastApplied]应用到状态机中
  • 如果接收到的 RPC 请求或响应中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换到follower状态

2.Followers:

响应来自候选人和领导者的请求,如果在超过选举超时时间的情况之前都没有收到领导人的心跳检查,或者是候选人的请求投票,就自己变成候选人

3.Candidates:

在转变成候选人后就立即开始选举过程:

  • 自增当前的任期号(currentTerm++)
  • 给自己投票 votedFor = me
  • 重置选举超时计时器
  • 发送请求投票的 RPC 给其他所有服务器
    -如果接收到大多数服务器的选票,那么就变成领导人
  • 如果接收到来自新的领导人的附加日志 RPC,转变成跟随者
  • 如果选举过程超时,再次发起一轮选举

4.Leaders:

  • 一旦成为领导人:发送空的附加日志 RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以阻止跟随者超时
  • 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端
  • 如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex,那么:发送从 nextIndex 开始的所有日志条目:
    • 如果成功:更新相应跟随者的 nextIndex 和 matchIndex
    • 如果因为日志不一致而失败,减少 nextIndex 重试
  • 如果存在一个满足N > commitIndex的 N,并且大多数的matchIndex[i] ≥ N成立,并且log[N].term == currentTerm成立,那么令 commitIndex 等于这个 N

2.1.5 超时限定

对于Raft而言,超时限定是十分重要的一个设定,只有合理实现超时限定才能满足Leader选举的内在要求,Raft 的要求之一就是安全性不能依赖时间:整个系统不能因为某些事件运行的比预期快一点或者慢一点就产生了错误的结果。但是,可用性(系统可以及时的响应客户端)不可避免的要依赖于时间。例如,如果消息交换比服务器故障间隔时间长,候选人将没有足够长的时间来赢得选举;没有一个稳定的领导人,Raft 将无法工作。
所以我们得保证在一个选举超时时间内,应该对于所有正常的网络通信而言,必须要来得及持久化状态存储以及及时作出回应。
Raft 可以选举并维持一个稳定的领导人,只要系统满足下面的时间要求:

广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)

在这个不等式中,广播时间指的是从一个服务器并行的发送 RPCs 给集群中的其他服务器并接收响应的平均时间;然后平均故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。广播时间必须比选举超时时间小一个量级,这样领导人才能够发送稳定的心跳消息来阻止跟随者开始进入选举状态;选举超时时间应该要比平均故障间隔时间小上几个数量级,这样整个系统才能稳定的运行(比如,如果平均故障时间是1天,如果超时时间设为2天,那肯定不行,这样的话整个系统都崩溃1天,才能进行下一次Leader选举)。当领导人崩溃后,整个系统会大约相当于选举超时的时间里不可用;我们希望这种情况在整个系统的运行中很少出现。
另外值得注意的一点是,选举时间必须得是一个随机时间,这样才能避免选票的瓜分,否则,所有follower都会在同一时间转换为candidate状态并发起拉票请求,导致各自的票都投给了自己,导致造成无限循环竞选Leader。

发布了69 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/JustKian/article/details/102738385
今日推荐