从十一月开始,前前后后花了刚好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还可以包括日志压缩,成员变更等。
如上图为论文中的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部分各个成员的含义:
- 需持久化成员,非易失:这部分成员需要进行持久化,以便server恢复时使用,所有server都需有。
currentTerm
: server最新的Term,初次启动时为0,单调递增。votedFor
:当前server所投票的候选server,若没有投给任何节点,则初始化为none(实际实现中我初始化为了-1)。log[]
:用于保存LogEntry的结构,索引从1开始。
- 不需持久化,易失:这部分成员不需要持久化,每次server重启或恢复时,初始化为0即可。所有server都需要有。
commitIndex
:最后一个被提交的LogEntry的index,初始化为0,单调递增。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情况。因此,有了下面这两个成员:
- Leader特有成员,易失
nextIndex[ ]
:对每一个server i,下一个需要发送给i的LogEntry的index,初始化为leader最后一个LogEntry的index + 1。可回退,非递增。matchIndex[]
:对每一个server i,i中log[]
所存储的LogEntry的最大index,初始化为0,单调递增。
领导选举
Raft中的server由三种身份:Leader、Candidate以及Follower。各身份之间的转换如下图所示:
在某一个任期内,如果有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
相关内容。
- 参数Arguments
Term
: 候选人的currentTermCandidateID
:候选人IDLastLogIndex
:候选人最后一个LogEntry的index(log[]
的长度,即log[ ]
末尾LogEntry的index)LastLogTerm
:LastLogIndex所对应的LogEntry的Term
- 回复Reply
Term
:接收者的Term,用于更新候选人的Term(如果有必要)VoteGranted
:布尔值,true表示接收者投票给当前候选人
- 接收者如何响应,即RequestVote RPC响应逻辑
- 如果候选人的Term小于接收者(无论接收者是什么身份),将
Term
设置为自己的Term,VoteGranted
设置为False后返回 - 如果接收者的
votedFor
为none或者为CandidateID
,且候选人的日志信息与接收者一样新或更新,设置VoteGranted
为True并返回。
- 如果候选人的Term小于接收者(无论接收者是什么身份),将
这里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)特性。
在“总览”一节提到过LogEntry的结构,由Term以及Command两部分组成,如上图所示。在一切正常的情况下,follower的log[]
与leader的不会相差过多。但更多时候会因为故障、网络等问题,导致follower的log[]
落后于其他server。Raft的日志复制机制能够保证日志匹配特性(Log Matching),即:
- 如果两个server的
log[]
在同一个Index的LogEntry有同一个Term,那么这个Index对应的LogEntry存储着同样的Command。 - 如果两个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[]
是否存在对应的prevLogTerm
和prevLogIndex
。如果没有,会拒绝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[]
中存在对应的prevLogTerm
和prevLogIndex
,那么就会执行对应的日志复制操作。之前提到,Raft使用一种强领导人的机制来进行日志的管理。当Follower中的prevLogIndex
之后的日志与AppendEntries RPC
中的日志出现冲突,那么follower从冲突index开始的所有LogEntries会被删除,然后将AppendEntries RPC
中的日志append至follower的log[]
中。Leader收到对应的reply后,更新nextIndex[]
和matchIndex[]
,并根据LogEntry的复制情况更新自己的commitIndex。
这里有一点需要注意,即心跳的发送也是使用的AppendEntries RPC
,只不过对应的需要附加的LogEntries为空。
下面介绍一下AppendEntries RPC
的细节:
- 参数Arguments
Term
: Leader的currentTermLeaderID
:领导人IDPrevLogIndex
:领导人log[]
中需要附加的至follower的新LogEntry前一条日志的indexPrevLogTerm
:领导人log[]
中PrevLogIndex
所在LogEntry的TermEntries[]
:需要附加至follower的LogEntries(心跳中为空)LeaderCommit
:领导人的commitIndex
- 回复Reply
Term
:接受者的Term,用于更新领导人的Term(如果有必要)Success
:布尔值,true表示接受者包含匹配PrevLogIndex
和PrevLogTerm
的LogEntry。
- 接收者如何响应,即RequestVote RPC响应逻辑
- 如果领导人人的Term小于接收者(无论接收者是什么身份),将
Term
设置为自己的Term,Success
设置为False后返回 - 如果接收者在
PrevLogIndex
位置不包含匹配PrevLogTerm
的LogEntry,Success
设置为False后返回 - 从
PrevLogIndex
开始,如果follower中存在与Entries[]
冲突的LogEntry(相同index上LogEntry的Term不同),从冲突Index开始删除所有后面的LogEntry - 追加
Entries[]
中follower不存在的LogEntry至log[]
- 如果
LeaderCommit > commitIndex
,将commitIndex
更新为min(LeaderCommit, len(logs[]))
,关于这一点的理由会在后文的“思考”部分讨论。
- 如果领导人人的Term小于接收者(无论接收者是什么身份),将
个人觉得除了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。我们看下面这幅论文中的图:
图中,假设在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增加一些限制:
- 候选人的
Log[]
只有在跟接收者的Log[]
一样新或者更新的情况下,接受者才有可能投票给候选人。这里的新是指对于两者log[]
中的最后一条LogEntry,如果Term不同,Term更大的更新;否则,Index更大的更新(即log[]
更长的更新)。 - 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可以保证肯定不会被覆盖。
这两点限制也解释了前文“总览”一节和“领导选举”一节关于commitIndex
和RequestVote RPC
中的介绍。
日志压缩
我们知道,Raft中的log[]
不能无限增长,因此需要引入一种日志压缩的方法。论文中推荐的是快照SnapShot的方法,其结构如下所示:
快照有两点需要注意一下,第一点就是对“日志复制”的影响。由于部分LogEntry被存入快照,因此对所有的server,我们需要增加两个需要持久化的成员:lastIncludedIndex
与lastIncludeTerm
,以标识哪些LogEntry被存入了快照。当某一个follower的PrevLogIndex
小于等于lastIncludedIndex
时,leader发送对应的InstallSnapShot RPC
给follower让follower创建快照。
第二点就是快照的创建是独立的,每一个server都可以创建快照。这一点可能与Raft的强领导性相违背。但论文作者说强领导性的目的是为了达成一致,而快照的创建时,一致已经达成了(Log Matching),所以这一点可以接受。
这一部分其实理解起来并不是非常困难,这里结合自己在MIT6.824中的实现,直接介绍InstallSnapShot RPC
简化后对应的内容:
- 参数Arguments
Term
: Leader的currentTermLeaderID
:领导人IDLastIncludedIndex
:快照所替换的log[]
中最后一条LogEntry的indexLastIncludedTerm
:LastIncludedIndex
LogEntry对应的TermData[]
:快照需存储至磁盘的元数据
- 回复Reply
Term
:接收者的Term,用于更新领导人的Term(如果有必要)Success
:布尔值,true表示接受者包含匹配PrevLogIndex
和PrevLogTerm
的LogEntry。
- 接受者如何响应,即RequestVote RPC响应逻辑
- 如果领导人的Term小于接收者(无论接收者是什么身份),将
Term
设置为自己的Term,Success
设置为False后返回 - 存储快照文件,并删除任何位于
LastIncludedIndex
之前的LogEntry(包括LastIncludedIndex
) - 根据快照文件重置State Machine,设置
Success
为true后返回。
- 如果领导人的Term小于接收者(无论接收者是什么身份),将
优化
论文第七页的最后提到了对于nextIndex[i]
回退的优化,即从原来的每次回退一个index优化为每次回退整个Term的index,以加速日志复制的过程。在实际情况中,存在follower由于长时间宕机导致落后leader巨量LogEntry的可能(先不考虑压缩)。如果不进行nextIndex[i]
回退优化,那么leader与follower之间需要进行数次AppendEntries RPC
的沟通才能完成日志的复制,效率较低。这里我结合课程组的建议,讲一下自己的优化方式:
在论文原有的AppendEntries RPC
Reply字段基础上,我增加了两个字段:
ConflictIndex
:用于更新nextIndex[i]
,并不是指发生冲突的indexConflictTerm
:发生Conflict的的Term
接收者对于这两个字段的处理封装在AppendEntries RPC
接收者处理的第二步中,当PrevLogIndex
无法通过一致性检查时,进行如下操作:
- 如果
PrevLogIndex
大于接收者的log[]
长度,令ConflictIndex = len(log[])
,ConflictTerm = -1
,返回 - 找到第一个冲突的LogEntry,并将
ConflictTerm
设置为该LogEntry的Term,ConflictIndex
设置为上一个Term的最后一个LogEntry的Index。
对于Leader的更新,进行如下的改动:
- 如果leader的
log[]
中包含了ConflictTerm
的LogEntry,设置nextIndex[i]
为leader的log[]
中ConflictTerm
下一个Term的第一个LogEntry的Index - 如果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开始,那么commitIndex
,lastApplied
等均需要从-1开始,并且对于空log[]
的一致性检查而言,从1开始可以让PrevLogIndex
初始化为0。
为什么要分commitIndex
和lastApplied
因为在一条LogEntry被复制到过半数的server上与一条LogEntry被应用到这些server的状态机上有时间差,这就造成log[]
的长度与实际apply的index之间有差值,所以需要分两个值来维护。那么为什么不能直接使用commitIndex
与len(log[])
来维护这个差值呢?因为len(log[])
的值可能会回退,不可靠。
关于两者之间的区别,上文“总览”部分有介绍。
为什么要分nextIndex[i]
和matchIndex[i]
两者之间不是简单-1
的关系,前者会backward,表示一种乐观估计,考虑性能;后者必须递增,表示一种悲观估计,考虑安全性。
为什么在AppendEntries RPC
的最后一步需要取LeaderCommit
和len(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来更新自己的的commitIndex
。nextIndex[i]
和matchIndex[i]
更甚,因为这两者为Leader特有的state成员。
何时重置选举计时器?
我们只在三种情况下重置选举计时器:
- Follower收到一个
AppendEntries RPC
请求时 - Follower投票给一个Candidate时
- Candidate发起新的选举时
其他任何情况都不要重置选举计时器。
是否有Command可以不提交
可以,例如上文中安全性保证一节中Term2的LogEntry。不过一般情况下,client会重试没有提交的command。
后记
写完Lab2后,真的受益匪浅。在了解Raft之前,一直都以为Raft是一种复杂、高大上的分布式算法,遥不可及。但实际接触后,才发现并没有想象中的那么难以触摸——很多事情都是自己给自己设限。如果想尝试,不妨试一试。
前前后后,写这篇文章,花了一万六千个字,九个小时的时间。不敢说毫无纰漏,但每一个字确实都花费了精力。尽管如此,还有些想写内容没写,例如一些case study、成员变更等,部分内容会放在后面的Lab实现文章中。如果能够对你有帮助,那就值得。