分布式一致性共识算法之Raft算法浅析

从十一月开始,前前后后花了刚好14天的时间,完成了MIT6.824(MIT 6.5840)课程的lab2四个实验,并在本地通过了八百次左右的测试,目前正在进行6666次的测试中。本篇文章将结合自己做实验时的理解,不谈实验细节,单纯聚焦Raft本身,简单介绍一下Raft算法思想。关于实现的细节,将在后续文章介绍。

Raft Extended论文
Raft 可视化
Raft Website
在这里插入图片描述

Raft算法

在介绍Raft前,先简单说一下关于一致性和共识性的个人理解。很多情况下,这两者都混为一谈。前者描述的是一种状态,即所存储的数据为“一致的”(具体的分类这里不做讨论了);后者描述的则是一种过程,即达成前者“一致性”的过程。

Raft算法即为“为了使分布式系统达到一致性的一种共识算法”,即“分布式一致性共识算法”。当然了,这里稍微理解一下就好,不用过于抠字眼,我们把重点放回到Raft上来。

Raft为一种管理日志复制的共识算法,能够保证在部分servers宕机或网络发生部分割裂的情况下正常工作。相比于Paxos,Raft在保持与其同样高效(efficient)的同时,将算法单独拆分为了几个子部分,使得Raft更易于理解。Raft最核心的内容包括三个部分:领导选举、日志复制、安全性,其具体实现见下图。此外,Raft还可以包括日志压缩,成员变更等。
在这里插入图片描述

图片来源:raft-extended paper fig2

如上图为论文中的Figure2,总结了Raft的所有核心内容的实现细节:领导选举、日志复制以及安全性。根据我自己在MIT6.824中的实现情况看下来,可以说Figure2里的每一个单词,甚至是每一个符号都是精华,没有一个多余的字符。在实现时需要完全一致,否则可能会出现问题。下面的“总览”首先介绍State部分的内容,其余部分会结合后面的其他章节一起介绍。后文中server与“节点”一词表示的含义相同。

总览

Raft使用一种强领导人Strong Leader的机制进行日志的管理,server之间的通信均使用RPC(Remote Procedure Call)的方式,任何server都有可能在领导人、候选人以及跟随者三者之间转换。领导人由选举生成,任何日志都由领导人从客户端client接收,流向follower。即,Follower的任何日志都由领导人发送,Leader负责管理所有server的日志,并响应Client。任何从Client发起的Log请求都会由Leader接收append至自己的log,并复制至Follower上。当Leader确认过半数的server(包括自己)已添加当前log,领导人便会认为当前log已提交并apply、通知Client。

Raft将时间划分为一个个的Term(可以称之为任期,从0开始的整数),在任何一个Term内,至多选出一个Leader。Leader会定时地发送heartbeat(心跳)至所有Follower来“维持”自己的Leader身份。一旦有Follower在一定时间后没有收到Leader的heartbeat,那么就会令自己的Term加1,并转换为Candidate候选人发起投票。

在论文中,每个server可以分为两部分,一部分为State Machine,可以应用(Apply)log中的各种操作。这里如果不好理解,可以简单认为State Machine就是key/value键值对数据库(或者是其他任何数据库,为叙述方便,后文均以k/v举例);另一部分即为实现Raft协议本身的部分,包括log的存储。

Raft中的Log实体(LogEntry)主要包含两个内容,一个为Command,即具体的可运用于State Machine的指令;另一个为Term,表示该Log生成时的任期。

实际实现中每个server都有一个唯一的ID用于区分,由0开始编号,更多细节会在后面的章节展开,了解这些总览后就可以理解Figure2 State部分各个成员的含义:

  1. 需持久化成员,非易失:这部分成员需要进行持久化,以便server恢复时使用,所有server都需有。
    1. currentTerm: server最新的Term,初次启动时为0,单调递增。
    2. votedFor:当前server所投票的候选server,若没有投给任何节点,则初始化为none(实际实现中我初始化为了-1)。
    3. log[]:用于保存LogEntry的结构,索引从1开始
  2. 不需持久化,易失:这部分成员不需要持久化,每次server重启或恢复时,初始化为0即可。所有server都需要有。
    1. commitIndex:最后一个被提交的LogEntry的index,初始化为0,单调递增。
    2. lastApplied:最后一个被应用到State Machine的LogEntry的index,初始化为0,单调递增。

