6.5840 实验 4:容错键/值服务

介绍

在本实验中,你将使用 实验 3 中实现的 Raft 库构建一个容错的键/值存储服务。你的键/值服务将作为一个复制状态机运行,由多个键/值服务器组成,每个服务器维护一个键/值对数据库(与 实验 2 中类似),但额外使用 Raft 进行数据复制。只要多数服务器存活且能够互相通信,你的服务就应继续处理客户端请求,即使在其他服务器发生故障或出现网络分区的情况下也是如此。完成实验 4 后,你将实现 Raft 交互图 中展示的所有部分(Clerk、Service 和 Raft)。

客户端与键/值服务的交互方式与实验 2 大致相同。具体来说,客户端可以向键/值服务发送三种 RPC 请求:

键和值均为字符串。注意,与实验 2 不同,PutAppend 不再向客户端返回值。每个客户端通过一个具有 Put/Append/Get 方法的 Clerk 与服务通信,该 Clerk 负责管理与各服务器间的 RPC 交互。

你的服务必须确保应用程序对 Clerk 的 Get/Put/Append 调用满足线性化要求。若这些调用是逐个进行的,则它们应表现得仿佛系统只有一份状态副本,每次调用都能观察到之前调用所做的状态修改;对于并发调用,其返回值和最终状态必须与操作以某个顺序逐个执行的结果一致。调用之间若存在时间重叠(例如,客户端 X 调用了 Clerk.Put(),客户端 Y 调用了 Clerk.Append(),随后客户端 X 的调用返回),则每次调用都必须能观察到所有在其开始前完成的调用的影响。

对于单个服务器来说,提供线性化较为简单;但当服务被复制时,由于所有服务器必须对并发请求选择相同的执行顺序,避免使用未更新状态回复客户端,并在故障后恢复状态时保持所有已确认的客户端更新,因此实现起来会更为复杂。

本实验分为两个部分:

请在各自截止日期前提交对应部分。

建议你阅读 扩展 Raft 论文,特别是其中的第 7 节和第 8 节;若想获得更广泛的视角,可参考 Chubby、Paxos Made Live、Spanner、Zookeeper、Harp、Viewstamped Replication 以及 Bolosky 等人 的论文。

请及早开始!

入门

我们在 src/kvraft 目录中为你提供了骨架代码和测试。你需要修改以下文件:

要启动开发环境,请执行以下命令(别忘了先 git pull 获取最新代码):

$ cd ~/6.5840
$ git pull
...
$ cd src/kvraft
$ go test
...
$

Part A:不使用快照的键/值服务 (moderate/hard)

每个键/值服务器(以下简称 “kvserver”)都将拥有一个对应的 Raft 节点。Clerk 将向其所认为是领导者的 kvserver 发送 Put()、Append() 和 Get() RPC。kvserver 代码将把 Put/Append/Get 操作提交给 Raft,使得 Raft 日志中保存一系列的 Put/Append/Get 操作。所有 kvserver 按 Raft 日志的顺序执行操作,将操作应用到各自的键/值数据库中,其目标是让所有服务器维持一模一样的键/值数据库副本。

有时,Clerk 无法确定哪个 kvserver 是当前的 Raft 领导者。如果 Clerk 向错误的 kvserver 发送 RPC,或无法联系到 kvserver,则 Clerk 应重试并转而发送到其他 kvserver。如果键/值服务将操作成功提交到 Raft 日志中(并因此将操作应用于状态机),领导者会通过回复 RPC 将结果反馈给 Clerk;若操作未能提交(例如领导者被替换),服务器会返回错误,此时 Clerk 应重试向其他服务器发送请求。

注意:你的 kvserver 之间不应直接通信,而只能通过 Raft 进行交互。

你的第一个任务是实现一个在无消息丢失且服务器均正常工作的情况下可运行的方案。

当你的实现能可靠地通过测试套件中 “One client” 测试时,说明任务完成。

额外注意事项:

接下来,你需要修改你的方案,使其能够应对网络和服务器故障。一个常见问题是,一个 Clerk 可能需要多次发送 RPC 才能找到回复正确的 kvserver。例如,如果一个领导者在将条目提交到 Raft 日志后刚刚失败,Clerk 可能收不到回复,从而会将请求重发给另一个领导者。每次对 Clerk.Put() 或 Clerk.Append() 的调用都应只执行一次,因此你必须确保重发请求不会导致服务器多次执行相同请求。

