Lab3 Raft

news/2024/9/24 23:46:34

Lab3 Raft

1.Getting Started

代码位置:

  • 基础框架代码位置:src/raft/raft.go

  • 测试代码:src/raft/test_test.go

建议测试时使用-race

2.The code

  1. raft/raft.go添加代码来实现Raft。

  2. 实现必须支持以下接口

    // 创建一个Raft Server
    rf := Make(peers, me, persister, applyCh)
    func Make(peers []*labrpc.ClientEnd, me int,persister *Persister, applyCh chan ApplyMsg);// start agreement on a new log entry:
    rf.Start(command interface{}) (index, term, isleader)// ask a Raft for its current term, and whether it thinks it is leader
    rf.GetState() (term, isLeader)// each time a new entry is committed to the log
    // each Raft peer should send an ApplyMsg to the 
    // service (or tester)
    type ApplyMsg
    

    通过调用Make(peers,me,…)来创建Raft peer。

    • peers:Raft所有Server的网络标识符(比如IP地址),包括该peer本身,用于RPC。
    • me:该peer在peers数组中的下标。

    Start(command):要求Raft开始处理、将命令追加到复制日志。Start()应立即返回,无需等待日志追加完成。

    service期望您的实现将每个新提交的日志条目的ApplyMsg发送到Make()applyChchannel参数。??

  3. raft.go包含发送RPC(sendRequestVote())和处理传入RPC(RequestVote())的示例代码。您的Raft peer应使用labrpc Go包(位于src/labrpc)交换RPC。测试组件可以告诉labrpc延迟某个RPC、重新排序并丢弃它们以模拟各种网络故障。你可以临时修改labrpc,但请确保你的Raft与原始labrpc兼容,因为需要使用它来测试和评分。您的Raft实例必须仅与RPC交互;例如,它们不允许使用共享变量或文件进行通信

  4. 后续实验都以该实验为基础,编写可靠的代码非常重要。

Part 3A: leader election (moderate)

Task:

实现Raft的Leader选举和心跳(没有日志条目的AppendEntries RPC)机制。本节的目标是选举出Leader,如果Leader没有失效,其将保持Leader状态;如果旧Leader失效或旧Leader的消息丢失,则选出新Leader接管。

运行 go test -run 3A 来测试你的代码。

提示:

  • 直接运行你的Raft实现会有些困难;你可以通过测试器来运行它,即go test -run 3A
  • 遵循论文中的图2。本节你只需关注发送和接收RequestVote RPC、Leader选举规则,以及选举相关的State属性。
  • 将图2中的Leader选举状态添加到raft.go的Raft结构体中。还需要定义一个结构体存储每个日志条目的信息。
  • 填写RequestVoteArgsRequestVoteReply结构体。修改Make()函数以创建一个后台goroutine,该goroutine会定期启动领导者选举,通过发送 RequestVoteRPC来实现,尤其是在一段时间没有收到其他节点的响应时。实现 RequestVote()RPC处理器,以便服务器之间能够投票。
  • 为实现心跳,定义一个AppendEntries RPC 结构体(暂时不需要所有的参数),并使Leader定期发送RPC。编写一个AppendEntries RPC处理方法。
  • 测试器要求Leader每秒发送心跳RPC的次数不超过十次。
  • 测试器要求你的Raft在旧领导者失败后(如果多数节点仍能通信),五秒内选举出新的领导者。
  • 论文的第5.2节提到选举超时范围为150到300ms。由于测试器限制每秒最多发送十次心跳,因此你需要使用比150到300毫秒更大的选举超时时间,但又不能太大,以免在五秒内无法选出Leader。
  • 你可能会发现Go的rand很有用。
  • 你需要编写周期性或延迟时间后执行的代码。最简单的方法是创建一个 goroutine,并在循环中调用time.Sleep();请参见Make()为此目的创建的 ticker() goroutine。不要使用Go的time.Timertime.Ticker,因为它们很难正确使用。
  • 如果你的代码无法通过测试,请重新阅读论文中的图2;Leader选举的完整逻辑分布在图的多个部分。
  • 不要忘记实现GetState()
  • 测试器在关闭一个实例时会调用rf.Kill()。你可以通过rf.killed()检查 Kill()是否已被调用。你可能需要在所有循环中进行检查,以避免死掉的Raft实例打印出混乱的消息。
  • Go RPC只发送字段名以大写字母开头的结构体字段。子结构体的字段名也必须以大写字母开头(例如,数组中的日志记录字段)。labgob包会警告你这点;不要忽视这些警告。
  • 本实验中最具挑战性的部分可能是调试。记得让你的实现易于调试。

