Lab2A的地址:https://pdos.csail.mit.edu/6.824/labs/lab-raft.htmlhtml
Lab2A须要完成Raft协议中的Leader Election
部分。
按照论文Figure2实现以下功能:网络
RequestVote rpc
AppendEntry rpc
, 包括心跳实验提示:多线程
raft.go
添加必须的状态。log entry
的结构。RequestVoteArgs
和RequestVOteReply
结构。Make()
以建立后台goroutine, 必要时这个goroutine会发送RequestVote
RPC。RequestVote()
RPC的handler。AppendEntries
RPC结构和它的handler。tester
要求heartbeat发送速率不能超过10个/s, 因此要限制HB发送速率。split vote
的状况下,也必须在5s内选出新的leader, 因此要设置恰当的Election timeout
(不能按照论文中的election timeout设置为150ms~300ms, 必须不大不小)。lab 2A有两个测试:TestInitialElection()
和TestReElection()
. 前者较为简单, 只须要完成初始选举而且各节点就term达成一致就能够经过. 后者的测试内容见Bug分析部分。函数
通过不断重构, 最后的程序结构以下:测试
rf.Controller
的信号并转换状态.RequestVote RPC
和计算选举结果.四个经过channel来通讯(自定义整型信号).
先前的设计是MainBody()
监听来自若干个channel的信号,后来了解到channel一发多收, 信号只能被一个gorouine接受, 可能会有出乎意料的后果.
因此修改MainBody主循环, 使得主循环只监听来自rf.Controller
的信号, 而后向其余channel中发送信号(MPSC).ui
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
使得在第三段时, leader1
和leader2
(即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执行少许逻辑判断后就进入睡眠阻塞,与计算密集绝不相干。
此外检查执行环境, 确认所有核心都投入使用。
笔者终于找到了问题的根源, 而且找到了解决办法.
笔者在今天忽然意识到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过程给笔者最大的教训是, 不要盲目信任别人的代码和承诺。