【分布式】MIT 6.824 Lab 2C实现细节分析

写完Lab2B后搞大论文,又回来做的Lab2C

在Lab2B基础上简单实现persist后无法通过测试样例Test (2C): Figure 8。根据raft图2的最后一个提示,更新commitIndexi的时候要校验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的过时包:
      • 如果PrevLogIndexargs.Entries 少等于已有log,只检查最后的 args.Entries 对应位置条目的Term,与已有log不一致则替换
      • 如果PrevLogIndexargs.Entries 多于已有log,直接替换
  • 收到的是AppendEntries包,commitIndex更新的大小不能高于本次args.PrevLogIndex + len(args.Entries)
  • 日志对齐函数中,取消过时包:
    • reply.Success == truematchIndex不能降低
    • reply.Success == false 的逻辑没注意,下面就是这个问题)
  • 选举计票优化了下,不用等所有票投完,只有赞成票和拒绝票都不够majority才继续cond.Wait(),同时要判断当前还是Candidate、Term没变

但还是不行,也就是像解决Lab2B问题时那样不依赖调试是不行了,然后搜了一下TestFigure8Unreliable2C,似乎个人的问题都不同。不过还是得到一些提示,虽然已经开始阅读甚至改造config.gotest_test.go了,但是有个githubIssue总结得确实好一点:

TestFigure8Unreliable2C 这测试用例一共运行不超过40s。前10秒在可靠网络各种插入数据。中间20秒,在不可靠网络中各种插入数据。最后10s,开始的时候,恢复网络,插入一个数据,等待这个数据被提交,如果超过这10s就报错。

这里本人补充一点:最后校验时必须所有节点(5个)全部提交(提交到applyCh)。
这里改进了无数次调试输出信息,需要注意几组关键值,还有些方便观察的技巧:

  • 注意每个节点的rf.lastApplied,rf.commitIndex,len(rf.log)
  • 注意所有时刻rf.matchIndexrf.nextIndex

最终发现我最后的问题是,在最后一个Term中,Leader已经对齐某节点的matchIndexnextIndex后收到过时包,导致nextIndex单独回退,使得后续不给该节点发AppendEntries了。(因为我的实现中新当选后从节点日志对齐和日志发送是两个函数,日志对齐函数理论上所有节点都对齐就结束了)所以问题还是没有在一处细节上处理好过时包。
注意,使用命令可单独测试某组样例:

go test -run TestFigure8Unreliable2C

接下来又遇到了问题,TestReliableChurn2CTestUnreliableChurn2C过不了,总是报错如:

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)
}
上一篇:6、【单例模式】确保了一个类在程序运行期间只有一个实例


下一篇:图片切换案例