Files
6.824-golabs-2021-6.824/docs/6.5840: Distributed System/7. Lab 5: Sharded Key-Value Service-cn.md
2026-02-25 23:17:05 +08:00

16 KiB
Raw Permalink Blame History

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 的 shardshardgrp 1 存 key "a" 的 shardshardgrp 2 存 key "b" 的 shard。客户端通过 clerk绿色圆与服务交互clerk 实现 GetPut 方法。为找到 Put/Get 所传 key 对应的 shardgrpclerk 从 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 发 RPCFreezeShardInstallShardDeleteShard2) 更新存储在 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 中存储/读取 configurationshardgrp(用 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 2kvsrv,以及 Lab 4rsm 和 Raft。Lab 5 与 Lab 4 必须使用相同的 rsm 和 Raft 实现。

迟交时长仅可用于 Part A不能用于 Part BD。


起步

执行 git pull 获取最新实验代码。

我们在 src/shardkv1 中提供了测试和骨架代码:

  • shardctrler 包:shardctrler.go,包含 controller 变更 configuration 的 ChangeConfigTo 和获取 configuration 的 Query
  • shardgrpshardgrp clerk 与 server
  • shardcfg 包:计算 shard configuration
  • client.goshardkv clerk

运行以下命令即可开始:

$ 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 以及 InitConfigQueryChangeConfigTo。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() 序列化后 Putshardcfg.FromString() 反序列化。通过第一个测试时即完成:

$ 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 clerkStatic 测试)

通过从 Lab 4 kvraft 方案复制,在 shardkv1/shardgrp/server.go 实现 shardgrp 的初始版本,在 shardkv1/shardgrp/client.go 实现 shardgrp clerk。在 shardkv1/client.go 实现 shardkv clerk:用 Query 找到 key 对应的 shardgrp再与该 shardgrp 通信。通过 Static 测试时即完成。

  • 创建时,第一个 shardgrpshardcfg.Gid1)应将自己初始化为拥有所有 shard
  • shardkv1/client.go 的 Put 在回复可能丢失时必须返回 ErrMaybe内部shardgrp的 Put 可用错误表示这一点。
  • 要向 shardgrp put/get 一个 keyshardkv clerk 应通过 shardgrp.MakeClerk 创建 shardgrp clerk传入 configuration 中的服务器以及 shardkv clerk 的 ck.clnt。用 ShardConfig.GidServers() 获取 shard 的 group。
  • shardcfg.Key2Shard() 得到 key 的 shard 编号。测试程序将 ShardCtrler 传给 shardkv1/client.goMakeClerk;用 Query 获取当前 configuration。
  • 可从 kvraft 的 client.goserver.go 复制 Put/Get 及相关代码。
$ 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。为拒绝过时 RPCFreezeShardInstallShardDeleteShard 应包含 Num(见 shardgrp/shardrpc/shardrpc.go),且 shardgrp 须记住每个 shard 见过的最大 Num

shardctrler/shardctrler.go 中实现 ChangeConfigTo,并扩展 shardgrp 支持 freezeinstalldelete。在 shardgrp/client.goshardgrp/server.go 中实现 FreezeShardInstallShardDeleteShard,使用 shardgrp/shardrpc 中的 RPC并根据 Num 拒绝过时 RPC。修改 shardkv1/client.go 中的 shardkv clerk 以处理 ErrWrongGroup(当 shardgrp 不负责该 shard 时返回)。先通过 JoinBasicDeleteBasic(加入 group离开可稍后

  • 像 Put 和 Get 一样,通过你的 rsm 包执行 FreezeShardInstallShardDeleteShard
  • 若 RPC 回复中包含属于服务器状态的 map,可能产生数据竞争;在回复中附带该 map 的副本
  • 可以在 RPC 请求/回复中发送整个 map使 shard 迁移代码更简单。
  • 若 Put/Get 的 key 的 shard 未分配给该 shardgrpshardgrp 应返回 ErrWrongGroupshardkv1/client.go 应重新读取 configuration 并重试。

4. 离开的 ShardgrpsTestJoinLeaveBasic5A

扩展 ChangeConfigTo 以处理离开的 shardgrp在当前 config 中但不在新 config 中)。通过 TestJoinLeaveBasic5A

5. 全部 Part A 测试

你的方案必须在 configuration 变更进行时继续服务未受影响的 shard。通过全部 Part A 测试:

$ 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 中维护两个 configurationcurrentnext。Controller 开始重配置时存储 next configuration。完成时把 next 变为 current。在 InitController 中,若存在存储的 next configuration 且其 Num 大于 current完成 shard 迁移以重配置到该 next config。

从前一个失败 controller 继续的 controller 可能重复 FreezeShard、InstallShard、Delete RPCshardgrp 可用 Num 检测重复并拒绝。

在 shardctrler 中实现上述逻辑。通过 Part B 测试时即完成:

$ 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.goInitController 中实现恢复。

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。通过并发测试:

$ 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 正确 fencingPart B可能不需要额外代码。通过 Partition 测试:

$ 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 3AD、Lab 4AC 和 5C 测试。提交前:

$ 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。编写测试。
  • (难) 让 kvraft leader 不经 rsm 直接处理 GetRaft 论文 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/kvtest1src/shardkv1src/tester1

提交步骤

提交前最后运行一遍全部测试:

$ go test ./raft1
$ go test ./kvraft1
$ go test ./shardkv1

来源: 6.5840 Lab 5: Sharded Key/Value Service