Lab2A
Lab2A的地址:https://pdos.csail.mit.edu/6.824/labs/lab-raft.html
Lab2A需要完成Raft协议中的Leader Election
部分。
按照论文Figure2实现如下功能:
- 初始选举
- Candidate发布
RequestVote rpc
- Leader发布
AppendEntry rpc
, 包括心跳 - server的状态转换(Follower, Candidate, Leader)
实验提示:
- 为
raft.go
添加必须的状态。 - 定义
log entry
的结构。 - 填充
RequestVoteArgs
和RequestVOteReply
结构。 - 修改
Make()
以创建后台goroutine, 必要时这个goroutine会发送RequestVote
RPC。 - 实现
RequestVote()
RPC的handler。 - 定义
AppendEntries
RPC结构和它的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).
Bugs分析
follower长期收不到HB
TestInitElection()
能成功通过, 但是TestReElection()
偶尔会失败, 于是笔者
给TestReElection()
添加一些输出语句, 将其分为多个阶段, 方便debug。每个阶段,测试函数都会检查该阶段内是否不存在leader, 或者仅有一个leader。
选出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.
笔者进行多次测试发现如下规律:
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。
笔者发现, 经过更长时间(12s), old leader最终都得知了存在更大的Term从而变为follower. 在修改后的测试中, 每次测试均正常通过. 所以可能是线程长时间阻塞导致的。
问题根源
笔者终于找到了问题的根源, 并且找到了解决办法.
首先在笔者的实现中,server称为leader会创建n-1个常驻的goroutine,它们负责阶段性发送HB。在这种实现中,某个goroutine可能会被长时间阻塞,进而导致对应的follower长时间收不到HB。
笔者意识到rpc调用本身可能有问题:
rf.peers[server_index].Call("Raft.AppendEntry", &args, &replys[server_index])
此处的Call()
, 是实验代码中自带的、用来模拟RPC的函数, 如果RPC正常返回, 则该函数返回true
, 如果超时则返回false
.
Lab1中也有类似的函数, 但是一切正常。所以笔者并没有怀疑此函数有问题.
笔者继续翻看测试代码,:
// 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下, 最多Call会阻塞达7s
} else {
ms = (rand.Int() % 100)
}
time.AfterFunc(time.Duration(ms)*time.Millisecond, func() {
req.replyCh <- replyMsg{false, nil}
})
}
}
综上所述,某个goroutine可能被阻塞长达7s。修改办法也很简单,给它加上一个50ms的计时器,超时就丢弃这个HB的处理,开始下一个HB。
另一种实现方式
总结
对于多线程+定时器这样的场景,基本不能用断点调试的办法。打日志,分析日志几乎是唯一路径。分析日志也挺考验思维逻辑的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。