228 lines
15 KiB
Markdown
228 lines
15 KiB
Markdown
# 6.5840 Lab 4: Fault-tolerant Key/Value Service
|
||
|
||
## 简介
|
||
|
||
在本实验中你将使用 Lab 3 的 Raft 库构建一个容错 key/value 存储服务。对客户端而言,该服务与 Lab 2 的服务器类似。但服务不是单台服务器,而是由一组使用 Raft 保持数据库一致的服务器组成。只要**多数**(majority)服务器存活且能通信,你的 key/value 服务就应继续处理客户端请求,即使存在其他故障或网络分区。完成 Lab 4 后,你将实现 Raft 交互图中所示的全部部分(Clerk、Service 和 Raft)。
|
||
|
||
客户端通过 **Clerk** 与你的 key/value 服务交互,与 Lab 2 相同。Clerk 实现的 **Put** 和 **Get** 方法与 Lab 2 语义一致:Put 为**至多一次**,Put/Get 必须形成 **linearizable** 历史。
|
||
|
||
对单机而言提供 linearizability 相对容易。在复制服务中更难,因为所有服务器必须对并发请求选择相同的执行顺序、必须避免用未更新的状态回复客户端、且必须在故障后以保留所有已确认客户端更新的方式恢复状态。
|
||
|
||
本实验分**三个部分**。在 **Part A** 中,你将用你的 Raft 实现一个与具体请求无关的复制状态机包 **rsm**。在 **Part B** 中,你将用 rsm 实现一个复制的 key/value 服务,但不使用快照。在 **Part C** 中,你将使用 Lab 3D 的快照实现,使 Raft 能丢弃旧日志条目。请分别在各自截止日前提交各部分。
|
||
|
||
建议复习 [Raft 扩展论文](../papers/raft-extended-cn.md),尤其是 **Section 7**(不含 8)。更广视角可参阅 Chubby、Paxos Made Live、Spanner、Zookeeper、Harp、Viewstamped Replication 以及 [Bolosky et al](https://pdos.csail.mit.edu/6.824/papers/bolosky-usenix2010.pdf)。
|
||
|
||
**尽早开始。**
|
||
|
||
---
|
||
|
||
## 起步
|
||
|
||
我们在 `src/kvraft1` 中提供了骨架代码和测试。骨架使用 `src/kvraft1/rsm` 包复制服务器。服务器必须实现 rsm 中定义的 **StateMachine** 接口才能通过 rsm 复制自身。你的主要工作是在 rsm 中实现与具体服务器无关的复制逻辑。此外需要修改 `kvraft1/client.go` 和 `kvraft1/server.go` 实现服务器相关部分。这种拆分便于在下一实验复用 rsm。可以复用部分 Lab 2 代码(例如复制或导入 "src/kvsrv1" 包中的服务器代码),但非必须。
|
||
|
||
运行以下命令即可开始。别忘了 `git pull` 获取最新代码。
|
||
|
||
```bash
|
||
$ cd ~/6.5840
|
||
$ git pull
|
||
...
|
||
```
|
||
|
||
---
|
||
|
||
## Part A: Replicated State Machine (RSM)
|
||
|
||
```bash
|
||
$ cd src/kvraft1/rsm
|
||
$ go test -v
|
||
=== RUN TestBasic
|
||
Test RSM basic (reliable network)...
|
||
..
|
||
config.go:147: one: took too long
|
||
```
|
||
|
||
在使用 Raft 复制的常见客户端/服务架构中,服务与 Raft 有两种交互:服务 leader 通过调用 **raft.Start()** 提交客户端操作,所有服务副本通过 Raft 的 **applyCh** 接收已提交的操作并执行。在 leader 上,这两类活动会交织:某些服务器 goroutine 在处理客户端请求、已调用 **raft.Start()**,各自在等待其操作提交并得到执行结果;而 **applyCh** 上出现的已提交操作需要由服务执行,且结果需交给曾调用 **raft.Start()** 的 goroutine 以便返回给客户端。
|
||
|
||
**rsm** 包封装上述交互。它位于服务(如 key/value 数据库)与 Raft 之间。你需要在 **rsm/rsm.go** 中实现:
|
||
|
||
1. 一个**“reader” goroutine**,读取 applyCh
|
||
2. 一个 **rsm.Submit()** 函数,为客户端操作调用 **raft.Start()**,然后等待 reader goroutine 把该操作的执行结果交回
|
||
|
||
使用 rsm 的服务对 rsm 的 reader goroutine 呈现为提供 **DoOp()** 方法的 **StateMachine** 对象。Reader goroutine 应将每条已提交操作交给 **DoOp()**;**DoOp()** 的返回值应交给对应的 **rsm.Submit()** 调用并返回。**DoOp()** 的参数和返回值类型为 **any**;实际类型应分别与服务传给 **rsm.Submit()** 的参数和返回值类型一致。
|
||
|
||
服务应将每个客户端操作传给 **rsm.Submit()**。为便于 reader goroutine 将 applyCh 消息与等待中的 **rsm.Submit()** 调用对应,**Submit()** 应将每个客户端操作与一个**唯一标识**一起包装进 **Op** 结构。**Submit()** 然后等待该操作提交并执行完毕,返回执行结果(**DoOp()** 的返回值)。若 **raft.Start()** 表明当前节点不是 Raft leader,**Submit()** 应返回 **rpc.ErrWrongLeader** 错误。**Submit()** 须检测并处理这种情况:在调用 **raft.Start()** 后 leadership 发生变化,导致该操作丢失(从未提交)。
|
||
|
||
在 Part A 中,rsm 测试程序充当服务,提交被解释为对单个整数状态做自增的操作。Part B 中你将把 rsm 用作实现 **StateMachine**(及 **DoOp()**)并调用 **rsm.Submit()** 的 key/value 服务的一部分。
|
||
|
||
顺利时,一次客户端请求的事件序列为:
|
||
|
||
1. 客户端向服务 leader 发送请求。
|
||
2. 服务 leader 用该请求调用 **rsm.Submit()**。
|
||
3. **rsm.Submit()** 用该请求调用 **raft.Start()**,然后等待。
|
||
4. Raft 提交该请求并向所有节点的 applyCh 发送。
|
||
5. 每个节点上的 rsm reader goroutine 从 applyCh 读取该请求并交给服务的 **DoOp()**。
|
||
6. 在 leader 上,rsm reader goroutine 将 **DoOp()** 的返回值交给最初提交该请求的 **Submit()** goroutine,**Submit()** 返回该值。
|
||
|
||
你的服务器之间不应直接通信;它们只应通过 Raft 交互。
|
||
|
||
**实现 rsm.go**:**Submit()** 方法以及 reader goroutine。当通过 rsm 的 4A 测试时,该任务即完成:
|
||
|
||
```bash
|
||
$ cd src/kvraft1/rsm
|
||
$ go test -v -run 4A
|
||
=== RUN TestBasic4A
|
||
Test RSM basic (reliable network)...
|
||
... Passed -- 1.2 3 48 0
|
||
--- PASS: TestBasic4A (1.21s)
|
||
=== RUN TestLeaderFailure4A
|
||
... Passed -- 9223372036.9 3 31 0
|
||
--- PASS: TestLeaderFailure4A (1.50s)
|
||
PASS
|
||
ok 6.5840/kvraft1/rsm 2.887s
|
||
```
|
||
|
||
* 不应需要给 Raft 的 ApplyMsg 或 AppendEntries 等 Raft RPC 增加字段,但允许这样做。
|
||
* 你的方案须处理:rsm leader 已为 **Submit()** 提交的请求调用了 **Start()**,但在该请求提交到日志前失去了 leadership。一种做法是 rsm 通过发现 Raft 的 term 已变或 **Start()** 返回的 index 处出现了不同请求,检测到已失去 leadership,并从 **Submit()** 返回 **rpc.ErrWrongLeader**。若旧 leader 独自处于分区中,它无法得知新 leader;但同一分区内的客户端也无法联系新 leader,因此服务器无限等待直到分区恢复是可以接受的。
|
||
* 测试在关闭节点时会调用你的 Raft 的 **rf.Kill()**。Raft 应**关闭 applyCh**,以便 rsm 得知关闭并退出所有循环。
|
||
|
||
---
|
||
|
||
## Part B: 无快照的 Key/value 服务
|
||
|
||
```bash
|
||
$ cd src/kvraft1
|
||
$ go test -v -run TestBasic4B
|
||
=== RUN TestBasic4B
|
||
Test: one client (4B basic) (reliable network)...
|
||
kvtest.go:62: Wrong error
|
||
$
|
||
```
|
||
|
||
现在用 rsm 包复制 key/value 服务器。每台服务器("**kvserver**")对应一个 rsm/Raft 节点。Clerk 向与 Raft leader 对应的 kvserver 发送 **Put()** 和 **Get()** RPC。kvserver 代码将 Put/Get 操作提交给 rsm,rsm 通过 Raft 复制并在每个节点调用你服务器的 **DoOp**,将操作应用到该节点的 key/value 数据库;目标是使各服务器维护一致的 key/value 数据库副本。
|
||
|
||
Clerk 有时不知道哪台 kvserver 是 Raft leader。若 Clerk 向错误的 kvserver 发 RPC 或无法到达该 kvserver,Clerk 应**重试**,向其他 kvserver 发送。若 key/value 服务将操作提交到其 Raft 日志(从而应用到 key/value 状态机),leader 通过回复该 RPC 将结果报告给 Clerk。若操作未提交(例如 leader 被替换),服务器报告错误,Clerk 向其他服务器重试。
|
||
|
||
你的 kvserver 之间不应直接通信;它们只应通过 Raft 交互。
|
||
|
||
第一个任务是实现**在无丢包、无服务器失败时**正确的方案。
|
||
|
||
可以将 Lab 2 的客户端代码(`kvsrv1/client.go`)复制到 `kvraft1/client.go`。需要增加决定每次 RPC 发往哪台 kvserver 的逻辑。
|
||
|
||
还需在 **server.go** 中实现 **Put()** 和 **Get()** 的 RPC 处理函数。这些处理函数应通过 **rsm.Submit()** 将请求提交给 Raft。rsm 包从 **applyCh** 读取命令时,会调用 **DoOp** 方法,你将在 **server.go** 中实现。
|
||
|
||
当你能**稳定**通过测试套件中第一个测试(`go test -v -run TestBasic4B`)时,该任务即完成。
|
||
|
||
* 若 kvserver **不处于多数**,则**不应完成 Get() RPC**(以免提供过期数据)。一种简单做法是像每个 **Put()** 一样,通过 **Submit()** 把每个 **Get()** 也写入 Raft 日志。不必实现论文 Section 8 中只读操作的优化。
|
||
* 最好从一开始就加锁,因为避免死锁有时会影响整体代码设计。用 **go test -race** 检查代码无数据竞争。
|
||
|
||
接下来应修改方案以**在网络和服务器故障下继续正确工作**。你会遇到的一个问题是 Clerk 可能需多次发送 RPC 才能找到能正常回复的 kvserver。若 leader 在将条目提交到 Raft 日志后立即失败,Clerk 可能收不到回复,从而向另一台 leader 重发请求。对同一 version 的每次 **Clerk.Put()** 调用应**只执行一次**。
|
||
|
||
**加入故障处理代码。** 你的 Clerk 可采用与 lab 2 类似的重试策略,包括在重试的 Put RPC 的回复丢失时返回 **ErrMaybe**。当你的代码能稳定通过全部 4B 测试(`go test -v -run 4B`)时,即完成。
|
||
|
||
* 回忆:rsm leader 可能失去 leadership 并从 **Submit()** 返回 **rpc.ErrWrongLeader**。此时应让 Clerk 向其他服务器重发请求直到找到新 leader。
|
||
* 可能需要修改 Clerk,使其**记住上一轮 RPC 中哪台服务器是 leader**,并优先将下一轮 RPC 发往该服务器。这样可避免每次 RPC 都重新找 leader,有助于在限定时间内通过部分测试。
|
||
|
||
你的代码此时应能通过 Lab 4B 测试,例如:
|
||
|
||
```bash
|
||
$ cd kvraft1
|
||
$ go test -run 4B
|
||
Test: one client (4B basic) ...
|
||
... Passed -- 3.2 5 1041 183
|
||
Test: one client (4B speed) ...
|
||
... Passed -- 15.9 3 3169 0
|
||
Test: many clients (4B many clients) ...
|
||
... Passed -- 3.9 5 3247 871
|
||
Test: unreliable net, many clients (4B unreliable net, many clients) ...
|
||
... Passed -- 5.3 5 1035 167
|
||
Test: unreliable net, one client (4B progress in majority) ...
|
||
... Passed -- 2.9 5 155 3
|
||
Test: no progress in minority (4B) ...
|
||
... Passed -- 1.6 5 102 3
|
||
Test: completion after heal (4B) ...
|
||
... Passed -- 1.3 5 67 4
|
||
Test: partitions, one client (4B partitions, one client) ...
|
||
... Passed -- 6.2 5 958 155
|
||
Test: partitions, many clients (4B partitions, many clients (4B)) ...
|
||
... Passed -- 6.8 5 3096 855
|
||
Test: restarts, one client (4B restarts, one client 4B ) ...
|
||
... Passed -- 6.7 5 311 13
|
||
Test: restarts, many clients (4B restarts, many clients) ...
|
||
... Passed -- 7.5 5 1223 95
|
||
Test: unreliable net, restarts, many clients (4B unreliable net, restarts, many clients ) ...
|
||
... Passed -- 8.4 5 804 33
|
||
Test: restarts, partitions, many clients (4B restarts, partitions, many clients) ...
|
||
... Passed -- 10.1 5 1308 105
|
||
Test: unreliable net, restarts, partitions, many clients (4B unreliable net, restarts, partitions, many clients) ...
|
||
... Passed -- 11.9 5 1040 33
|
||
Test: unreliable net, restarts, partitions, random keys, many clients (4B unreliable net, restarts, partitions, random keys, many clients) ...
|
||
... Passed -- 12.1 7 2801 93
|
||
PASS
|
||
ok 6.5840/kvraft1 103.797s
|
||
```
|
||
|
||
每个 Passed 后的数字依次为:**实际时间(秒)**、**节点数**、**发送的 RPC 数**(含客户端 RPC)、**执行的 key/value 操作数**(Clerk Get/Put 调用)。
|
||
|
||
---
|
||
|
||
## Part C: 带快照的 Key/value 服务
|
||
|
||
目前你的 key/value 服务器没有调用 Raft 库的 **Snapshot()** 方法,因此重启的服务器必须重放完整持久化 Raft 日志才能恢复状态。现在将修改 kvserver 和 rsm,与 Raft 协作以节省日志空间并缩短重启时间,使用 Lab 3D 的 Raft **Snapshot()**。
|
||
|
||
测试程序将 **maxraftstate** 传给你的 **StartKVServer()**,你再传给 rsm。**maxraftstate** 表示持久化 Raft 状态的**最大允许大小**(字节),含日志但不含快照。应将 **maxraftstate** 与 **rf.PersistBytes()** 比较。每当 rsm 检测到 Raft 状态大小接近该阈值时,应通过调用 Raft 的 **Snapshot** 保存快照。rsm 可通过调用 **StateMachine** 接口的 **Snapshot** 方法获取 kvserver 的快照来创建该快照。若 **maxraftstate** 为 **-1**,则不必做快照。maxraftstate 限制适用于你的 Raft 作为第一个参数传给 **persister.Save()** 的 GOB 编码字节。
|
||
|
||
persister 对象的源码在 **tester1/persister.go**。
|
||
|
||
**修改你的 rsm**,使其在检测到持久化 Raft 状态过大时向 Raft 提交快照。rsm 服务器重启时,应用 **persister.ReadSnapshot()** 读取快照,若快照长度大于零则传给 StateMachine 的 **Restore()** 方法。当通过 rsm 的 **TestSnapshot4C** 时,该任务即完成。
|
||
|
||
```bash
|
||
$ cd kvraft1/rsm
|
||
$ go test -run TestSnapshot4C
|
||
=== RUN TestSnapshot4C
|
||
... Passed -- 9223372036.9 3 230 0
|
||
--- PASS: TestSnapshot4C (3.88s)
|
||
PASS
|
||
ok 6.5840/kvraft1/rsm 3.882s
|
||
```
|
||
|
||
* 考虑 rsm **何时**应对状态做快照,以及快照中除服务器状态外还应**包含什么**。Raft 用 **Save()** 将每个快照与对应 Raft 状态一起存入 persister。可用 **ReadSnapshot()** 读取最新存储的快照。
|
||
* 快照中存储的结构体**所有字段名首字母大写**。
|
||
|
||
**实现 kvraft1/server.go 中的 Snapshot() 和 Restore() 方法**,供 rsm 调用。**修改 rsm 以处理 applyCh 上包含快照的消息。**
|
||
|
||
* 该任务可能暴露出 Raft 和 rsm 库中的 bug。若修改了 Raft 实现,请确保其仍能通过全部 Lab 3 测试。
|
||
* Lab 4 测试的合理耗时为**实际时间 400 秒**、**CPU 时间 700 秒**。
|
||
|
||
你的代码应通过 4C 测试(如下例),以及 4A+B 测试(且 Raft 须继续通过 Lab 3 测试)。
|
||
|
||
```bash
|
||
$ go test -run 4C
|
||
Test: snapshots, one client (4C SnapshotsRPC) ...
|
||
Test: InstallSnapshot RPC (4C) ...
|
||
... Passed -- 4.5 3 241 64
|
||
Test: snapshots, one client (4C snapshot size is reasonable) ...
|
||
... Passed -- 11.4 3 2526 800
|
||
Test: snapshots, one client (4C speed) ...
|
||
... Passed -- 14.2 3 3149 0
|
||
Test: restarts, snapshots, one client (4C restarts, snapshots, one client) ...
|
||
... Passed -- 6.8 5 305 13
|
||
Test: restarts, snapshots, many clients (4C restarts, snapshots, many clients ) ...
|
||
... Passed -- 9.0 5 5583 795
|
||
Test: unreliable net, snapshots, many clients (4C unreliable net, snapshots, many clients) ...
|
||
... Passed -- 4.7 5 977 155
|
||
Test: unreliable net, restarts, snapshots, many clients (4C unreliable net, restarts, snapshots, many clients) ...
|
||
... Passed -- 8.6 5 847 33
|
||
Test: unreliable net, restarts, partitions, snapshots, many clients (4C unreliable net, restarts, partitions, snapshots, many clients) ...
|
||
... Passed -- 11.5 5 841 33
|
||
Test: unreliable net, restarts, partitions, snapshots, random keys, many clients (4C unreliable net, restarts, partitions, snapshots, random keys, many clients) ...
|
||
... Passed -- 12.8 7 2903 93
|
||
PASS
|
||
ok 6.5840/kvraft1 83.543s
|
||
```
|
||
|
||
---
|
||
|
||
## 参考链接
|
||
|
||
* [6.5840 Lab 4: Fault-tolerant Key/Value Service (官方页面)](https://pdos.csail.mit.edu/6.824/labs/lab-kvraft1.html)
|