mit_6.824_2021_lab4B_Sharded_Key/Value_Server
最后,构建一个分片的 kv 容错系统;感觉比 lab2 更难理解,因为 lab2 有一个模板论文可以参考,但是 lab4b 基本没有一个固定的范式
Part B: Sharded Key/Value Server
每个 shardkv server 相当于副本组的一部分,每个副本组相当于 lab3,都支持Get, Put, Append
操作,当然它们各自仅维护各自的分片;
客户端使用key2shard()
去找到一个 key 对应哪个分片;
shardctrler 服务单例将分片分配给副本组;当这个分片分配发生变化时,raft 副本组之间必须互相交换分片,同时确保客户端不会看到不一致的响应。
multi-raft group 作为一个完整的存储系统,需要对外线性一致性,考虑当客户端发起请求和配置更改同时发生时,请求需要无感知响应
容错
从大到小的粒度有:整个 kv 系统 + 一个 raft 分片控制器组 > 多个 raft 分片副本组 > 一个 raft 副本组内的各个 server 节点
当一个 raft 组内的大多数能正常通信,该 raft 组可以正常工作;当至少一个 raft 组能和 分片控制器组单例通信时,该分片系统才能正常工作
实现必须能正常运行(为请求提供服务,并能够根据需要重新配置),即使某些副本组中的少数服务器死机、暂时不可用或运行缓慢(高分区容错性)
关于raft 组内的成员变更
一个shardkv服务器仅仅是一个副本组的成员,给定副本组中的服务器集永远不会更改。
即不需要实现 raft 组成员变更
关于客户端
Clerk 将每个RPC发送到负责RPC键的副本组;但如果该 key 并不在副本组负责的分片中,副本组会提示 Clerk 重定向;在这种情况下,客户端需要向分片控制器询问最新的配置并重试。参考 kvraft,进行修改;
实验要求
首要任务是通过第一个 shardkv 测试;最大的改变是需要让服务器知道配置发生了更改并开始接受新配置分片所属的请求;
配置变更
通过第一个测试之后,需要处理配置变更;
需要让服务器监听配置变更,一旦配置发生变更,需要开始分片迁移过程;
-
若一个副本组卸载了一个分片,其必须立即停止服务该分片的请求,并开始迁移分片数据到新接管的副本组
-
若一个副本组装载了一个分片,其需要等该分片的前任发送完分片数据后开始接收该分片的请求
确保副本组内的所有服务器在同一线性顺序下完成分片迁移,以至于服务器可以同时接受或拒绝客户端请求;
在处理后面的测试之前,您应该集中精力通过第二个测试join then leave
。当您通过到TestDelete
(不包括TestDelete)的所有测试时,就完成了此任务。
实验提示
-
服务器不需要调用分片控制器的
Join()
,tester 才会去调用; -
服务器将需要定期轮询 shardctrler 以监听新的配置。预期大约每100毫秒轮询一次;可以更频繁,但过少可能会导致 bug。
-
服务器需要互相发送rpc,以便在配置更改期间传输分片。shardctrler的Config结构包含服务器名,一个 Server 需要一个
labrpc.ClientEnd,
以便发送RPC。使用make_end()
函数传给StartServer()
函数将服务器名转换为ClientEnd。shardkv /client.go
需要实现这些逻辑。 -
在
server.go
中添加代码去周期性从 shardctrler 拉取最新的配置,并且当请求分片不属于自身时,拒绝请求 -
当被请求到错误分片时,需要返回
ErrWrongGroup
给客户端,并确保Get, Put, Append
在面临并发重配置时能正确作出决定 -
重配置需要按流程执行唯一一次
-
labgob 的提示错误不能忽视,它可能导致实验不过
-
分片重分配的请求也需要做重复请求检测
-
若客户端收到
ErrWrongGroup
,是否更改请求序列号?若服务器执行请求时返回ErrWrongGroup
,是否更新客户端信息? -
当服务器转移到新配置后,它可以继续存储它不再负责的分片(生产环境中这是不允许的),但这个可以简化实现
-
当 G1 在配置变更时需要来自 G2 的分片数据,G2 处理日志条目的哪个时间点将分片发送给 G1 是最好的?
-
你可以在整个 rpc 请求或回复中发送整个 map,这可以简化分片传输
-
map 是引用类型,所以在发送 map 的时候,建议先拷贝一次,避免 data race(在 labrpc 框架下,接收 map 时也需要拷贝)
-
在配置更改期间,一对组可能需要互相传送分片,这可能会发生死锁
challenge
如果想达到生产环境系统级别,如下两个挑战是需要实现的
challenge1:Garbage collection of state
当一个副本组失去一个分片的所有权时,副本组需要删除该分片数据。但这给迁移带来一些问题,考虑两个组G1 和 G2,并且新配置C 将分片从 G1 移动到 G2,若 G1 在转换配置到C时删除了数据库中的分片,当G2 转换到C时,如何获取 G1 的数据
实验要求
使每个副本组保留旧分片的时长不再是无限时长,即使副本组(如上面的G1)中的所有服务器崩溃并恢复正常,解决方案也必须工作。如果您通过TestChallenge1Delete
,您就完成了这个挑战。
解决方案
在分片迁移成功之后,可以立马进行分片 GC 了,GC 完毕后再进入到配置更新阶段
chanllenge2:Client requests during configuration changes
配置更改期间最简单的方式是禁止所有客户端操作直到转换完成,虽然简单但是不满足于生产环境要求,这将导致客户端长时间停滞,最好可以继续为不受当前配置更改的分片提供服务
上述优化还能更好,若 G3 在过渡到配置C时,需要来自G1 的分片S1 和 G2 的分片S2。希望 G3 能在收到其中一个分片后可以立即开始接收针对该分片的请求。如G1宕机了,G3在收到G2的分片数据后,可以立即为 S2 分片提供服务,而不需要等待 C 配置转换完全完成
实验要求
修改您的解决方案,以便在配置更改期间继续执行不受影响的分片中的 key 的客户端操作。当您通过 TestChallenge2Unaffected
测试时,您已经完成了这个挑战。
修改您的解决方案,在配置转换进行中,副本组也可以立即开始提供分片服务。当您通过TestChallenge2Partial
测试时,您已经完成了这个挑战。
解决方案
分片迁移应该是以 gid 为单位,即 gid -> []shard
,这样即使一个 gid 挂了,也不会影响到另一个 gid 中的分片
以上总结自实验官网:https://pdos.csail.mit.edu/6.824/labs/lab-shard.html
不知道什么情况,在 2021 的 schedule 里点进 lab4 ,还是 2020 的
实验思路
一开始真的无从下手,信息量太大,看别人的博客文章甚至都不知道问题本身在描述什么;
在参考了 https://github.com/LebronAl/MIT6.824-2021/blob/master/docs/lab4.md 的讨论后,有了些许眉目
概括来说,lab4B 需要你实现一个 基于 multi-raft 的分片分布式 key-value 系统,主要包含两个部分shardctrler
配置管理器,以及shardkv
分片key-value 系统,架构图如下
并且在行为上:
- 客户端可以和
shardctrler
交互,拿到最新的配置,然后用该配置找到对应 key 的 shard,最终请求服务该 shard 的 group - 服务端也需要定期和
shardctrler
交互,保证更新到最新配置(monitor) - 棘手的部分是:测试代码会偶尔执行
Join
,Leave
等,让某些 group 在系统中脱离,然后各个 group 之间能依照最新配置(lab4A),做分片的迁移和分片的GC
如果没有第三部分,其实前两部分很好实现
Clerk
想一切从简,先尝试从 Clerk 改造入手,和 lab4A 引入的概念一样,在 multi-raft 的框架下,变成了一个 group 下包含多个 servers:group_id(gid) -> server_id,并且一个 group 可能负责多个 shard;则 Clerk 的请求逻辑为:
- 根据
shard
从当前配置中拿到有效的 gid:gid := config.Shards[shard]
- 继续从配置中拿到有效的 group 信息:
group := config.Groups[gid]
- 循环:在一个 group 中重复寻找 leader,直到请求成功
- 若
ErrWrongGroup
或整个 group 都遍历请求过了,则跳出 3. 的循环 Query
最新的配置,回到 1. 重复
for {
shard := key2shard(key)
if gid := ck.config.Shards[shard]; gid != 0 {
leaderId := ck.gid2LeaderIdMap[gid]
oldLeaderId := leaderId
if group, ok := ck.config.Groups[gid]; ok {
for {
var reply Reply
serverName := group[leaderId]
ok := ck.sendGet(serverName, &args, &reply) // makeEnd & Call