MIT6.824-Lab2A

Lab2A

Lab2A的地址:https://pdos.csail.mit.edu/6.824/labs/lab-raft.htmlhtml

Lab2A须要完成Raft协议中的Leader Election部分。
按照论文Figure2实现以下功能:网络

  • 初始选举
  • Candidate发布RequestVote rpc
  • Leader发布AppendEntry rpc, 包括心跳
  • server的状态转换(Follower, Candidate, Leader)

实验提示:多线程

  • raft.go添加必须的状态。
  • 定义log entry的结构。
  • 填充RequestVoteArgsRequestVOteReply结构。
  • 修改Make()以建立后台goroutine, 必要时这个goroutine会发送RequestVoteRPC。
  • 实现RequestVote()RPC的handler。
  • 定义AppendEntriesRPC结构和它的handler。
  • 处理election timeout和"只投票一次"机制。
  • tester要求heartbeat发送速率不能超过10个/s, 因此要限制HB发送速率。
  • 即便在split vote的状况下,也必须在5s内选出新的leader, 因此要设置恰当的Election timeout(不能按照论文中的election timeout设置为150ms~300ms, 必须不大不小)。
  • 切记, 在Go中, 大写字母开头的函数和结构(方法和成员)才能被外部访问!

测试

lab 2A有两个测试:TestInitialElection()TestReElection(). 前者较为简单, 只须要完成初始选举而且各节点就term达成一致就能够经过. 后者的测试内容见Bug分析部分。函数

构思

通过不断重构, 最后的程序结构以下:测试

  • MainBody(), 负责监听来自rf.Controller的信号并转换状态.
  • Timer(), 计时器.
  • Voter(), 负责发送RequestVote RPC和计算选举结果.
  • APHandler(), 负责发送心跳,而且统计结果.

四个经过channel来通讯(自定义整型信号).
先前的设计是MainBody() 监听来自若干个channel的信号,后来了解到channel一发多收, 信号只能被一个gorouine接受, 可能会有出乎意料的后果.
因此修改MainBody主循环, 使得主循环只监听来自rf.Controller的信号, 而后向其余channel中发送信号(MPSC).ui

Bugs分析

2019-10-30

TestInitElection()能成功经过, 可是TestReElection()偶尔会失败, 因而笔者
TestReElection()添加一些输出语句, 将其分为多个阶段, 方便debug。每一个阶段,测试函数都会检查该阶段内是否不存在leader, 或者仅有一个leader。.net

选出leader1
# 1 ------------------------------
    leader1 下线
    选出新leader
# 2 ------------------------------
    leader1 上线, 变为follower
# 3 ------------------------------
    leader2 和 (leader2 + 1 ) % 3 下线
    等待2s, 没有新leader产生
# 4 ------------------------------
    (leader2 + 1 ) % 3 产生
    选出新leader
# 5 ------------------------------
    leader2 上线
# 6 ------------------------------

这里说明一下, 测试函数TestReElection()使用disconnect(s)把节点s从网络中下线。节点自己没有crash, 仍然正常工做。使用connect(s)将会恢复节点与网络的通讯。线程

测试在第3-4阶段时偶尔会失败. 分析发现: 此时系统中不该该存在leader, 可是仍有某个节点声称本身是leader.debug

笔者进行屡次测试发现以下规律:设计

test    leader1  leader2  (leader2 + 1) % 3
success   0         2         0
failed    0         1         2
success   2         1         2
success   1         0         1
failed    0         1         2
..

只有当 (leader2 + 1) % 3 == leader1时才测试成功。为何呢?

在3-4阶段时,节点leader2(leader2 + 1)%3都被下线。
条件l1 == (l2 + 1) % 3使得在第三段时, leader1leader2(即1-3阶段产生的两个leader)都下线, 因此此时网络中没有leader了.

若是不知足这个条件, 那么惟一一个在网络中的节点就是leader1了。leader1迟迟不变为Follower, 会致使测试失败.

经过分析日志发现: leader1从新加入网络时, 没有收到leader2的心跳(若是收到,会使leader1变为follower), 本身也没有发送心跳, 也没有收到candidate的RequestVote RPC. 看起来是被阻塞了。