如果实现正确,你的测试结果会和下图类似

image-20240829014055963

每行Passed包含五个数字:分别是

  • 测试花费的时间(秒)
  • Raft节点数量
  • 测试期间发送的RPC数量
  • RPC消息的总字节数
  • Raft报告已提交的日志条目数量

你可以忽略这些数字,但它们会有助于你检查实现中发送的RPC数量。在后续Lab中,如果测试go test总花费超过600秒,或任何单个测试超过120秒,会判定失败。

Part 3B: log (hard)

Task

完成Leader和Follower代码,实现Append New Log Entries的功能,通过go test -run 3B的测试。

提示:

  • 第一个目标是通过TestBasicAgree3B()测试。首先实现Start(),然后编写代码通过AppendEntries RPC发送和接收新的日志条目,如图2所示。在每个对等点上的applyCh上发送每个新提交的条目。
  • 您将需要实现选举限制(论文中第5.4.1节)。
  • 您的代码中可能存在反复检查某些事件的循环。不要让这些循环不停地执行,这会减慢代码速度,导致测试失败。使用Go的条件变量,或在每个循环迭代中插入time.Sleep(10 * time.Millisecond)
  • 为方便之后的Lab,编写干净清晰的代码。如需参考,请重新访问我们的指导页面,其中包含开发和调试代码的建议。
  • 如果测试失败,请查看test_test.goconfig.go了解正在测试的内容。config.go还说明了测试如何使用Raft API。

如果代码运行速度太慢,测试可能会失败,可以使用time命令检查代码使用了多少实际时间和CPU时间。以下是典型的输出:

$ time go test -run 3B
Test (3B): basic agreement ...... Passed --   0.9  3   16    4572    3
Test (3B): RPC byte count ...... Passed --   1.7  3   48  114536   11
Test (3B): agreement after follower reconnects ...... Passed --   3.6  3   78   22131    7
Test (3B): no agreement if too many followers disconnect ...... Passed --   3.8  5  172   40935    3
Test (3B): concurrent Start()s ...... Passed --   1.1  3   24    7379    6
Test (3B): rejoin of partitioned leader ...... Passed --   5.1  3  152   37021    4
Test (3B): leader backs up quickly over incorrect follower logs ...... Passed --  17.2  5 2080 1587388  102
Test (3B): RPC counts aren't too high ...... Passed --   2.2  3   60   20119   12
PASS
ok  	6.5840/raft	35.557sreal	0m35.899s
user	0m2.556s
sys	0m1.458s
$
  • ok 6.5840/raft 35.557s表示3B测试所用时间为35.557秒的实际时间。
  • user 0m2.556s表示代码消耗了2.556秒的CPU时间,即实际执行指令(而不是等待或休眠)所用的时间。
  • 如果你的代码在3B测试中使用的实际时间远超一分钟,或者CPU时间远超5秒,您以后可能会遇到麻烦。寻找以下情况:花费时间在休眠或等待RPC超时,循环在没有休眠或等待条件或通道消息的情况下运行,或者发送大量RPC。

Part 3C: persistence (hard)

如果Raft服务器重启,它应该从上次中断的地方继续运行。这要求Raft能够保存和恢复持久化状态。论文的图2显示了哪些状态属于持久化状态。

你需要在每次状态发生变化时将持久化状态写入磁盘,并在重启后从磁盘读取状态。本次Lab不使用磁盘,而是通过Persister对象保存和恢复持久化状态(参见 persister.go)。Raft.Make()的调用方会提供Persister,该对象包含Raft最近的持久化状态(如果有的话)。Raft应该从Persister中获取之前保存的状态并初始化自己,并在每次状态变化时将持久化状态保存进去。本节主要使用Persister的ReadRaftState()Save()方法。

TASK:

完成raft.go中的persist()readPersist()函数,实现保存和恢复持久化状态的功能。你需要将持久化状态序列化为字节数组,传递给Persister

使用labgob编码器;查看persist()readPersist()中的注释。labgob类似于Go的gob编码器,但如果编码的结构体中有以小写开头的字段名,labgod会报错。

你可以传递nil作为persister.Save()的第二个参数。在持久化状态发生更改的地方调用persist()进行存储。完成这些步骤后,如果你的其余实现正确,你应该可以通过所有3C测试。

