关于raft算法的指导(6.824 guide 自己的翻译 + 添加部分理解)
最关键点是 figure 2 是必须满足,而不是最好满足,也就是must 满足而不是should 满足。
任何分布式共识算法的细节是最重要也是最难的。当所有的服务器、网络等外部因素都是没有错误的情况下,共识算法都是很好理解的,但是实际的应用中,算法必须考虑到每一个可能发生的错误,而这里面的细节就是相当复杂的。
实现Raft
figure 2指定了 Raft 服务器之间的 RPC 的行为,给出了服务器必须维护的各种不变量,并指定了何时应发生某些操作。我们将在本文的其余部分详细讨论figure 2。必须严格遵守。
最关键点是 figure 2 是必须满足,而不是最好满足,也就是must 满足而不是should 满足。例如,初步读完论文后,你可能认为 任何服务器收到AppendEntries RPC
或者 RequestVote RPC
后都会重置election timeout
定时器。但是实际上文章中是这样准确描述的:
如果follower 在 选举超时 时间内没有收到
AppendEntries RPC
或者 没有 同意投票给另外一个candidate,那么这个follower 就开启新的选举。
显然原文的内容是准确的,当收到RequestVote RPC
后,follower并不会无条件的重置选举超时时间,而是只有同意给到来的RPC投票时才会重置。例如,如果到来的RPC的term小于currentTerm,那么就不会投票,并且也不会重置选举超时时间。
一些实现上的细节
这里会举几个可能会让大伙困惑的例子。初读论文,有人可能会把 没有log entry的心跳 AppendEntries RPC
与有log entry的RPC分开处理。对于前者,服务器收到心跳后直接重置 选举超时时间,然后返回true。这是错误的,因为心跳RPC的作用不仅仅时让follwer重置选举时间,它还要根据Leader的prelogIndex来判断当前sever的log是否匹配上了,如果没有匹配上,那么就要返回false。不然一味的返回true,会让Leader误以为log entry已经复制到大部分的机器上去了。
在完成上面的要求后,还有另外一个问题。例如,有的人会在 prelogIndex 和 prelogterm 都正确匹配后,将server这个log后面所有的log entries全部截断,然后添加新的log。 这是不对的,看看原文的说法:
如果存在任何的entry冲突(例如,相同的index 但是不同的term),那么就这个entry和后面的所有的entry全部删除
原文中有一个假设,也就是存在冲突才会全部删除。因为如果有的心跳RPC由于网络等问题,时过时的心跳RPC,但是有没有错误,这时如果我们截断后面的entry,会导致丧失了很多entry,而leader不知道。此外如果此时RPC args中的字段匹配,那么就需要继续往后面遍历,直到找到不匹配的,或者到任何一个entries最后,然后进行添加以及返回。
Debugging Raft
在调试的过程,通常有四个主要的bug来源:活锁(空转),不正确的RPC handler处理,没有遵循规则,以及term混乱。此外死锁也是一个问题之一,可以将所有的获取锁的过程 log出来,就可以找到。
活锁 (空转)
活锁就是Ratf中的所有server服务器都在运转,但是要么一直没有leader被选出(我有遇到过),要么leader一被选出就马上退位,没有进行实质上的操作。有很多原因会导致这个问题,这里举几个例子:
-
需要正确的重置选取超时的时间。有且仅有如下的三种情况需要重置选举超时时间:a) 获得当前leader的
AppendEntries RPC
(注意,如果term 过期的话,不会重置,这不是来自自己认为的当前的leader);b) 服务器正在开始选举;c) 服务器同意了requestRPC
投票请求。最后一点在前面强调过。 -
正确知道何时开始一个选举。特别是如果一个服务器正在选举,但是选举时间太长(可能是RPC网络出现问题),选举超时又一次触发,那么就要开始新的选举了。
-
确保满足 figure 2 中的这条规则:
在所有的RPC 的请求或回复中,当term > currenTerm,也就是一旦传来的term大于(注意是严格大于)自己当前的term,那么就立马转变成follewer,并且同步自己的term,同时重置votedfor (踩过坑)
不正确的 RPC handlers
- 如果当handler的某一个步骤决定了要返回false,那么就立刻返回,不要执行后面的动作
- 如果收到的
AppendEntries RPC
中prevlogIndexl
超过了自己的log长度,那么就当作自己有这个logindex的log但是他们的term不一样,所以就是要返回false,让leader继续减少nextIndex - 记得对心跳
AppendRpc
也要进行上面的检查 - 图二中
AppendEntries RPC
中的 第5条建议是重要的,注意这里面的min。commitIndex更新的位置除了与Leadercommit有关外,还和这个RPC发送的最新logs有关系,不能认为此时的commitindex应该要么是leadercommit 要么是服务器的最后一个log。这是因为如果leader发送的条目 在本server中能够完全匹配,但是server中后续的一些log是与leader不同的,而leadercommit刚好又覆盖到了这些log,如果这些log 被提交那么就出现了错误,所以这些log不能够commit。(踩坑,我直接使用min(Leadercommit, len(rf.logs)出错 )) - 记得准确的判断 log entry 最新的条件,不要偷懒只判断index或者长度,term也是很重要的
不遵循相关规则
注意相关的rules:
- 一旦
commitIdex > lastApplied
就可以 去执行中间的log,当然 也不不需要立刻执行。最好有一个专门的applier 处理器单独使用一个协程来处理,处理时要用锁锁上。(可能只需要在读取的时候锁上,应用的时候应该不需要锁) - 要么确保 定期的检查 commitIdex > lastApplied ,要么在commitindex更新后去检查。(感觉前者实现更加简单,就是循环时间需要再看看)
- 如果Leader发送出RPC后被False,并且不是因为日志冲突,也就是出现了reply中的term 大于 currentterm,leader的term过时,那么leader立马下台,并且不要改变nextIndex,不然可能会跟自己再次当选重置nextIndex有race的风险。(感觉这条用处不大,我统统用大锁)
- 在本次任期内的log没有正确的commit的时候,leader无法将之前的log认为commit,也就是刚上任的leader无法确保之前任期的term有没有commit,只有在自己任期中有log能够commit了,才可以准确覆盖之前的log。(Leader只能够知道自己当前的term是最新的,但是无法知道前一个term是否也是最新的,详情可以参考我记录的Raft笔记)
此外 还有一个普遍令人困惑的点是关于
nextIndex
和matchIndex
之间的区别。 我一度认为(现在暂时还这么认为,但是后面可能会改变)matchIndex = nextIndex - 1
,所以matchIndex 没有存在的必要,所以可以不用实现都可以。但是这是不安全的。尽管两个的更新会最终处于一个固定的差值,但是两者的目的是完全不同的。nextIndex是Leader对follower具有相同前缀log的猜测是很乐观的,甚至初始化就设置为自己最后一个log的后面一位。 发送log 的时候 需要根据nextIndex 来发送。
matchIndex是为了安全考虑,是Leader对follower具有相同前缀log的保守估计,matchIndex 决定了commitIndex,所以不能激进,要确保正确。这也就是为什么matchIndex最开始初始化为-1。判断一个log是否被复制到大部分的server中的时候用matchIndex。
Term 混乱
上述内容以及图二中很好的说明了,当收到了RPC回复中有旧的term时要做什么。但是一个比较严重的问题是,当收到旧的RPC要做什么? 注意这里旧term 与 旧RPC 是不一样的。 旧RPC指的是在当前任期内收到 之前任期发送的RPC请求,也就是currentTerm > 发送RPC时参数中的term,此时简单的做法是直接返回,不用管他。
同样的,类似的问题可能出现在收到response后对matchIndex 的更新上。在收到true的respons后,一个可能的操作是 matchIndex = nextIndex - 1
, or matchIndex = len(log)
,但是这是可能是错误的,因为在接收和发送RPC之间,服务器可能会有其他的更新操作,所以我们不能直接的使用服务器当前的字段来更新,而应该用我们发送RPC时传入的参数来进行更新相关的操作。
上面都是一类问题,也就是用RPC参数来进行判断与更新,而不是利用当前的参数