mit6.824 实验3汉化

689 阅读6分钟

前言

仅供学习,转载请标明出处。

有任何翻译上的问题欢迎评论和私聊。具体的实现思路会发布在另一篇文章中,本篇文章仅讨论翻译问题。

原文: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)keyvalue都是字符串。Put(key, value)替换key的值为valueAppend(key, arg)增加argskey的值上,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是公开的,所以请不要将代码放在上面,除非您将仓库设为私有。您可能会喜欢使用 MITGitHub,但请务必创建一个私有存储库。

第一步

我们在src/kvraft提供了框架代码和测试。接下来需要修改kvraft/client.gokvraft/server.gokvraft/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之后会输出五个数字;他们分别是

  1. 测试所用的时间(单位:秒)
  2. Raft Peer 的数量(通常为 3 或 5)
  3. 测试期间发送 RPC 的次数
  4. RPC 消息中的字节总数(包括客户端的RPC)
  5. 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编码。

测试将传递参数maxraftstateStartKVServer()maxraftstate描述了Raft日志能够保存的最大值(以b为单位)(包括日志,不包括快照)。我们需要比较maxraftstatepersister.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

检查提交网站,以确保它看到您的提交。

您可以多次提交。我们将使用您最后一次提交计算迟到日期。您的成绩取决于您的解决方案在正确运行测试时所达到的分数。