你可能需要一种优化,即一次回退nextIndex多个条目。查看Raft论文的第7页底部和第8页顶部(灰色线标记处)。

当拒绝AppendEntries请求时,Follower可以将冲突条目的term以及该term它存储的第一个Index返回给Leader。Leader通过这些信息减少nextIndex,以跳过该任期的所有冲突条目;对于每个包含冲突条目的term,只需要一个AppendEntries RPC,而不是每个条目都需要一个RPC

论文在细节上较为模糊,你需要自行补充其中的空白。一种可能的实现是让拒绝消息包含以下内容:

XTerm:  冲突条目的term(如果有的话)
XIndex: Follower中,XTerm的第一个条目的Index(如果有的话)
XLen:   Log的长度

Leader的处理逻辑如下:

Case 1: Leader中没有XTerm的条目:nextIndex = XIndex
Case 2: Leader有XTerm的条目:nextIndex = leader's last entry for XTerm
Case 3: Follower日志的长度太短:nextIndex = XLen

一些提示:

  • 运行git pull更新Lab代码。
  • 3C测试比3A或3B测试要求更高,如果失败,可能是由3A或3B中的问题引起的

你的代码应通过所有3C测试(如下所示),以及3A和3B测试。

$ go test -run 3C
Test (3C): basic persistence ...... Passed --   5.0  3   86   22849    6
Test (3C): more persistence ...... Passed --  17.6  5  952  218854   16
Test (3C): partitioned leader and one follower crash, leader restarts ...... Passed --   2.0  3   34    8937    4
Test (3C): Figure 8 ...... Passed --  31.2  5  580  130675   32
Test (3C): unreliable agreement ...... Passed --   1.7  5 1044  366392  246
Test (3C): Figure 8 (unreliable) ...... Passed --  33.6  5 10700 33695245  308
Test (3C): churn ...... Passed --  16.1  5 8864 44771259 1544
Test (3C): unreliable churn ...... Passed --  16.5  5 4220 6414632  906
PASS
ok  	6.5840/raft	123.564s
$

在提交之前,多次运行测试并PASS确保代码正确。

$ for i in {0..10}; do go test; one

Part 3D: log compaction (hard)

Raft论文第7章概述了SnapShot的思想,需要自行设计完成其中的细节

必须提供以下函数,服务可以调用该函数,并传入其状态的序列化快照:

Snapshot(index int, snapshot []byte)

在Lab 3D中,测试会定期调用Snapshot()。在Lab4中,你将编写一个调用 Snapshot()的键值服务器;快照将包含完整的键值对表。服务层会在每个节点上调用 Snapshot()(不仅仅是在领导者上)。

index表示快照中最高日志条目。Raft应该丢弃该点之前的日志条目。你需要修改Raft代码,使其在仅存储日志尾部的情况下工作。

你需要实现论文中讨论的InstallSnapshotRPC,允许Raft Leader通知落后的节点用快照替换其状态。你需要仔细思考InstallSnapshot如何与图2中的状态和规则进行交互。

当Follower接收到InstallSnapshot RPC时,它可以使用applyCh将快照发送给服务,在ApplyMsg中进行传递。ApplyMsg 结构定义已经包含了你需要的字段(以及测试器期望的字段)。注意,这些快照只能推进服务的状态,而不能使其倒退。

如果服务器崩溃,它必须从持久化数据重新启动。你的Raft应该同时持久化Raft状态和相应的快照。使用persister.Save()的第二个参数保存快照。如果没有快照,传递nil作为第二个参数。

当服务器重新启动时,读取持久化的快照并恢复其保存的状态。

Task:

实现 Snapshot() 和 InstallSnapshot RPC 以及对 Raft 的更改以支持这些功能(例如,使用精简后的日志操作)。当你的解决方案通过 3D 测试(以及之前所有的 Lab 3 测试)时,表示已经完成。

