mit_6.824_2021_lab2A_leader_election

本文详细介绍了 MIT 6.824 分布式系统课程中 Lab2A 的内容,重点是 Raft 分布式一致性算法的领导者选举实现。实验要求实现 RequestVote 和 Heartbeat RPC,确保在旧领导者失效后能在5秒内选举新领导者。文中探讨了实验提示、实现思路、状态机转换和测试结果,强调了论文 Figure2 的重要性,以及如何处理选举超时和状态转换的细节。此外,还分享了作者在实现过程中遇到的问题和解决方案,以及实验后的感想。
摘要由CSDN通过智能技术生成

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.goRaft结构体中添加图 2 中的领导人选举状态。您还需要定义一个结构来保存有关每个日志条目的信息(这里我定义logEntry表示一条日志条目)。

  • 填写RequestVoteArgsRequestVoteReply结构。修改 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.Timertime.Ticker,这是很难正确使用。

  • 向导页,对如何开发和调试代码的一些技巧。

  • 如果您的代码无法通过测试,请再次阅读论文的图 2;领导选举的完整逻辑分布在图中的多个部分。

  • 不要忘记实现GetState()

  • 测试人员在永久关闭实例时调用您的 Raft 的rf.Kill()。可以使用rf.killed()检查是否被killed。您可能希望在所有循环中都这样做,以避免死 Raft 实例打印混乱的消息。

  • Go RPC 只发送名称以大写字母开头的结构体字段。子结构还必须具有大写的字段名称(例如数组中的日志记录字段)。该labgob包会警告你这一点; 不要忽略警告。

实现思路

消化一下实验提示和查看raft论文 5.2 节,可以得出一些实现上的信息:

  1. 首先明确 raft 算法中,只有三个角色:leader, candidate, follower;其状态机变更论文中描述得很清楚

  2. 参考论文 Fig2,可以知道三个角色的结构体中的属性和需要实现的方法

  3. 其中 leader 负责周期性地广播发送AppendEntries rpc请求,candidate 也负责周期性地广播发送RequestVote rpc 请求

  4. 需要实现的 RPC 接口有:AppendEntriesRequestVote,follower 仅负责被动地接收rpc请求,从不主动发起请求(但其实 leader 和 candidate 也会收到其他 peer 发过来的请求,在网络发生错乱的时候)

  5. 心跳超时需要在Make中起一个 ticker 做周期性检查,并且这里不建议用 timer,建议用 time.Sleep(),并且我这里基本全部周期性的实现都用 sleep

  6. 有些周期性的 sleep(timeout),里面的 timeout 是要随机的,比如心跳超时,选举超时

    leader 广播则不要随机,并且足够频繁,这里我使用 100ms;心跳超时和选举超时均是 [250, 400] ms

  7. 需要实现GetState(),测试用

  8. RPC 结构体属性均用大写开头,否则 golang 不导出

  9. 多在代码中埋点DPrint,勤打印 leaderId 和 rf.me,找bug基本靠它了QAQ

  10. 一定要把助教的 guide 反复观看几次,将助教的go-test-many.sh`拿到,有时候全pass可能是偶然现象,需要批量测试无错,才是正确的

关于组织结构

不太建议全部代码都塞进raft.go里,从 lab2A 到 lab2D,我的文件的组织结构一直在变,因为即使在封装复用的情况下,一个AppendEntries的 rpc 方法实现都有将近一百行(包括 log 和注释)

可以按角色功能分结构,见仁见智

关于 Figure2

image-20211010213108332

很多资料,包括 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() 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值