MIT6.824-Lab2A

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的结构。
  • 填充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).

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使得在第三段时, leader1leader2(即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。

另一种实现方式

总结

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

阅读 742

推荐阅读
从菜鸡到菜须鲲
用户专栏

1 人关注
11 篇文章
专栏主页