提示:

  • 运行 git pull 确保你拥有最新的软件。

  • 一个好的起点是修改你的代码,使其能够存储从索引X开始的部分日志。开始可以将X设置为0并运行3B/3C测试。然后让Snapshot(index)丢弃index之前的日志,并将X设置为index。如果一切顺利,你现在应该能够通过第一个3D测试。

  • 接下来:当Leader没有所需的日志条目以使Follower赶上时,发送一个InstallSnapshot RPC

  • 通过一个InstallSnapshot RPC发送完整快照,不要实现图13中拆分快照的offset机制。

  • Raft必须以某种方式丢弃旧的日志条目,以使Go的垃圾收集器能够释放并重新使用内存;这要求不能有可达的引用(指针)指向丢弃的日志条目。

  • 对于完整的Lab 3测试集(3A+3B+3C+3D)

    • 不带-race,合理耗时大约是6分钟实际时间和1分钟CPU时间
    • -race,合理耗时大约是10分钟实际时间和2分钟CPU时间

你的代码应通过所有3D测试(如下所示),以及3A,3B和3C测试。

$ go test -run 3D
Test (3D): snapshots basic ...... Passed --  11.6  3  176   61716  192
Test (3D): install snapshots (disconnect) ...... Passed --  64.2  3  878  320610  336
Test (3D): install snapshots (disconnect+unreliable) ...... Passed --  81.1  3 1059  375850  341
Test (3D): install snapshots (crash) ...... Passed --  53.5  3  601  256638  339
Test (3D): install snapshots (unreliable+crash) ...... Passed --  63.5  3  687  288294  336
Test (3D): crash and restart all servers ...... Passed --  19.5  3  268   81352   58
PASS
ok      6.5840/raft      293.456s

坑:

  1. Leader发给Follower的RPC,Follower都能收到,但是Follower的回复,Leader一直收不到,

    比如Follower已经有了1~8的Log,然后进行提交中

    当发送到6时,Leader发来了最新的Snapshot,包含Index [1~9]的日志,然后我们将其提交,提交完后

    Follower又提交了Index为8的Log,此时就出错了,因为Raft预期收到Index = 10的Log

    怎么办?

    接收到Snapshot时,不要自行提交,全部交给一个方法去提交

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/64388.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

Lab3 记录

Part 3A: leader election 1.选举主要流程新服务器加入集群服务器在启动时状态是Follower。只要持续接收到Leader或Candidate的心跳信息,就继续保持Follower状态。开始选举每个Server都有一个随机的选举超时时间,选举超时在一个固定区间内随机选择(例如,150-300毫秒) 如果…

软件工程课程第三次作业

软件工程 https://edu.cnblogs.com/campus/fzu/SE2024作业要求 https://edu.cnblogs.com/campus/fzu/SE2024/homework/13261作业目标 分析学生们的需求,设计一个app原型解决他们的问题学号 072208130合作伙伴 052205144张诗悦使用figma设计原型,原型链接:https://www.figma.…

大文件去重

若文件存的字符如下图,要求进行去重可将数据存入HashSet,如下,但如果文件很大,大于虚拟机内存的话,会报异常java.lang.OutOfMemoryError: Java heap spaceHashSet set = new HashSet();File file = new File("E:\\aa.txt");BufferedReader reader = new Buffere…

9月13日关于数组存储数据

在题目中要求建立数组来存储项目信息,储存的内容包括String、int、boolean、double等各种不同类型,刚开始我还处于建立普通数组要不是int【】要不是string【】,越琢磨越不对劲这样并不能存储不同类型的数据,但是数据又需要统一存取,网上又没有这么简单的讲解,也是被这个简…

9.24日总结

今日上学配置了Node.JS的环境变量,并应用VScode进行JavaScript的相关学习应用 其中

软件工程作业——结对项目

这个作业属于哪个课程 22级计科12班这个作业要求在哪 作业要求这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序成员姓名 学号 GitHub地址吕宏鸿 3122004446 结对项目宋观瑞 3122004402 结对项目1.PSP表格PSP2.1 预估耗时(分钟) 实际耗时(分钟)计划 10 5* 估计…

9月10日循环条件的结束

在测试编程中涉及到输入错误要重新返回UI界面,但是我写的总是输入不管是对还是错都会直接结束程序,完全不符合要求,经过整理思路,查询代码结构,此处应该设计为双层循环外部为while,内部为witch case语句,当输入为1时执行case==1;经应该是执行生产计划类然后跳出witch条…

IDEA更改远程git仓库地址

前言 我们在使用IDEA开发时,一般会配置好对应的git仓库,这样就比较容易对代码进行控制以及协同开发。但有时候,我们远程的仓库地址由于这样那样的原因,需要迁移(这在爱折腾的企业是常有的事情)。那么,我们该如何在IDEA中更新远程仓库地址呢? 如何设置 首先,我们点击上…