mit_6.824_2021_lab2A_leader_election
做完 lab2 之后回来写系列文章总结
如果说 lab1 的 mapreduce 是用来入门分布式系统课程的,那么 lab2 开始就是课程设计的真正开始
lab2 系列为 raft 分布式一致性协议算法的实现,论文 extended Raft paper 更是要反复看,尤其是 Figure 2,以及第五章节的一些实现细节
raft 将分布式一致性共识分解为若干个子问题,lab2 系列也随之挂钩:
- leader election,领导选举(lab2A)
- log replication,日志复制(lab2B)
- safety,安全性(lab2B&2C);2C除了持久化还有 Fig8 的错误日志处理
以上为 raft 的核心特性,除此之外,要用于生产环境,还有许多地方可以优化:
- log compaction,日志压缩-快照(lab2D)
- Cluster membership changes,集群成员变更
lab2A:leader election
实验内容
实现 Raft 领导者选举和心跳(没有日志条目的AppendEntries
RPC)。第 2A 部分的目标是选出一个单一的领导者,如果没有瘫痪,领导者继续担任领导者,如果旧领导者瘫痪或 往返 旧领导者的数据包丢失,则由新领导者接管丢失。运行go test -run 2A -race
来测试你的 2A 代码。
实验提示
以下提示直接提取翻译自6.824 lab2 raft
-
必须用
-race
运行测试,即go test -run 2A -race
。 -
按照论文的图 2。实现 RequestVote RPC,与选举相关的规则,以及与领导选举相关的状态,
-
在
raft.go
的Raft
结构体中添加图 2 中的领导人选举状态。您还需要定义一个结构来保存有关每个日志条目的信息(这里我定义logEntry
表示一条日志条目)。 -
填写
RequestVoteArgs
和RequestVoteReply
结构。修改Make()
以创建一个后台 goroutine,当它有一段时间没有收到其他对等方(leader)的消息时,它将通过发送RequestVote
RPC 来定期启动领导者选举。通过这种方式,peer 可以知道谁是leader,或者在无法和leader取得联系时,自己成为leader。实现RequestVote()
RPC 处理程序,以便followers投票给候选者。 -
要实现心跳,请定义一个
AppendEntries
RPC 结构(lab2A 还不会用到日志条目),并让领导者定期发送它们。编写一个AppendEntries
RPC处理程序方法来“重置”选举超时,这样当一个服务器已经当选时,其他服务器不会向前作为leader. -
确保不同 peer 的选举超时不会总是同时触发,否则所有 peer 只会为自己投票,没有人会成为领导者。
-
测试者要求领导者每秒发送心跳 RPC 不超过十次。(即 100ms 发送一次心跳)
-
测试者要求你的 Raft 在旧领导者失败后的 5 秒内选举一个新领导者(如果大多数对等点仍然可以通信)。但是请记住,如果发生分裂投票(如果数据包丢失或候选人不幸选择相同的随机退避时间,可能会发生这种情况),领导者选举可能需要多轮投票。您必须选择足够短的选举超时(以及心跳间隔),即使选举需要多轮,也很可能在不到五秒的时间内完成。
这条意思是,必须在5s 内能选出唯一的 leader,否则测试会失败
-
该论文的第 5.2 节提到了 150 到 300 毫秒范围内的选举超时。只有当领导者发送心跳的频率远高于每 150 毫秒一次时,这样的范围才有意义。因为测试器将您限制为每秒 10 次心跳,您将不得不使用比论文中的 150 到 300 毫秒大的选举超时时间,但不要太大,因为那样您可能无法在 5 秒内选举出领导者。
-
您可能会发现 Go 的 rand 很有用。
-
您需要编写定期或延迟后采取行动的代码。最简单的方法是使用调用time.Sleep()的循环创建一个 goroutine ;(请参阅
Make()
为此目的创建的ticker()
协程)。不要使用Go的time.Timer
或time.Ticker
,这是很难正确使用。 -
该向导页,对如何开发和调试代码的一些技巧。
-
如果您的代码无法通过测试,请再次阅读论文的图 2;领导选举的完整逻辑分布在图中的多个部分。
-
不要忘记实现
GetState()
。 -
测试人员在永久关闭实例时调用您的 Raft 的
rf.Kill()
。可以使用rf.killed()
检查是否被killed。您可能希望在所有循环中都这样做,以避免死 Raft 实例打印混乱的消息。 -
Go RPC 只发送名称以大写字母开头的结构体字段。子结构还必须具有大写的字段名称(例如数组中的日志记录字段)。该
labgob
包会警告你这一点; 不要忽略警告。
实现思路
消化一下实验提示和查看raft论文 5.2 节,可以得出一些实现上的信息:
-
首先明确 raft 算法中,只有三个角色:leader, candidate, follower;其状态机变更论文中描述得很清楚
-
参考论文 Fig2,可以知道三个角色的结构体中的属性和需要实现的方法
-
其中 leader 负责周期性地广播发送
AppendEntries
rpc请求,candidate 也负责周期性地广播发送RequestVote
rpc 请求 -
需要实现的 RPC 接口有:
AppendEntries
和RequestVote
,follower 仅负责被动地接收rpc请求,从不主动发起请求(但其实 leader 和 candidate 也会收到其他 peer 发过来的请求,在网络发生错乱的时候) -
心跳超时需要在
Make
中起一个 ticker 做周期性检查,并且这里不建议用 timer,建议用time.Sleep()
,并且我这里基本全部周期性的实现都用 sleep -
有些周期性的 sleep(timeout),里面的 timeout 是要随机的,比如心跳超时,选举超时
leader 广播则不要随机,并且足够频繁,这里我使用 100ms;心跳超时和选举超时均是 [250, 400] ms
-
需要实现
GetState()
,测试用 -
RPC 结构体属性均用大写开头,否则 golang 不导出
-
多在代码中埋点
DPrint
,勤打印 leaderId 和 rf.me,找bug基本靠它了QAQ -
一定要把助教的 guide 反复观看几次,将助教的go-test-many.sh`拿到,有时候全pass可能是偶然现象,需要批量测试无错,才是正确的
关于组织结构
不太建议全部代码都塞进raft.go
里,从 lab2A 到 lab2D,我的文件的组织结构一直在变,因为即使在封装复用的情况下,一个AppendEntries
的 rpc 方法实现都有将近一百行(包括 log 和注释)
可以按角色功能分结构,见仁见智
关于 Figure2
很多资料,包括 student guide 都说,论文的图二需要反复检查,并且全部严格实现;
是的没错,但是在做实验的过程中发现,仅仅是严格实现也是不够的,因为 Fig2 透露的信息是有限的,并且很容易引人遐想。
首先,它没有指明 leader 和 candidate 的发送 rpc 请求后如何处理 reply 的逻辑,这些逻辑藏在了论文第5节中,只描述了 rpc 请求接收者,即 followers 的实现
并且有的规则适用于 all server,比如
If commitIndex > lastApplied: increment lastApplied, apply log[lastApplied] to state machine (§5.3)
If RPC request or response contains term T > currentTerm: set currentTerm = T, convert to follower (§5.1)
这两条适用于 all server 的处理需要放在 rpc 方法或者处理 reply 的哪里呢?
还有比如candidate 中的:
If AppendEntries RPC received from new leader: convert to follower
这条又应该放在 AppendEntries
的哪里呢?
还有等等问题,可以理解到,Fig 2 为一个大纲,我们需要在不背离大纲上加入一些自己对于论文第5节的理解
实现细节
-
图2 的 State 全部补充到代码中,这个没什么好说的
-
两个 RPC 的 args 和 reply 各封装一个结构体,以及补充大写属性
-
我感觉 lab2A 相对头疼的地方在于周期性检查,状态机转换,具体 rpc 细节上
-
每次 rpc 发送 args 和 处理 reply 之前,需要判断自身状态是否发生了改变,如果发生了改变,这次请求就不发送或者返回体不处理,直接丢弃;即在做动作之前,检查自身状态是否发生变化或过期
因为这个bug卡在 lab2C 很久,血泪QAQ
-
其他常见错误,其实可以参考其他资料,这里只说明自己遇到的头疼的地方
普遍封装
对于刚刚提及的 all server 的规则和实现细节,是可以普遍处理的,则可以得出 rpc handler 和 返回体的一般处理形式
// RpcHandler 指 AppendEntries 和 RequestVote,并没有实现这个方法啦
func (rf *Raft) RpcHandler(){
rf.mu.Lock()
defer func()