这里解释一下两者的含义即区别。首先是commitIndex,顾名思义,就是指已提交的LogEntry中的最大的index。这里已提交的定义是指当前Term的LogEntry已被复制至过半数的server的log[ ],如果满足这一条件,我们就认为这一条Log为commit的。lastApplied则指某个LogEntry被应用到State Machine上,两者不同显而易见:前者指复制到了log[]中,后者在复制后还需应用到k/v状态机上。

除了以上两部分所有server都需要的成员外,还有两个Leader的成员。显然,Leader要将log复制到所有Follower,那么就需要掌握每个Follower的log情况。因此,有了下面这两个成员:

  1. Leader特有成员,易失
    1. nextIndex[ ]:对每一个server i,下一个需要发送给i的LogEntry的index,初始化为leader最后一个LogEntry的index + 1。可回退,非递增。
    2. matchIndex[]:对每一个server i,i中log[]所存储的LogEntry的最大index,初始化为0,单调递增。

领导选举

Raft中的server由三种身份:Leader、Candidate以及Follower。各身份之间的转换如下图所示:
在这里插入图片描述

图片来源:raft-extended paper fig4

在某一个任期内,如果有follower i在一定时间内没有收到Leader的heartbeat rpc,就会令自己的currentTerm + 1,转变为Candidate、投票给自己并向其他所有servers发送RequestVote RPC发起选举。当所有的RPC reply中有超过半数的server选择投票给自己后,Follower i便转变为Leader。一旦转变为Leader,立刻发送心跳至所有Follower。

这里提到的“一定时间内”其实是Raft的一个计时器,称为选举计时器,所有server都有。Raft中总共有两个计时器,另一个为leader特有的心跳计时器。当Follower在选举计时器超时前没有收到当前Term或更新Term的Leader发来的heartbeat时,就会转变为Candidate候选人并发起选举,同时重置选举计时器。选举期间,如果Candidate没有收到当前Term的新Leader的心跳或更新Term的Leader心跳(如果收到,回退为follower),且没有获得足够多的选票,那么会让自己的currentTerm + 1并发起新一轮的选举。重复这一过程,直到产生新的Leader。可以看出,选举计时器在Follower与Candidate中都需要使用。

另一个计时器为心跳计时器,用于维护心跳周期。领导人在每个心跳周期都会向所有Follower发送heartbeat来“维持”自己的领导地位。

上面的方式其实还有一个问题:分票。即如果所有server的选举计时器均在同一时间超时,所有servers均投票给自己后再发起选举,会导致永远都无法选举出leader的情况产生。为了解决这一问题,Raft使用随机化的方式随机选举计时器的时长。具体的随机化区间视实际情况而定,论文中给出的范围为150ms~300ms。

任何收到RequestVote RPC的server都只有一票投票权。也就是说,对于同一个Term,每个server最多只能投给一个节点。这一点结合上面的“Candidate需要收到过半数server选票方可转换为Leader”,可以确保每个Term最多只能产生一个Leader——这就是Raft算法安全性中的选举安全(Election Safety)

此外,对于每一个server,无论其身份如何,只要收到了Term更大的RPC请求或者RPC Reply,就需要更新自己的Term并转变为Follower。

