写完Lab2B后搞大论文,又回来做的Lab2C
在Lab2B基础上简单实现persist后无法通过测试样例Test (2C): Figure 8
。根据raft图2的最后一个提示,更新commitIndex
到i
的时候要校验log[i-1].Term == currentTerm
。这个原理(可能包括raft整个文章)要重新整理下。
但是接下来TestFigure8Unreliable2C
卡了很长时间(可能断断续续有一个礼拜了),就是所谓的样例Test (2C): Figure 8 (unreliable) ...
。这里把原文细节(重点是Fig8的细节和原文5.4节的逻辑)和Students’ Guide to Raft又过了一下,也改进了一些点。
- 收到
AppendEntries
包后,就算对上了PrevLogIndex
,也要:- 加标志位,判断该Term更新过至少一次的话,是不是过时的AppendEntries包,使得log变短。
- 是否甚至是其他Term的过时包:
- 如果
PrevLogIndex
加args.Entries
少等于已有log,只检查最后的args.Entries
对应位置条目的Term,与已有log不一致则替换 - 如果
PrevLogIndex
加args.Entries
多于已有log,直接替换
- 如果
- 收到的是
AppendEntries
包,commitIndex更新的大小不能高于本次args.PrevLogIndex + len(args.Entries)
- 日志对齐函数中,取消过时包:
-
reply.Success == true
,matchIndex
不能降低 - (
reply.Success == false
的逻辑没注意,下面就是这个问题)
-
- 选举计票优化了下,不用等所有票投完,只有赞成票和拒绝票都不够majority才继续
cond.Wait()
,同时要判断当前还是Candidate、Term没变
但还是不行,也就是像解决Lab2B问题时那样不依赖调试是不行了,然后搜了一下TestFigure8Unreliable2C
,似乎个人的问题都不同。不过还是得到一些提示,虽然已经开始阅读甚至改造config.go
和test_test.go
了,但是有个githubIssue总结得确实好一点:
TestFigure8Unreliable2C 这测试用例一共运行不超过40s。前10秒在可靠网络各种插入数据。中间20秒,在不可靠网络中各种插入数据。最后10s,开始的时候,恢复网络,插入一个数据,等待这个数据被提交,如果超过这10s就报错。
这里本人补充一点:最后校验时必须所有节点(5个)全部提交(提交到applyCh
)。
这里改进了无数次调试输出信息,需要注意几组关键值,还有些方便观察的技巧:
- 注意每个节点的
rf.lastApplied
,rf.commitIndex
,len(rf.log)
- 注意所有时刻的
rf.matchIndex
和rf.nextIndex
最终发现我最后的问题是,在最后一个Term中,Leader已经对齐某节点的matchIndex
和nextIndex
后收到过时包,导致nextIndex
单独回退,使得后续不给该节点发AppendEntries了。(因为我的实现中新当选后从节点日志对齐和日志发送是两个函数,日志对齐函数理论上所有节点都对齐就结束了)所以问题还是没有在一处细节上处理好过时包。
注意,使用命令可单独测试某组样例:
go test -run TestFigure8Unreliable2C
接下来又遇到了问题,TestReliableChurn2C
和TestUnreliableChurn2C
过不了,总是报错如:
apply error: server 1 apply out of order 139
可以发现,这个报错是对单个节点的,没有所有节点都要一致的要求,排查下来发现就是日志复制太慢,但这个现象可能有多方面原因,这里观察日志,发现这两组样例似乎日志更新粒度很小,包很多,而我在上一个TestFigure8Unreliable2C
中为了加快commitIndex
更新自己设计了个小trick,就是Leader的commitIndex
更新后马上对log对齐的从节点发一轮带commitIndex
的心跳包。把这个取消后TestReliableChurn2C
这个样例就能过了。
但是其实在之后在2C实验整个测试时还是有部分过不了。这里就想起来前面看的springfieldking/mit-6.824-golabs-2018/issues/1。调整了时间参数,最终参数如下:
- 选举超时:200~400ms
- 心跳间隔:100ms
- 日志对齐包间隔:40ms
- 日志复制包间隔:50ms
这里有些方便调试的技巧记录下:
// 看日志具体内容对没对上?每次只看后5个即可
logPrintInd := len(log) - 5
if logPrintInd < 0 {
logPrintInd = 0
}
rf.LabDPrintf(3, "M[%v] T[%v],log[%v]: %v A[%v:%v]",
rf.membership, rf.currentTerm, len(log), log[logPrintInd:],
rf.lastApplied, rf.commitIndex)
// 刚成为Leader后的信息
voteCounter := make([]int, len(rf.peers))
for i:=0; i<len(rf.peers); i++ {
voteCounter[i] = 0
}
rf.LabDPrintf(3, "be Leader of Term[%v] log[%v:%v:%v] lastT[%v] vote[%v]",
rf.currentTerm, rf.lastApplied, rf.commitIndex, len(rf.log),
lastLogTerm, voteCounter)
// 查看Leader的commitIndex更新时的信息
rf.LabDPrintf(3, "M[%v] T[%v] update commit [%v] to [%v] with [%v][%v]",
rf.membership, rf.currentTerm, rf.commitIndex, newCommitIndex,
rf.matchIndex, rf.nextIndex)
// 项applyCh发送startIndex到endIndex信息。降低调试信息数量
if endIndex - startIndex + 1 <= 3 || i == startIndex - 1 || i == endIndex - 1 {
rf.LabDPrintf(3, "T[%v] apply [%v:%v] with log[%v]:[%v]",
rf.currentTerm, startIndex, endIndex, i + 1, applyMsg)
}
// 控制打印函数 Lab2C : 3
const LabDebug = 3
func (rf *Raft) LabDPrintf(labInd int, format string, a ...interface{}) {
// 分lab打印调试语句,对PeersDPrintf函数进行包装
if labInd != LabDebug {
return
}
content := fmt.Sprintf(format, a...)
number := strconv.Itoa(rf.me)
blankLeft := ""
blankRight := ""
for i:=0;i<rf.me;i++ {
blankLeft = blankLeft + " "
}
for i:=rf.me+1;i<len(rf.peers);i++ {
blankRight = blankRight + " "
}
blankRight = blankRight + " "
// DPrintf("Peers " + blankLeft + number + blankRight + content)
DPrintf("Peers " + blankLeft + number + blankRight + content)
}