为此,你需要添加代码来处理故障,并应对重复的 Clerk 请求,包括 Clerk 在一个任期中向 kvserver 领导者发送请求后因超时等待回复,再在新任期中向新领导者重发请求的情况。重发的请求应仅执行一次。相关指导请参阅 重复检测 文档。你的代码应能通过 go test -run 4A 的测试。

其他提示:

完成上述修改后,你的代码应能通过 Lab 4A 测试,类似输出如下:

$ go test -run 4A
Test: one client (4A) ...
  ... Passed --  15.5  5  4576  903
Test: ops complete fast enough (4A) ...
  ... Passed --  15.7  3  3022    0
Test: many clients (4A) ...
  ... Passed --  15.9  5  5884 1160
Test: unreliable net, many clients (4A) ...
  ... Passed --  19.2  5  3083  441
Test: concurrent append to same key, unreliable (4A) ...
  ... Passed --   2.5  3   218   52
Test: progress in majority (4A) ...
  ... Passed --   1.7  5   103    2
Test: no progress in minority (4A) ...
  ... Passed --   1.0  5   102    3
Test: completion after heal (4A) ...
  ... Passed --   1.2  5    70    3
Test: partitions, one client (4A) ...
  ... Passed --  23.8  5  4501  765
Test: partitions, many clients (4A) ...
  ... Passed --  23.5  5  5692  974
Test: restarts, one client (4A) ...
  ... Passed --  22.2  5  4721  908
Test: restarts, many clients (4A) ...
  ... Passed --  22.5  5  5490 1033
Test: unreliable net, restarts, many clients (4A) ...
  ... Passed --  26.5  5  3532  474
Test: restarts, partitions, many clients (4A) ...
  ... Passed --  29.7  5  6122 1060
Test: unreliable net, restarts, partitions, many clients (4A) ...
  ... Passed --  32.9  5  2967  317
Test: unreliable net, restarts, partitions, random keys, many clients (4A) ...
  ... Passed --  35.0  7  8249  746
PASS
ok      6.5840/kvraft   290.184s

每个 “Passed” 后面的数字分别表示实际运行时间(秒)、节点数、发送的 RPC 数量(包括客户端 RPC),以及执行的键/值操作次数(Clerk 的 Get/Put/Append 调用)。


Part B:使用快照的键/值服务 (hard)

目前,你的键/值服务器尚未调用 Raft 库的 Snapshot() 方法,因此在重启时需要重放完整的持久化 Raft 日志以恢复状态。现在,你需要修改 kvserver,使其与 Raft 协作,通过快照来节省日志空间并减少重启时间,使用的是实验 3D 中实现的 Snapshot()。

测试程序会将一个 maxraftstate 参数传递给你的 StartKVServer()。maxraftstate 指定了持久化 Raft 状态的最大允许字节数(包括日志,但不包括快照)。你应将 maxraftstatepersister.RaftStateSize() 进行比较。每当你的键/值服务器检测到 Raft 状态大小接近该阈值时,应通过调用 Raft 的 Snapshot() 来保存一个快照。如果 maxraftstate 为 -1,则无需进行快照。注意,maxraftstate 针对的是 Raft 传递给 persister.Save() 的经过 GOB 编码的字节。

请修改 kvserver,使其能检测到持久化的 Raft 状态增长过大,并将快照传递给 Raft。当 kvserver 重启时,应从 persister 中读取快照并恢复状态。

注意以下几点:

你的代码应能同时通过 Lab 4B 测试和 Lab 4A 测试(并且 Raft 必须继续通过实验 3 的所有测试),例如:

$ go test -run 4B
Test: InstallSnapshot RPC (4B) ...
  ... Passed --   4.0  3   289   63
Test: snapshot size is reasonable (4B) ...
  ... Passed --   2.6  3  2418  800
Test: ops complete fast enough (4B) ...
  ... Passed --   3.2  3  3025    0
Test: restarts, snapshots, one client (4B) ...
  ... Passed --  21.9  5 29266 5820
Test: restarts, snapshots, many clients (4B) ...
  ... Passed --  21.5  5 33115 6420
Test: unreliable net, snapshots, many clients (4B) ...
  ... Passed --  17.4  5  3233  482
Test: unreliable net, restarts, snapshots, many clients (4B) ...
  ... Passed --  22.7  5  3337  471
Test: unreliable net, restarts, partitions, snapshots, many clients (4B) ...
  ... Passed --  30.4  5  2725  274
Test: unreliable net, restarts, partitions, snapshots, random keys, many clients (4B) ...
  ... Passed --  37.7  7  8378  681
PASS
ok      6.5840/kvraft   161.538s

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注