下面介绍一下Candidate发送的RequestVote RPC相关内容。

  1. 参数Arguments
    1. Term: 候选人的currentTerm
    2. CandidateID:候选人ID
    3. LastLogIndex:候选人最后一个LogEntry的index(log[]的长度,即log[ ]末尾LogEntry的index)
    4. LastLogTerm:LastLogIndex所对应的LogEntry的Term
  2. 回复Reply
    1. Term:接收者的Term,用于更新候选人的Term(如果有必要)
    2. VoteGranted:布尔值,true表示接收者投票给当前候选人
  3. 接收者如何响应,即RequestVote RPC响应逻辑
    1. 如果候选人的Term小于接收者(无论接收者是什么身份),将Term设置为自己的Term,VoteGranted设置为False后返回
    2. 如果接收者的votedFor为none或者为CandidateID,且候选人的日志信息与接收者一样新或更新,设置VoteGranted为True并返回。

这里RequestVote RPC其实涉及到了一部分日志相关的内容,其主要目的是为了保证领导人完备性(Leader Completeness)。详细内容会在安全性保证一节解释。

从这一节其实不难看到,除了上一节介绍的State外,每个server中其实还需要保存计时器以及所有server的数量n(用于统计选票是否过半,以及下面一节的日志是否复制至过半server)。

日志复制

当选举出一个领导人后,Leader就开始为客户端client提供服务。client会发起一个请求,Leader会将该请求包装为一个LogEntry并append至自己的log[]中,之后并行地向所有Followers发送AppendEntries RPC来将这条新的LogEntry复制到Follower的log[]中。当日志被复制到了过半数的servers上后(也就是被commit后),leader会apply这条日志并将结果返回给客户端。Raft保证任何一个Leader都不会覆盖或者删除自己的log[],只会append新的LogEntry——这就是安全性中的领导人只可追加(Leader Append-Only)特性。
在这里插入图片描述

图片来源:raft-extended paper fig6

在“总览”一节提到过LogEntry的结构,由Term以及Command两部分组成,如上图所示。在一切正常的情况下,follower的log[]与leader的不会相差过多。但更多时候会因为故障、网络等问题,导致follower的log[]落后于其他server。Raft的日志复制机制能够保证日志匹配特性(Log Matching),即:

  1. 如果两个server的log[]在同一个Index的LogEntry有同一个Term,那么这个Index对应的LogEntry存储着同样的Command。
  2. 如果两个server的log[]在同一个Index的LogEntry有同一个Term,那么对这两个server的log[]而言,其Index前的所有LogEntry完全一致。

第一条是因为Leader在一个任期里对于给定的log[] Index,只会创建一个LogEntry。并且创建后,该LogEntry不会移动位置。(不会前后移动,但对于follower来讲可能会被删除,后面会提到)。

第二条则是由AppendEntries RPC的一致性检查实现的:当leader发送AppendEntries RPC时,除了附加需要append至follower的LogEntries外,还会附加LogEntries中第一条LogEntry前面一条LogEntry的Term(prevLogTerm)和Index(prevLogIndex)。Follower收到AppendEntries RPC后会检查自己的log[]是否存在对应的prevLogTermprevLogIndex。如果没有,会拒绝Append请求。

Leader对每一个follower会维护两个数组:nextIndex[]matchIndex[]。前者表示对于follower i,下一个需要append的LogEntry的index;后者表示已被成功复制至follower i的log[]中的最大的index。显然,对于给定的i,有prevLogIndex = nextIndex[i] - 1的等式成立。当AppendEntries RPC被拒绝时,领导人会将nextIndex[i]减小,并在下一个RPC中重试。

若follower的log[]中存在对应的prevLogTermprevLogIndex,那么就会执行对应的日志复制操作。之前提到,Raft使用一种强领导人的机制来进行日志的管理。当Follower中的prevLogIndex之后的日志与AppendEntries RPC中的日志出现冲突,那么follower从冲突index开始的所有LogEntries会被删除,然后将AppendEntries RPC中的日志append至follower的log[]中。Leader收到对应的reply后,更新nextIndex[]matchIndex[],并根据LogEntry的复制情况更新自己的commitIndex。

这里有一点需要注意,即心跳的发送也是使用的AppendEntries RPC,只不过对应的需要附加的LogEntries为空。

