2B中最主要的任务就是进行日志的复制。Raft是一个强领导人的系统,这意味着所有的日志添加都是由领导人发起的,与之相类似的,还有很多其他的结论(它们都是比较显然的),读者可以自行证明。
我们可以这样地分解复制日志的过程
我们首先需要完善Raft结构体的内容。
我们对新加入的内容进行解释:
commitIndex
:这代表了每一个Raft中最高的已经提交的日志下标,它表示对于这个下标以内的所有日志,我们都已经和领导达成共识。可以想象,它的更新是在收到RPC的时候完成的,因为如果领导人提交了某一条指令,且某个追随者有这条指令,那么就可以认为是达成共识的了,后续可以进行应用等操作。
lastApplied
:这是一个紧跟commitIndex
的成员,他指代的是每一个Raft中已经应用到状态机中的日志下标的最大值。我们会对每个Raft有一个轮询(至少我是这么实现的),如果lastApplied<commitIndex
,那么就把这个区间内的所有指令应用到复制状态机上。
nextIndex
(对于领导人):这代表的是对于每个成员,下一组要发的指令的下标的最小值,可以想象它代表着领导人和每个成员在哪些日志上达成了一致,它的更新应该是在AppenEntriesRPC
中实现的。
matchIndex
(对于领导人):这代表的是,对于每个成员,和领导人达成一致的下标的最大值(当然,你可以期待一般matchIndex=nextIndex-1
)。我们通过它来决定要提交到哪一条指令(也即大部分人matchIndex
的下界)。
经过上层服务器的多次尝试,我们终于在领导人这里加了一条日志:我们需要完成Start()
函数,它的参数为一条指令(一个空的interface),返回的是这条指令在领导人这边的日志编号,领导人的任期,和这个人是否是领导人。同时,在Start函数中我们也要显式地向每一个追随者发一条心跳(其实就是AppendEntriesRpc
).
类似于MakeElection
函数,我们需要完成launchAppendEnries()
这一函数,并处理回复信息。
我们需要完成AppendEntries()
函数,在里面完成RPCHandler
的功能。
我们需要完成上文中提到的轮询,对每一个Raft开一个Go程来检查要不要提交某一些指令,如果是的话,那就加到applyCh
内(这个applyCh
也需要自己完成)。
在Start
中,我们需要:
增加log
改变lastLogIndex, lastLogTerm
开Go程来发送信息:go rf.launchAppendEntries(peerId)
我们首先需要发送AppendEntriesArgs
,然后需要考虑AppendEntriesReply
,具体来说:
需要遵循Rules For All Servers,如果自己的任期变大,不需要处理;如果Reply的任期更高,变成追随者......(建议参考raft-extended)
如果回复显示成功,那么把nextIndex, matchIndex
都增加,并考察commitIndex
是否也可以增加(看看是不是有一半的matchIndex
)都超过了某一个值
如果回复显示失败,我们就把nextIndex[peerId]
减少一,来向下匹配。(在后文中我们会看到对于这一点的明显优化)
这是2B中的核心内容,我们在这里考察论文中的图示:
这个比较简单,只要知道“信道”是啥基本就可以了。
在上面这张图中,Reply false的意思是直接返回。
这里第三条的conflicts with的实现需要细细思考。如果接收者的日志中的每一条都是正确的,那么不需要截断(我到2C才发现这个)
对于Rules For All Server而言,如果变成Follower,不需要返回。这一点上的处理是和2A一致的。