前言
仅供学习,转载请标明出处。
有任何翻译上的问题欢迎评论和私聊。具体的实现思路会发布在另一篇文章中,本篇文章仅讨论翻译问题。
原文:nil.csail.mit.edu/6.824/2020/…
课程详情:nil.csail.mit.edu/6.824/2020/…
6.824 实验 3: Raft
介绍
在这个实验中,我们将在实验2的Raft函数库之上,搭建一个能够容错的key/value存储服务。你的key/value服务将由几个具有相同副本,并由Raft保持一致的状态机组成。只要大多数服务器在线并且网络通畅,就算其他服务器失败或者网络分区,都应该可以持续处理客户端的请求。
我们的服务需要提供三个接口:Put(key, value),Append(key, arg),Get(key)。key和value都是字符串。Put(key, value)替换key的值为value。Append(key, arg)增加args到key的值上,Get(key)获取当前key现在的值。Get(key)在遇到key不存在的情况下,将会返回一个空值。Append(key, arg)在key不存在的情况下,行为和Put(key, value)相似。每个客户端通过Clerk与服务器交流,调用三个接口。一个Clerk接管了服务器的RPC。
我们的服务器必须对三个接口(Put,Append,Get)提供强一致性保证。强一致性的解释如下:对于单个请求,整个服务需要表现得像个单机服务,并且对状态机的修改基于之前所有的请求。对于并发的请求,返回的值和最终的状态必须相同,就好像所有请求都是串行的一样。即使有些请求发生在了同一时间,那么也应当一个一个响应。此外,在一个请求被执行之前,这之前的请求都必须已经被完成(在技术上我们也叫着线性化(linearizability))。
强一致性能提供给应用便利性,因为强一致性意味着所有的客户端看到同样的、最新的状态。对于单机来说,提供将一致性相对简单。但是对于有多个副本的应用来说,很难。这是因为所有的节点都必须在并发的请求下,选择相同的执行顺序,还一定要避免提供给客户端陈旧的数据。
这个实验有两个部分,在A部分,我们将实现这个服务,并且不用担心Raft的日志无限增长。在B部分,我们将实现快照(在论文的Section 7),快照将允许Raft去丢弃老日志。请在每一部分的DDL前提交代码。
我们应该重复论文的Section 7和8。如果想开阔视野,可以看一看Chubby、Paxos Made Live、Spanner、Zookeeper、Harp、Viewstamped Replication和Bolosky et al。
记得尽早开始哦~
协作策略
您必须为 6.824 编写并上交的所有代码,其中一部分由我们为您提供。不允许查看其他人的解决方案,不允许查看前几年的代码,也不允许查看其他 Raft 实现。您可以与其他学生讨论作业,但不得查看或复制其他人的代码,或允许其他人查看您的代码。
请不要发布您的代码或将其提供给当前或将来的 学习 6.824 的学生。github.com是公开的,所以请不要将代码放在上面,除非您将仓库设为私有。您可能会喜欢使用 MIT 的GitHub,但请务必创建一个私有存储库。
第一步
我们在src/kvraft提供了框架代码和测试。接下来需要修改kvraft/client.go,kvraft/server.go,kvraft/common.go也可能需要修改。
执行以下的命令来运行代码。别忘了git pull来获取最新的代码。
$ cd ~/6.824
$ git pull
...
$ cd src/kvraft
$ go test
...
$
Part A - 不需要日志压缩的key/value服务
每一个key/value服务器(下面简称为kvservers)都会有一个Raft peer。Clerks发送Put(),Append()和Get()RPC给能连接到Leader的kvservers。kvservers将Put/Append/Get操作给Raft,让Raft来保证Put/Append/Get操作的顺序。所有的kvservers应用(apply)相同顺序的Raft日志,以保证所有的服务拥有相同的状态。
一个Clerk有时候不知道谁是Leader。如果Clerk发送了一个PRC给错误的服务器,或者与服务器通信失败,则应该尝试其他的服务器。如果一个服务器提交了Raft logs(并且之后应用了这个操作),则leader通过RPC响应Clerk。如果一个操作失败了,则服务响应失败,Clerk去尝试别的服务器。
kvservers不应该直接交流,而应该仅仅通过双方的Raft交互。在完成实验三的每个部分之后,你都应该确保你的实现可以通过所有的实验二的测试。
Task
第一步应该是实现一组不会丢弃消息且不会失败的服务器。
我们需要在
client.go中,为Clerk添加发送RPC到Put/Append/Get的代码,并且在server.go中,实现服务器的PutAppend()和Get()RPC接口。这些处理程序都应该使用Start()来在Raft日志中添加一个Op。我们需要在server.go中完善Op结构体的定义,使Op可以正确描述Put/Append/Get操作。每一个服务器应在Raft提交Op的时候,执行他们;即当日志发送给applyCh的时候。一个RPC请求应该等到Raft提交Op,然后再返回。当你稳定能通过一组测试中第一个测试“One client”时,你就完成了这一部分。
- 在kvservers调用
Start()之后,应该等待Raft达成共识。达成共识的Command应该发送到applyCh。在PutAppend()和Get()通过Start()提交命令之后,应该等待applyCh。小心kvservers和Raft之间产生的死锁。- 你可以为Raft的
ApplyMsg添加字段,也可以为Raft的RPC调用添加字段,比如AppendEntries。- 一个kvservers不应该在与集群失联的情况下响应
Get()RPC请求(不应该提供旧数据)。一个简单的解决方案是将Get()也添加到Raft日志之中(就像Put()和Append()一样)。你不一定要实现Section 8中的只读优化。- 最好在一开始就加锁,以免死锁影响全部的代码设计。记得通过
go test -face来检查竞争。
你应该保证你的解决方案在网络异常和服务器崩溃中工作正常。其中一个你将遇到的问题是,Clerk可能需要发送一个RPC非常多次,直到找到一个kvserver拥有正确的副本。如果一个Leader刚刚提交Raft日志就崩溃了,那么Clerk有可能收不到回复,并重新发送请求给其他Leader。每次调用Clerk.Put()或者Clerk.Append(),都应该保证只执行一次。所以我们应该保证重复发送同一个请求,不会导致服务器多次执行。
Task
实现容错、正确处理
Clerk重复的请求,包括Clerk发送给一个Leader的请求超时了,然后重新发送一个消息给新Leader。请求应该只执行一次。你的代码应该通过go test -run 3A。
- 你的解决方案应该能处理这种情况:一个Leader被调用了
Start(),并在提交日志前失去了Leader的身份。在这种情况下,我们应该安排Clerk给其他服务器发送请求,直到找到一个新的Leader。其中一种解决方案是让server去主动察觉自己失去了Leader身份,例如在Start()返回的索引位置上,出现了不同的请求。如果Leader自己处在一个网络分区之中,它不知道新的Leader,且在相同网络分区中的客户端也不能和新Leader通信。在这种情况下,服务器和客户端应该直接等待网络分区结束。- 你应该需要让
Clerk通过上一次RPC请求,记住哪个服务器是Leader,并在下一次请求时优先发送请求给它。这会避免浪费时间在每次RPC时都去寻找Leader,让你能够满足通过测试的时间要求。- 你需要唯一区分客户端的操作,以保证每个service只执行每个请求一次。
- 你的重复请求检测方案应该能快速释放内存,比如让每个RPC请求隐含客户端前一次看到的请求。这可以保证客户端在同一时间只发送一个请求给
Clerk。
你的代码应该能像这样通过Lab3A:
$ go test -run 3A
Test: one client (3A) ...
... Passed -- 15.1 5 12882 2587
Test: many clients (3A) ...
... Passed -- 15.3 5 9678 3666
Test: unreliable net, many clients (3A) ...
... Passed -- 17.1 5 4306 1002
Test: concurrent append to same key, unreliable (3A) ...
... Passed -- 0.8 3 128 52
Test: progress in majority (3A) ...
... Passed -- 0.9 5 58 2
Test: no progress in minority (3A) ...
... Passed -- 1.0 5 54 3
Test: completion after heal (3A) ...
... Passed -- 1.0 5 59 3
Test: partitions, one client (3A) ...
... Passed -- 22.6 5 10576 2548
Test: partitions, many clients (3A) ...
... Passed -- 22.4 5 8404 3291
Test: restarts, one client (3A) ...
... Passed -- 19.7 5 13978 2821
Test: restarts, many clients (3A) ...
... Passed -- 19.2 5 10498 4027
Test: unreliable net, restarts, many clients (3A) ...
... Passed -- 20.5 5 4618 997
Test: restarts, partitions, many clients (3A) ...
... Passed -- 26.2 5 9816 3907
Test: unreliable net, restarts, partitions, many clients (3A) ...
... Passed -- 29.0 5 3641 708
Test: unreliable net, restarts, partitions, many clients, linearizability checks (3A) ...
... Passed -- 26.5 7 10199 997
PASS
ok kvraft 237.352s
每一个Passed之后会输出五个数字;他们分别是
- 测试所用的时间(单位:秒)
- Raft Peer 的数量(通常为 3 或 5)
- 测试期间发送 RPC 的次数
- RPC 消息中的字节总数(包括客户端的RPC)
- Raft 确定并提交的日志条目(
Clerk调用Get/Put/Append)数
提交实验 3A 的程序
首先,请最后一次运行 3A 测试。然后,运行make lab3a将代码上载到提交站点。
您可以使用 MIT 证书或通过电子邮件请求 API 密钥首次登录。您的 API 密钥 (XXX) 将在您登录后显示,并可用于从控制台上载实验室,如下所示。
$ cd ~/6.824
$ echo "XXX" > api.key
$ make lab3a
检查提交网站,以确保它看到您的提交。
您可以多次提交。我们将使用您最后一次提交计算迟到日期。您的成绩取决于您的解决方案在正确运行测试时所达到的分数。
Part B - 包含日志压缩的key/value服务
现在我们的服务器可以通过执行完整的Raft日志,重新启动并恢复状态。但是对于长时间运行的服务器,不可能永远保存着所有的日志。与而代之,我们应该同时修改Raft和kvserver,让kvserver隔一段时间就生成一张快照,让Raft丢弃快照之前的日志,以此来节省空间。当一个服务器重启(或者落后leader太多),服务器应该先通过快照恢复,然后执行快照之后的日志。extended Raft paper的Section 7概述了方案,我们需要去设计实现的细节。
为了实现这一目的,Raft和kvserver之间应该有一个接口,允许Raft来丢弃日志。这意味着我们必须修改Raft部分的代码,使Raft仅仅保存尾部的一部分日志。Raft应该丢弃快照前的日志,并让Go的垃圾回收器释放并重用这部分内存。这需要没有任何的引用(指针)到被丢弃的日志。maxraftstate对应着Raft日志在persister.SaveRaftState()中的GOB编码。
测试将传递参数maxraftstate给StartKVServer(),maxraftstate描述了Raft日志能够保存的最大值(以b为单位)(包括日志,不包括快照)。我们需要比较maxraftstate和persister.RaftStateSize(),当kvserver发现Raft的日志大小已经超过了阈值,应该保存快照,并告诉Raft丢弃旧日志。如果maxraftstate是-1,则不需要产生快照。
Task
让Raft可以根据日志索引,丢弃指定索引之前的日志,并正常存储该索引之后的日志条目。确保实验2的所有的测试依旧能通过
Task
让kvserver检测Raft已保存日志的大小,在其超过阈值后将快照保存到Raft,并让Raft丢弃快照之前的日志。Raft应该用
persister.SaveStateAndSnapshot()来保存每个快照(不要使用文件)。一个kvserver应该在重新启动时,从persister中快速回复。
- 思考kvserver应该在何时生成快照,快照中应该包含哪些项目。Raft应该使用
SaveStateAndSnapshot()来保存快照和Raft的状态。可以用ReadSnapshot()来读取最新的快照。- kvserver必须能够检测出重复的日志,因此用于检测重复日志的状态也应该保存在快照之中。
- 快照中所有的字段都要大写。
- 向Raft添加方法,以便kvserver可以向Raft添加快照、通知Raft丢弃部分日志。
Task
当leader已经丢弃follower需要的日志时,leader通过InstallSnapshot RPC发送快照给follower。当follower收到InstallSnapshot RPC时,应该将快照交给它的kvserver。我们可以向
ApplyMsg添加字段,使用applyCh来实现。当通过所有的Lab3测试时,你的解决方案就完成了。
- 在单个InstallSnapshot RPC中发送完整的日志快照,不要实现Figure 13中的偏移机制来切分快照。
- 在进行其他测试前,确保已经通过了
TestSnapshotRPC。- 实验3的测试的合理时间是400秒的real time和700秒的CPU时间。此外,
go test -run TestSnapshotSize的real time应该少于20秒。
你的代码应该能通过所有的3B测试(就像下面一样)和3A测试(当然也包括实验2的测试)
$ go test -run 3B
Test: InstallSnapshot RPC (3B) ...
... Passed -- 1.5 3 163 63
Test: snapshot size is reasonable (3B) ...
... Passed -- 0.4 3 2407 800
Test: restarts, snapshots, one client (3B) ...
... Passed -- 19.2 5 123372 24718
Test: restarts, snapshots, many clients (3B) ...
... Passed -- 18.9 5 127387 58305
Test: unreliable net, snapshots, many clients (3B) ...
... Passed -- 16.3 5 4485 1053
Test: unreliable net, restarts, snapshots, many clients (3B) ...
... Passed -- 20.7 5 4802 1005
Test: unreliable net, restarts, partitions, snapshots, many clients (3B) ...
... Passed -- 27.1 5 3281 535
Test: unreliable net, restarts, partitions, snapshots, many clients, linearizability checks (3B) ...
... Passed -- 25.0 7 11344 748
PASS
ok kvraft 129.114s
提交实验 3B 的程序
首先,请最后一次运行 3B 测试。然后,运行make lab3b将代码上载到提交站点。
您可以使用 MIT 证书或通过电子邮件请求 API 密钥首次登录。您的 API 密钥 (XXX) 将在您登录后显示,并可用于从控制台上载实验室,如下所示。
$ cd ~/6.824
$ echo "XXX" > api.key
$ make lab3b
检查提交网站,以确保它看到您的提交。
您可以多次提交。我们将使用您最后一次提交计算迟到日期。您的成绩取决于您的解决方案在正确运行测试时所达到的分数。