下面介绍一下AppendEntries RPC的细节:

  1. 参数Arguments
    1. Term: Leader的currentTerm
    2. LeaderID:领导人ID
    3. PrevLogIndex:领导人log[]中需要附加的至follower的新LogEntry前一条日志的index
    4. PrevLogTerm:领导人log[]PrevLogIndex所在LogEntry的Term
    5. Entries[]:需要附加至follower的LogEntries(心跳中为空)
    6. LeaderCommit:领导人的commitIndex
  2. 回复Reply
    1. Term:接受者的Term,用于更新领导人的Term(如果有必要)
    2. Success:布尔值,true表示接受者包含匹配PrevLogIndexPrevLogTerm的LogEntry。
  3. 接收者如何响应,即RequestVote RPC响应逻辑
    1. 如果领导人人的Term小于接收者(无论接收者是什么身份),将Term设置为自己的Term,Success设置为False后返回
    2. 如果接收者在PrevLogIndex位置不包含匹配PrevLogTerm的LogEntry,Success设置为False后返回
    3. PrevLogIndex开始,如果follower中存在与Entries[]冲突的LogEntry(相同index上LogEntry的Term不同),从冲突Index开始删除所有后面的LogEntry
    4. 追加Entries[]中follower不存在的LogEntry至log[]
    5. 如果LeaderCommit > commitIndex,将commitIndex更新为min(LeaderCommit, len(logs[])),关于这一点的理由会在后文的“思考”部分讨论。

个人觉得除了3.5,以上的思路还是很清晰的。注意,在实际实现中,一定要按顺序实现,并且要实现到每一个字,否则代码中很可能出现bug。此外,对每一个server,commitIndex得到更新后,都需要检查是否有commitIndex > lastApplied,及时将新的Log应用到State Machine上。这里结合上面的内容,不难得出一个结论:follower需要apply一个LogEntry,至少需要leader发送两次AppendEntries RPC:第一次返回success后,leader更新自己的commitIndex,并更新第二次AppendEntries RPC中的LeaderCommit。follower收到第二次的AppendEntries RPC后,发现新的LeaderCommit大于自己的commitIndex,更新,apply。

安全性保证

论文中的Figure2分为四个部分,上面的几节详细介绍了三个部分,其实第四个部分“Rules For Servers”已经穿插在前文中进行介绍了,这一节主要介绍一下Raft中的几个安全性保证。

前面我们已经介绍了选举安全Election Safety领导人只可追加(Leader Append-Only)日志匹配特性(Log Matching),Raft中还有两个重要的安全性:领导人完备性(Leader Completeness)状态机安全性(State Machine Safety)

领导人完备性(Leader Completeness):在一个任期中,如果一个LogEntry已经被提交了,那么这个LogEntry将在将来所有任期的leader中永存。
状态机安全性(State Machine Safety):如果一个server已经将index i上的LogEntry应用到了State Machine,那么任何一个其他server对于同样的index i都不会apply一个不同的LogEntry。

领导人完备性保证了一条已被提交的LogEntry不会被删除或覆盖,状态机安全性则保证不同server之间apply的LogEntry在时序上的一致性。这两点不难理解:当一条LogEntry被复制至超半数server后,该LogEntry会被commit,之后随时可能会apply。如果不存在上面两条中的任何一条,那么就可能造成已被commit、apply的LogEntry无效的情况。显然,返回给client无效的结果是不可接受的。

要实现上面的内容,我们需要对领导选举进行额外的限制,以保证新的leader不会覆盖follower中已commit的LogEntry。我们看下面这幅论文中的图:
在这里插入图片描述

图片来源:raft-extended paper fig 8