因而笔者修改TestReElection()函数, 在2-3和4-5阶段让测试函数睡眠若干个election timeout:

fmt.Println("2-------------- ")
...
time.Sleep(time.Duration(2 * ElectionTimeout) * time.Millsecond)
// ...
fmt.Println("3-------------- ")
// ...
time.Sleep(time.Duration(2 * ElectionTimeout) * time.Millsecond)

笔者发现, 通过更长时间(12s), old leader最终都得知了存在更大的Term从而变为follower. 在修改后的测试中, 每次测试均正常经过.

笔者在此阶段作的工做有:

  • 插入输出语句, 打印更详细的日志。
  • 大量执行测试, 总结测试成功失败的规律。
  • 结合规律和测试逻辑来判断失败场景。
  • 修改测试函数,使其等待更长时间。

笔者发现问题不是在死锁,因而认为问题是线程饥饿所致。、

查阅资料发现, 有人提到当某些goroutine是计算密集的, 会致使饥饿。但是笔者的实现中,goroutine执行少许逻辑判断后就进入睡眠阻塞,与计算密集绝不相干。

此外检查执行环境, 确认所有核心都投入使用。

2019-11-08

笔者终于找到了问题的根源, 而且找到了解决办法.
笔者在今天忽然意识到rpc调用自己的问题:

rf.peers[server_index].Call("Raft.AppendEntry", &args, &replys[server_index])

此处的Call(), 是实验代码中自带的、用来模拟执行RPC的函数, 若是RPC正常返回, 则该函数返回true, 若是超时则返回false.

笔者在Lab1中也用过相似的函数, 因此并无怀疑此函数.

然而笔者忽然意识到,该函数的代码注释并无说明等待多久才算超时!

极可能Call的超时是致使测试失败的关键. 因而笔者翻阅源码,发现Call这个函数自身并无定时器,它依赖于Endpoint(模拟端点设备的结构)中的channel!也就是说channel什么时候返回结果,它就何时返回。

笔者继续翻看代码,忽然一个7000一闪而过,笔者定晴一看,不由背后一凉,心中大惊:

// src/raft/labrpc/labrpc.go
func (rn *Network) ProcessReq(req reqMsg) {
    enabled, servername, server, reliable, longreordering := rn.ReadEndnameInfo(req.endname)

    if enabled && servername != nil && server != nil {
        //... 这里也是设置超时参数, 非重点
    } else {
        //...
        if rn.longDelays {
            ms = (rand.Int() % 7000) // <--------------------- Lab2A下, 最多会等待7s!
        } else {
            ms = (rand.Int() % 100)
        }
        // 等待一段时间而后向channel发送信息。
        time.AfterFunc(time.Duration(ms)*time.Millisecond, func() {
            req.replyCh <- replyMsg{false, nil}
        })
    }

}

为何Lab2A下的超时设置是7000ms呢? 会不会跳到其余分支?不会!

首先测试开始时会配置测试环境, 其中就有这样一句:

// ...config.go: line: 84
cfg.net.LongDelays(true)

这里设置了长延时参数。
其次, 在测试函数TestReElection()中, .disconnect(s)会下线节点s, 使得s的网络请求老是超时. 在代码层面, 就是把enabled这个布尔值设为false

综合上面两点, .disconnect(s)s的超时设置将跳转到LongDelays这个分支, 从而rpc等待时间最高会达到7000ms, 这将会致使old leader长时间阻塞在rpc调用上, 不能及时处理到来的RPC, 也就没办法及时变为follower, 最终致使测试失败。

那该怎么办? 知道了缘由就好办了: 在APHandler处设置超时监控, 超时或者rpc按时返回则中止等待.

修复以后,再也没见过test fail了, 真开心。

总结

笔者虽然按照论文来实现,但在实现过程当中,屡屡遗漏某些细节,不得不花大量时间测试(好比rpc中携带比本身还大的Term,自身就要马上变为follower)。

此外,对于多线程+定时器这样的场景,基本不能用断点调试的办法。打日志,分析日志几乎是惟一路径。分析日志也挺考验思惟逻辑的。

这个Debug过程给笔者最大的教训是, 不要盲目信任别人的代码和承诺

本站公众号
   欢迎关注本站公众号,获取更多信息