# 6.5840 Lab 5: Sharded Key/Value Service ## 简介 你可以选择做基于自己想法的期末项目,或做本实验。 本实验中你将构建一个 key/value 存储系统,在一组由 Raft 复制的 key/value 服务器组(**shardgrps**)上对 key 进行 **"shard"**(分片/分区)。一个 **shard** 是 key/value 对的一个子集;例如所有以 "a" 开头的 key 可以是一个 shard,以 "b" 开头的为另一个,等等。分片的目的是**性能**。每个 shardgrp 只处理少数 shard 的 put 和 get,各 shardgrp 并行工作;因此系统总吞吐(单位时间内的 put/get 数)随 shardgrp 数量增加。 分片 key/value 服务的组件见实验示意图。**Shardgrps**(蓝色方块)存储带 key 的 shard:shardgrp 1 存 key "a" 的 shard,shardgrp 2 存 key "b" 的 shard。客户端通过 **clerk**(绿色圆)与服务交互,clerk 实现 **Get** 和 **Put** 方法。为找到 Put/Get 所传 key 对应的 shardgrp,clerk 从 **kvsrv**(黑色方块,即你在 **Lab 2** 实现的)获取 **configuration**。Configuration 描述从 shard 到 shardgrp 的映射(例如 shard 1 由 shardgrp 3 服务)。 管理员(即测试程序)使用另一个客户端 **controller**(紫色圆)向集群添加/移除 shardgrp 并更新应由哪个 shardgrp 服务哪个 shard。Controller 有一个主要方法:**ChangeConfigTo**,以新 configuration 为参数,将系统从当前 configuration 切换到新 configuration;这涉及将 shard 迁移到新加入的 shardgrp、以及从即将离开的 shardgrp 迁出。为此 controller 1) 向 shardgrp 发 RPC(**FreezeShard**、**InstallShard**、**DeleteShard**),2) 更新存储在 kvsrv 中的 configuration。 引入 controller 是因为分片存储系统必须能**在 shardgrp 之间迁移 shard**:用于负载均衡,或当 shardgrp 加入、离开时(新容量、维修、下线)。 本实验的主要挑战是在 1) shard 到 shardgrp 的分配发生变化,以及 2) controller 在 **ChangeConfigTo** 期间失败或处于分区时恢复的情况下,保证 Get/Put 操作的 **linearizability**。 1. **若 ChangeConfigTo 在重配置过程中失败**,部分 shard 可能已开始但未完成从一 shardgrp 迁到另一 shardgrp,从而不可访问。测试会启动新的 controller;你的任务是确保新的能完成旧 controller 未完成的重配置。 2. **ChangeConfigTo 会在 shardgrp 之间迁移 shard**。你必须保证**任意时刻每个 shard 最多只有一个 shardgrp 在服务请求**,这样使用旧 shardgrp 与新 shardgrp 的客户端不会破坏 linearizability。 本实验用 "configuration" 指 **shard 到 shardgrp 的分配**。这与 Raft 集群成员变更**不是**一回事;不需要实现 Raft 集群成员变更。 一个 shardgrp 服务器只属于一个 shardgrp。给定 shardgrp 内的服务器集合不会改变。 客户端与服务器之间的交互**只能通过 RPC**(不得使用共享 Go 变量或文件)。 * **Part A**:实现可用的 **shardctrler**(在 kvsrv 中存储/读取 configuration)、**shardgrp**(用 Raft rsm 复制)和 **shardgrp clerk**。shardctrler 通过 shardgrp clerk 迁移 shard。 * **Part B**:修改 shardctrler,在 configuration 变更期间处理故障与分区。 * **Part C**:允许多个 controller 并发且互不干扰。 * **Part D**:以任意方式扩展你的方案(可选)。 本实验的设计与 Flat Datacenter Storage、BigTable、Spanner、FAWN、Apache HBase、Rosebud、Spinnaker 等思路一致(细节不同)。 Lab 5 将使用你在 **Lab 2** 的 **kvsrv**,以及 **Lab 4** 的 **rsm 和 Raft**。Lab 5 与 Lab 4 必须使用相同的 rsm 和 Raft 实现。 迟交时长仅可用于 **Part A**;**不能**用于 Part B–D。 --- ## 起步 执行 `git pull` 获取最新实验代码。 我们在 **src/shardkv1** 中提供了测试和骨架代码: * **shardctrler** 包:`shardctrler.go`,包含 controller 变更 configuration 的 **ChangeConfigTo** 和获取 configuration 的 **Query** * **shardgrp** 包:shardgrp clerk 与 server * **shardcfg** 包:计算 shard configuration * **client.go**:shardkv clerk 运行以下命令即可开始: ```bash $ cd ~/6.5840 $ git pull ... $ cd src/shardkv1 $ go test -v === RUN TestInitQuery5A Test (5A): Init and Query ... (reliable network)... shardkv_test.go:46: Static wrong null 0 ... ``` --- ## Part A: 迁移 Shard(困难) 第一个任务是:在无故障时实现 shardgrp 以及 **InitConfig**、**Query**、**ChangeConfigTo**。Configuration 的代码在 **shardkv1/shardcfg**。每个 **shardcfg.ShardConfig** 有唯一编号 **Num**、从 shard 编号到 group 编号的映射、以及从 group 编号到复制该 group 的服务器列表的映射。通常 shard 数多于 group 数,以便以较细粒度调整负载。 ### 1. InitConfig 与 Query(尚无 shardgrp) 在 **shardctrler/shardctrler.go** 中实现: * **Query**:返回当前 configuration;从 kvsrv 读取(由 InitConfig 存储)。 * **InitConfig**:接收第一个 configuration(测试程序提供的 **shardcfg.ShardConfig**)并存入 Lab 2 的 **kvsrv** 实例。 用 **ShardCtrler.IKVClerk** 的 Get/Put 与 kvsrv 通信,用 **ShardConfig.String()** 序列化后 Put,用 **shardcfg.FromString()** 反序列化。通过第一个测试时即完成: ```bash $ cd ~/6.5840/src/shardkv1 $ go test -run TestInitQuery5A Test (5A): Init and Query ... (reliable network)... ... Passed -- time 0.0s #peers 1 #RPCs 3 #Ops 0 PASS ok 6.5840/shardkv1 0.197s ``` ### 2. Shardgrp 与 shardkv clerk(Static 测试) 通过从 Lab 4 kvraft 方案复制,在 **shardkv1/shardgrp/server.go** 实现 **shardgrp** 的初始版本,在 **shardkv1/shardgrp/client.go** 实现 **shardgrp clerk**。在 **shardkv1/client.go** 实现 **shardkv clerk**:用 **Query** 找到 key 对应的 shardgrp,再与该 shardgrp 通信。通过 **Static** 测试时即完成。 * 创建时,第一个 shardgrp(**shardcfg.Gid1**)应将自己初始化为**拥有所有 shard**。 * **shardkv1/client.go** 的 Put 在回复可能丢失时必须返回 **ErrMaybe**;内部(shardgrp)的 Put 可用错误表示这一点。 * 要向 shardgrp put/get 一个 key,shardkv clerk 应通过 **shardgrp.MakeClerk** 创建 shardgrp clerk,传入 configuration 中的服务器以及 shardkv clerk 的 **ck.clnt**。用 **ShardConfig.GidServers()** 获取 shard 的 group。 * 用 **shardcfg.Key2Shard()** 得到 key 的 shard 编号。测试程序将 **ShardCtrler** 传给 **shardkv1/client.go** 的 **MakeClerk**;用 **Query** 获取当前 configuration。 * 可从 kvraft 的 **client.go** 和 **server.go** 复制 Put/Get 及相关代码。 ```bash $ cd ~/6.5840/src/shardkv1 $ go test -run Static Test (5A): one shard group ... (reliable network)... ... Passed -- time 5.4s #peers 1 #RPCs 793 #Ops 180 PASS ok 6.5840/shardkv1 5.632s ``` ### 3. ChangeConfigTo 与 shard 迁移(JoinBasic、DeleteBasic) 通过实现 **ChangeConfigTo** 支持**在 group 之间迁移 shard**:从旧 configuration 切换到新 configuration。新 configuration 可能加入新 shardgrp 或移除现有 shardgrp。Controller 必须迁移 shard **数据**,使每个 shardgrp 存储的 shard 与新 configuration 一致。 **迁移一个 shard 的建议流程:** 1. 在源 shardgrp **Freeze** 该 shard(该 shardgrp 拒绝对该迁移中 shard 的 key 的 Put)。 2. **Install**(复制)该 shard 到目标 shardgrp。 3. 在源端 **Delete** 已 freeze 的 shard。 4. **Post** 新 configuration,使客户端能找到迁移后的 shard。 这样避免 shardgrp 之间直接交互,并允许继续服务未参与变更的 shard。 **顺序**:每个 configuration 有唯一 **Num**(见 **shardcfg/shardcfg.go**)。Part A 中测试程序顺序调用 ChangeConfigTo;新 config 的 **Num** 比前一个大 1。为拒绝过时 RPC,**FreezeShard**、**InstallShard**、**DeleteShard** 应包含 **Num**(见 **shardgrp/shardrpc/shardrpc.go**),且 shardgrp 须记住每个 shard 见过的**最大 Num**。 在 **shardctrler/shardctrler.go** 中实现 **ChangeConfigTo**,并扩展 shardgrp 支持 **freeze**、**install**、**delete**。在 **shardgrp/client.go** 和 **shardgrp/server.go** 中实现 **FreezeShard**、**InstallShard**、**DeleteShard**,使用 **shardgrp/shardrpc** 中的 RPC,并根据 Num 拒绝过时 RPC。修改 **shardkv1/client.go** 中的 shardkv clerk 以处理 **ErrWrongGroup**(当 shardgrp 不负责该 shard 时返回)。先通过 **JoinBasic** 和 **DeleteBasic**(加入 group;离开可稍后)。 * 像 Put 和 Get 一样,通过你的 **rsm** 包执行 **FreezeShard**、**InstallShard**、**DeleteShard**。 * 若 RPC 回复中包含属于服务器状态的 **map**,可能产生数据竞争;**在回复中附带该 map 的副本**。 * 可以在 RPC 请求/回复中发送整个 map,使 shard 迁移代码更简单。 * 若 Put/Get 的 key 的 shard 未分配给该 shardgrp,shardgrp 应返回 **ErrWrongGroup**;**shardkv1/client.go** 应重新读取 configuration 并重试。 ### 4. 离开的 Shardgrps(TestJoinLeaveBasic5A) 扩展 **ChangeConfigTo** 以处理**离开**的 shardgrp(在当前 config 中但不在新 config 中)。通过 **TestJoinLeaveBasic5A**。 ### 5. 全部 Part A 测试 你的方案必须**在 configuration 变更进行时继续服务未受影响的 shard**。通过全部 Part A 测试: ```bash $ cd ~/6.5840/src/shardkv1 $ go test -run 5A Test (5A): Init and Query ... (reliable network)... ... Passed -- time 0.0s #peers 1 #RPCs 3 #Ops 0 Test (5A): one shard group ... (reliable network)... ... Passed -- time 5.1s #peers 1 #RPCs 792 #Ops 180 Test (5A): a group joins... (reliable network)... ... Passed -- time 12.9s #peers 1 #RPCs 6300 #Ops 180 ... Test (5A): many concurrent clerks unreliable... (unreliable network)... ... Passed -- time 25.3s #peers 1 #RPCs 7553 #Ops 1896 PASS ok 6.5840/shardkv1 243.115s ``` --- ## Part B: 处理失败的 Controller(简单) Controller 生命周期短,在迁移 shard 时可能**失败或失去连接**。任务是在启动新 controller 时**恢复**:新 controller 必须**完成**前一个未完成的重配置。测试程序在启动 controller 时调用 **InitController**;你可以在其中实现恢复。 **做法**:在 controller 的 kvsrv 中维护**两个 configuration**:**current** 和 **next**。Controller 开始重配置时存储 next configuration。完成时把 next 变为 current。在 **InitController** 中,若存在存储的 **next** configuration 且其 Num 大于 current,则**完成 shard 迁移**以重配置到该 next config。 从前一个失败 controller 继续的 controller 可能**重复** FreezeShard、InstallShard、Delete RPC;shardgrp 可用 **Num** 检测重复并拒绝。 在 shardctrler 中实现上述逻辑。通过 Part B 测试时即完成: ```bash $ cd ~/6.5840/src/shardkv1 $ go test -run 5B Test (5B): Join/leave while a shardgrp is down... (reliable network)... ... Passed -- time 9.2s #peers 1 #RPCs 899 #Ops 120 Test (5B): recover controller ... (reliable network)... ... Passed -- time 26.4s #peers 1 #RPCs 3724 #Ops 360 PASS ok 6.5840/shardkv1 35.805s ``` * 在 **shardctrler/shardctrler.go** 的 **InitController** 中实现恢复。 --- ## Part C: 并发 Configuration 变更(中等) 修改 controller 以允许**多个 controller 并发**。当某个失败或处于分区时,测试会启动新的,新 controller 必须完成任何进行中的工作(同 Part B)。因此多个 controller 可能并发运行,向 shardgrp 和 kvsrv 发 RPC。 **挑战**:确保 controller **互不干扰**。Part A 中你已用 **Num** 对 shardgrp RPC 做 fencing,过时 RPC 会被拒绝;多个 controller 的重复工作是安全的。剩余问题是**只有一个 controller** 应更新 **next** configuration,这样两个 controller(例如分区中的与新的)不会为同一 Num 写入不同 config。测试会并发运行多个 controller;每个读取当前 config、为 join/leave 更新、然后调用 ChangeConfigTo——因此多个 controller 可能用**同一 Num 的不同 config** 调用 ChangeConfigTo。可使用 **version 与带 version 的 Put**,使只有一个 controller 能成功提交该 Num 的 next config,其他直接返回不做任何事。 修改 controller,使**对给定 configuration Num 只有一个 controller 能提交 next configuration**。通过并发测试: ```bash $ cd ~/6.5840/src/shardkv1 $ go test -run TestConcurrentReliable5C Test (5C): Concurrent ctrlers ... (reliable network)... ... Passed -- time 8.2s #peers 1 #RPCs 1753 #Ops 120 PASS ok 6.5840/shardkv1 8.364s $ go test -run TestAcquireLockConcurrentUnreliable5C Test (5C): Concurrent ctrlers ... (unreliable network)... ... Passed -- time 23.8s #peers 1 #RPCs 1850 #Ops 120 PASS ok 6.5840/shardkv1 24.008s ``` * 参见 **test.go** 中的 **concurCtrler** 了解测试如何并发运行 controller。 **恢复 + 新 controller**:新 controller 仍应执行 Part B 的恢复。若旧 controller 在 ChangeConfigTo 期间处于分区,确保旧的不干扰新的。若所有 controller 更新都用 Num 正确 fencing(Part B),可能不需要额外代码。通过 **Partition** 测试: ```bash $ go test -run Partition Test (5C): partition controller in join... (reliable network)... ... Passed -- time 7.8s #peers 1 #RPCs 876 #Ops 120 ... Test (5C): controllers with leased leadership ... (unreliable network)... ... Passed -- time 60.5s #peers 1 #RPCs 11422 #Ops 2336 PASS ok 6.5840/shardkv1 217.779s ``` 重新运行全部测试,确保最近的 controller 修改没有破坏前面的部分。 **Gradescope** 会运行 Lab 3A–D、Lab 4A–C 和 5C 测试。提交前: ```bash $ go test ./raft1 $ go test ./kvraft1 $ go test ./shardkv1 ``` --- ## Part D: 扩展你的方案 在这最后一部分你可以**以任意方式扩展**你的方案。你必须**为自己的扩展编写测试**。 实现下列想法之一或你自己的想法。在 **extension.md** 中写**一段话**描述你的扩展,并将 **extension.md** 上传到 Gradescope。对较难、开放式的扩展,可与另一名同学组队。 **想法(前几个较易,后面更开放):** * **(难)** 修改 shardkv 以支持**事务**(跨 shard 的多个 Put 和 Get 原子执行)。实现两阶段提交与两阶段锁。编写测试。 * **(难)** 在 kvraft 中支持**事务**(多个 Put/Get 原子)。这样带 version 的 Put 不再必要。参见 [etcd's transactions](https://etcd.io/docs/v3.4/learning/api/)。编写测试。 * **(难)** 让 kvraft **leader 不经 rsm 直接处理 Get**(Raft 论文 Section 8 末尾的优化,含 **leases**),并保持 linearizability。通过现有 kvraft 测试。增加测试:优化后的 Get 更快(如更少 RPC)、以及 term 切换更慢(新 leader 等待 lease 过期)。 * **(中等)** 为 kvsrv 增加 **Range** 函数(从 low 到 high 的 key)。偷懒做法:遍历 key/value map;更好做法:支持范围查询的数据结构(如 B-tree)。包含一个在偷懒实现下失败、在更好实现下通过的测试。 * **(中等)** 将 kvsrv 改为 **恰好一次** Put/Get 语义(如 Lab 2 丢包风格)。在 kvraft 中也实现恰好一次。可移植 2024 的测试。 * **(简单)** 修改测试程序,使 controller 使用 **kvraft** 而非 kvsrv(例如在 test.go 的 MakeTestMaxRaft 中用 kvraft.StartKVServer 替换 kvsrv.StartKVServer)。编写测试:在一个 kvraft 节点宕机时 controller 仍能查询/更新 configuration。测试代码在 **src/kvtest1**、**src/shardkv1**、**src/tester1**。 --- ## 提交步骤 提交前最后运行一遍全部测试: ```bash $ go test ./raft1 $ go test ./kvraft1 $ go test ./shardkv1 ``` --- ## 参考链接 * [6.5840 Lab 5: Sharded Key/Value Service (官方页面)](https://pdos.csail.mit.edu/6.824/labs/lab-shard1.html)