图中,假设在Term2,S1当选为Leader,并将任期2的LogEntry复制到了S2上,如a所示。之后,S1宕机,任期3 S5当选leader(图b)。S5还没来得及将Term3的LogEntry复制至follower就宕机了,此时S1重启,并重新当选为Leader,将Term2的日志复制到了S2、S3(图c)。由于已经复制到了过半数server,Term2的这条LogEntry被commit。此时,S1在复制Term4的LogEntry宕机,S5重启并重新当选。根据前面的介绍,此时S5会将自己Term3的LogEntry复制至Follower,导致已提交的Term2 LogEntry被覆盖。显然,覆盖已提交的LogEntry是不可接受的。

为此,我们需要对领导选举和LogEntry的commit增加一些限制:

  1. 候选人的Log[]只有在跟接收者的Log[]一样新或者更新的情况下,接受者才有可能投票给候选人。这里的新是指对于两者log[]中的最后一条LogEntry,如果Term不同,Term更大的更新;否则,Index更大的更新(即log[]更长的更新)。
  2. leader只能commit当前Term的LogEntry(顺带commit之前Term的),不能只commit之前Term的LogEntry。

我们回到上面这张图的c部分。因为S1此时为Term4的leader,所以在增加了限制2后,我们不能提交Term2的LogEntry,也就不会将其应用到State Machine。此时,如果发生图d的情况,覆盖的也只是未提交的LogEntry,可接受。而如果S1在图c中在复制完Term2后没有宕机,而是继续复制Term4的LogEntry(图e),那么此时S1可以提交Term4 LogEntry,提交时会将Term2的LogEntry一起提交。之后,如果S1宕机,那么S5也不会被选举为Leader(因为日志不够新),自然无法覆盖已提交的LogEntry。

说白了,这里就是加强了Commit的限制:限制前的LogEntry被提交后可能存在被覆盖的风险。但加了限制后,在这种条件下commit的LogEntry可以保证肯定不会被覆盖。

这两点限制也解释了前文“总览”一节和“领导选举”一节关于commitIndexRequestVote RPC中的介绍。

日志压缩

我们知道,Raft中的log[]不能无限增长,因此需要引入一种日志压缩的方法。论文中推荐的是快照SnapShot的方法,其结构如下所示:
在这里插入图片描述

图片来源:raft-extended paper fig 12
快照的主要思想就是将一部分连续的LogEntry存入至磁盘中,其中包含了被压缩为快照的最后一个LogEntry的index和Term,以及一些State Machine的状态。保存最后一条LogEntry的Index和Term的目的是为了能够在append快照后第一条LogEntry时进行一致性检查。

快照有两点需要注意一下,第一点就是对“日志复制”的影响。由于部分LogEntry被存入快照,因此对所有的server,我们需要增加两个需要持久化的成员:lastIncludedIndexlastIncludeTerm,以标识哪些LogEntry被存入了快照。当某一个follower的PrevLogIndex小于等于lastIncludedIndex时,leader发送对应的InstallSnapShot RPC给follower让follower创建快照。

第二点就是快照的创建是独立的,每一个server都可以创建快照。这一点可能与Raft的强领导性相违背。但论文作者说强领导性的目的是为了达成一致,而快照的创建时,一致已经达成了(Log Matching),所以这一点可以接受。

