当前位置: 代码迷 >> 综合 >> mit6.824 Lab2: Raft
  详细解决方案

mit6.824 Lab2: Raft

热度:33   发布时间:2023-12-21 12:19:13.0

项目地址:https://github.com/rsy56640/Distributed_System_Learning/tree/master/MIT6.824/1st_learning

Lab 2A: Raft leader election

代码可以在 commit 记录中找到

说实话体验不是很好,我看网上很多人到 lab2就不做了,之前以为是有难度,但其实不然。主要问题在于,它给了 code base skeleton,但是几乎没有什么说明,我一开始根本都不知道我的哪些函数会被调用,一脸懵逼。而且 raft 是一个整体,它给了一个框架但其实很多东西是互相牵制的,又没说明,刚开始心态就崩了。。。举个例子:它的 Raft 结构体里面没有 timer,这个要自己加,但是整体调用逻辑它写死了,我都不知道它怎么调用,调用谁。我还是看了比人的实现才知道这是咋回事,然后又不熟悉 go,总共写了2天才过了 lab2A。
记录一下踩过的 go 的坑: timer.Afterfunc(duration, func) *Timer,这个函数返回一个 timer,到时自动调用 func,我一开始以为 resetTimer 直接重新赋值就好,但是之前的那个其实没有关掉。就导致一个 follower 不断地开始 canvass,term 瞬间上万。

总结一下我的实现思路:

  • 首先进入 Make() 初始化为 follower,并且设置 timer
  • timer 自动触发 rf.candidateRequestVote(),开始拉选票
  • 所有的接受消息处理例程都要考虑到 network delay,也就是说收到的消息是过时的,这个要正确处理
    • candidate 处理 vote reply:当这个消息 delay 时,自己已经可能不是candidate了
    • server 处理 requestVoteRPC:正常走,没啥说的
    • leader 处理 appendLog reply:有可能自己不是leader了,或者可能是自己之前的appendRPC,要正确更新 nextIndexmatchIndex
    • server 处理 appendLogRPC:正常走
  • 我用了一个自动触发选举的 timer: rf.electionTimer = time.AfterFunc(rf.electionTimeout, func() { rf.candidateRequestVote() }),记得每次重设前先 rf.electionTimer.Stop()
  • election 成功后,就成为 leader,并开启一个服务例程,用来持续的发送 heartbeat,如果从 leader 变为 follower,记得关闭这个 heartbeat 服务

Lab 2B: AppendEntry RPC

代码可以在 commit 记录中找到

还是有些坑的,,我之前没有用到 applyCh,导致完全不懂它在怎么测试,后来我人肉跟踪进去,发现他维护了我的 apply 的 log,即 cfg.logs[]。因为没有用 applyCh,这些全是空的,所以一个测试也过不了。。。
另一个问题是什么时候 reset timer:结论是只有 RequestVoteRPC中确定了vote 或 自己的term小且自己是leader 或者 AppendEntryRPC中对比prevIndex和prevTerm成功 时才 reset timer。这个bug我搞了挺久才发现,把日志全部打出来,然后一点点跟踪,最后发现 RequestVote 在 reject 的时候那个 server 也 reset timer。然而总共就2个server,一个log旧的 timer 时间短,一直让那个应该发起 canvass 的 server reset timer。。。对着这个“raft动画”玩了好久终于明白问题在哪了。(这个模拟好像不支持网络分区)

总结一下实现思路:

  • client 请求直接进入 Start(),然后返回就行(没显式调用 append,因为我的 heartbeatRoutine() 自动尝试同步 log)
  • 我这里面的有3个持续性task
    • timer:自动触发 candidateRequestVote()。(除了 leader 都有这个,candidate 选举成功时,stop timer)
    • 所有 server:applyEntryRoutine(),用条件变量阻塞,直到 commitIndex > lastApplied,然后 apply 到自己的 state machine 里面(在 lab 里,就是发送到 applyCh
    • leader 有的:heartbeatRoutine(),candidate 选举成功时开启,每隔一段时间就发起 sendAppendEntryRPC,如果对应的 server 没啥发的,就发空的,代表 heartbeat。还没同步到的 server 就一步一步往前尝试同步
  • 然后就是那个bug了:什么时候 reset timer,上面已经讨论过了
  • 还要注意的一点就是 network delay,当你收到消息(RPC 或者 RPC-reply)的时候,整个 state 可能已经变了,一些 corner case 要做到正确处理

Lab 2C: persist and Figure-8

代码可以在 commit 记录中找到

我之前写的代码里有2处bug:

  • canvass 接收 vote 时有可能 msg delay,也就是说如果一个 server 连续发起两次 canvass,有可能在第二次收到第一次的 vote,所以一定要比较一下 vote 的是哪个 term
  • 第二个bug算是对 go 不熟练造成的:AppendLogRPC 需要把多余的 log truncate 掉,因为有可能自己的 log 比 leader 长(自己可能是上一届 leader,有多余的 log)

我认为 TestFigure8Unreliable2C() 有一个重大问题

先来了一个 cfg.one(),然后乱搞(乱序消息,掉线,重连)一波,恢复,再来一个 cfg.one()。重点就是后面这个 cfg.one()
cfg.one() 是这个意思:选一个 leader,我们认为它可以就这个 command 达成一致,检测并返回。
但是现在有个问题:乱搞一波之后 reconnect,紧接着调用 cfg.one()。考虑这样一种情况:一个 old-term leader reconnect 之后,被 cfg.one() 选中,并承诺完成这个 agreement,然而它马上就会发现自己的 term 是 out-of-date,所以这个 command 就没了。这样 cfg.one() 中会认为没有达成一致。
我个人的做法是在 reconnect 和 cfg.one() 中间等待3s,让 reconnect server 认清一下现状。