这一部分其实理解起来并不是非常困难,这里结合自己在MIT6.824中的实现,直接介绍InstallSnapShot RPC简化后对应的内容:

  1. 参数Arguments
    1. Term: Leader的currentTerm
    2. LeaderID:领导人ID
    3. LastIncludedIndex:快照所替换的log[]中最后一条LogEntry的index
    4. LastIncludedTermLastIncludedIndexLogEntry对应的Term
    5. Data[]:快照需存储至磁盘的元数据
  2. 回复Reply
    1. Term:接收者的Term,用于更新领导人的Term(如果有必要)
    2. Success:布尔值,true表示接受者包含匹配PrevLogIndexPrevLogTerm的LogEntry。
  3. 接受者如何响应,即RequestVote RPC响应逻辑
    1. 如果领导人的Term小于接收者(无论接收者是什么身份),将Term设置为自己的Term,Success设置为False后返回
    2. 存储快照文件,并删除任何位于LastIncludedIndex之前的LogEntry(包括LastIncludedIndex
    3. 根据快照文件重置State Machine,设置Success为true后返回。

优化

论文第七页的最后提到了对于nextIndex[i]回退的优化,即从原来的每次回退一个index优化为每次回退整个Term的index,以加速日志复制的过程。在实际情况中,存在follower由于长时间宕机导致落后leader巨量LogEntry的可能(先不考虑压缩)。如果不进行nextIndex[i]回退优化,那么leader与follower之间需要进行数次AppendEntries RPC的沟通才能完成日志的复制,效率较低。这里我结合课程组的建议,讲一下自己的优化方式:

在论文原有的AppendEntries RPC Reply字段基础上,我增加了两个字段:

  1. ConflictIndex:用于更新nextIndex[i]并不是指发生冲突的index
  2. ConflictTerm:发生Conflict的的Term

接收者对于这两个字段的处理封装在AppendEntries RPC接收者处理的第二步中,当PrevLogIndex无法通过一致性检查时,进行如下操作:

  1. 如果PrevLogIndex大于接收者的log[]长度,令ConflictIndex = len(log[])ConflictTerm = -1,返回
  2. 找到第一个冲突的LogEntry,并将ConflictTerm设置为该LogEntry的Term,ConflictIndex设置为上一个Term的最后一个LogEntry的Index。

对于Leader的更新,进行如下的改动:

  1. 如果leader的log[]中包含了ConflictTerm的LogEntry,设置nextIndex[i]为leader的log[]ConflictTerm下一个Term的第一个LogEntry的Index
  2. 如果leader的log[]中不包含了ConflictTerm的LogEntry,设置nextIndex[i]ConflictIndex + 1

注意这里的ConflictIndex并不是指发生冲突的LogEntry的index,可以理解为发生冲突的LogEntry的上一个Term的最后一个LogEntry的index。这么讲有些抽象,举几个例子:

在这里插入图片描述
如图,为follower的log[]长度小于PrevLogIndex的情况。初始化时nextIndex[i]为6,PrevLogIndex为5。返回中ConflictTerm为-1,ConflictIndex为3。根据上面的规则,将nextIndex[i]更新为4,PrevLogIndex更新为3。

在这里插入图片描述

如图,初始化时nextIndex[i]为8,PrevLogIndex为7。返回中ConflictTerm为2,ConflictIndex为1。根据上面的规则,leader的log[]中存在Term为2的LogEntry,因此,将nextIndex[i]更新为下一个Term的第一个LogEntry的Index,即为4,PrevLogIndex更新为3。

在这里插入图片描述
如图,初始化时nextIndex[i]为6,PrevLogIndex为5。返回中ConflictTerm为3,ConflictIndex为3。根据上面的规则,leader的log[]中不存在Term为3的LogEntry,因此,将nextIndex[i]更新为ConflictIndex + 1,即为4,PrevLogIndex更新为3。

其他

基本上,Raft的主要内容除了成员变更外,已全部介绍了一遍。但还有一个关于时间的细节:除了两个计时器外,我们还要考虑两个时间:一个为server之间RPC的通信时间broadcastTime,另一个为server每次failure之间的平均时间MTBF。显然,我们的计时器时间不能小于broadcastTime,也不能大于MTBF。论文中说一般的broadcastTime时间在[0.5ms, 20ms]之间,所以计时器时间可以取10毫秒到500毫米之间。而典型MTBF时间一般按月算,满足要求。

这里其实我更想提的时两个计时器之间的关系。显然,心跳计时器的时间一定要比选举计时器短,否则容易造成不停地发起选举。同时,考虑到一些网络不可靠的因素,我们要尽最大可能地保证在一个选举周期内能够有心跳发送到follower。所以,在我的实现中,将选举计时器的最小值设定为了心跳计时器的三倍,保证一个选举周期内能够有三个心跳发出。(如果三个都因为网络原因丢了,那就老老实实重选吧_(:_」∠)_

一些思考

这一节写了一些自己在学习Raft过程中的思考,如有疑问或者不同意见,欢迎讨论~

为什么需要Leader?

个人感觉leader的存在简化了server之间的通信:在确立了leader后,所有server只需要与leader通信,彼此之间并不需要发送rpc,减少了通信开销。此外,确立leader以后也简化了客户端的与集群之间的交流。

为什么需要Candidate?

如果没有候选人这个状态,直接从Follower转换为Leader的话,容易造成一个Term出现多个Leader的情况,发生脑裂现象。

那么,可不可以在发起选举时,继续保持Follower的身份,在获得足够选票后再成为leader?

这个问题,我的想法是更多地在实际代码实现上会方便点:因为Candidate在选举时也要维护选举计时器,到计时器结束时,需要判断当前是否依旧为候选人来决定是否发起下一轮选举。(好像有点牵强ಠ_ಠ

为什么不能简单地选择Log最长的server为Leader?

直接上图:
在这里插入图片描述
总共三个servers,S0在Term2当选,append自身一个LogEntry后crash。在Term3 restart后又重新当选,append自身一个LogEntry后又crash。Term4 S1当选,并append一个LogEntry至过半数的节点上。此时S1 crash,如果在Term5当选,那么已提交的Term4 LogEntry将会被覆盖,违背我们上文提到的安全性原则。

为什么log[]索引从1开始

这一点可能是为考虑其他一些State成员:如果索引从0开始,那么commitIndexlastApplied等均需要从-1开始,并且对于空log[]的一致性检查而言,从1开始可以让PrevLogIndex初始化为0。

为什么要分commitIndexlastApplied

因为在一条LogEntry被复制到过半数的server上与一条LogEntry被应用到这些server的状态机上有时间差,这就造成log[]的长度与实际apply的index之间有差值,所以需要分两个值来维护。那么为什么不能直接使用commitIndexlen(log[])来维护这个差值呢?因为len(log[])的值可能会回退,不可靠。

关于两者之间的区别,上文“总览”部分有介绍。

为什么要分nextIndex[i]matchIndex[i]

两者之间不是简单-1的关系,前者会backward,表示一种乐观估计,考虑性能;后者必须递增,表示一种悲观估计,考虑安全性。

为什么在AppendEntries RPC的最后一步需要取LeaderCommitlen(logs[])的较小值

我们知道,每台server中都有一个State Machine,这也就意味着每台server都会进行log的apply。假设LeaderCommit更大,我们取较大值进行commitIndex的set,显然会导致apply错误(logs[]越界)。假设len(logs[])更大,由于follower存在日志被覆盖的可能,会导致已apply的LogEntry无效。

为什么commitIndex为易失,nextIndex[i]matchIndex[i]呢?

首先,我们明确,当一个server从failure中恢复回来以后,其身份肯定为follower。因此,此时可以通过leader的rpc来更新自己的的commitIndexnextIndex[i]matchIndex[i]更甚,因为这两者为Leader特有的state成员。

何时重置选举计时器?

我们只在三种情况下重置选举计时器:

  1. Follower收到一个AppendEntries RPC请求时
  2. Follower投票给一个Candidate时
  3. Candidate发起新的选举时

其他任何情况都不要重置选举计时器。

是否有Command可以不提交

可以,例如上文中安全性保证一节中Term2的LogEntry。不过一般情况下,client会重试没有提交的command。

后记

写完Lab2后,真的受益匪浅。在了解Raft之前,一直都以为Raft是一种复杂、高大上的分布式算法,遥不可及。但实际接触后,才发现并没有想象中的那么难以触摸——很多事情都是自己给自己设限。如果想尝试,不妨试一试。

前前后后,写这篇文章,花了一万六千个字,九个小时的时间。不敢说毫无纰漏,但每一个字确实都花费了精力。尽管如此,还有些想写内容没写,例如一些case study、成员变更等,部分内容会放在后面的Lab实现文章中。如果能够对你有帮助,那就值得。

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值