Lab4
实验介绍
这个实验是在之前几个Lab的基础上实现数据的分片存储,就像Spanner视频里面说的那样,把相同key值的数据存储到一个group里面,这样在写入类似 A:XXX B:XXX C:XXX 数据的时候可以并行的往3个group里面插入XXX.
服务包含两个组件,replica groups和shard controller,如下图示.

由于热点数据和节点上下线的原因,数据需要在每个group之前复制移动.
实验的难点在于重配置,重配置的意思是本来 key:A 是存在第一个分组, key:B 是存在第二个分组,然后经过重配置之后 key:A 现在要存到第三个分组去了,如果重配置信息和数据同时到达,配置信息比数据到得晚的话,数据就会写入,到得早的话,数据就不能写入,而要分配到第三个group去写入.所以你需要Raft的log来记录不仅仅是数据的序列,也要把重配置的序列插入log,并且需要确保同一时间只有一个group为每个分片服务.
重分配之后,已经存储在group内的数据也需要同步迁移到新的group
4A
实验任务
完成 shardctrler/server.go 和 client.go 模块的 shard controller
在 shardctrler/目录中实现 client.go 和 server.go 中指定的接口。Shardctrler 必须是容错的,使用Lab2/3中的 Raft 库。在对实验4评分时重新运行实验2和3中的测试,所以请确保不要在 Raft 实现中引入 bug。
提示
Shardctrler 管理一系列配置,每个配置描述了副本组的组成并且向副本组分配分片,当分片配置需要更改,shard controller 创建一个新的配置.kv客户端和服务器会连接shardctrler请求配置.
实现 shardctrler/common.go 中描述的 RPC 接口,该接口由 Join、 Leave、 Move 和 Query RPC 组成。这些 RPC 旨在允许管理员(和测试)控制 shardctrler: 添加新的副本组,删除副本组,并在副本组之间转移数据。
Join RPC 用来添加新的副本组。它的参数是从唯一的非零副本组标识符(GID)到服务器名称列表的一组映射。Shardctrler 应该通过创建包含新副本组的新配置来做出反应。新配置应该将分片尽可能均匀地分配到完整的组中,并且应该移动尽可能少的分片以实现该目标。如果 GID 不是当前配置的一部分,shardctrler 应该允许重用它(例如,应该允许 GID 先加入,然后退出,然后再加入)。
Leave RPC 的参数是以前加入的组的 GID 列表。Shardctrler 应该创建一个不包含这些组的新配置,并将这些组的分片分配给其余的组。新配置应该在组之间尽可能均匀地分配分片,并且应该尽可能少地移动分片以实现这一目标。
Move RPC 的参数是一个分片号和一个 GID。Shardctrler 应该创建一个新配置,其中将分片分配给组。Move 的目的是允许我们测试您的软件。在 Move 之后加入或离开可能会撤销还原Move,因为Join和Leave会重新分配分片。
Query RPC 的参数是一个配置号。Shardctrler 使用具有该数字的配置进行响应。如果该数字为 -1或大于已知的最大配置数,shardctrler 应该使用最新的配置进行响应。Query (- 1)的结果应该反映 shardctrler 在接收 Query (- 1) RPC 之前完成处理的每个 Join、 Leave 或 Move RPC。
第一个配置应该编号为零。它不应该包含任何组,并且所有碎片都应该分配给 GID 0(一个无效的 GID)。下一个配置(为响应 JoinRPC 而创建)应该编号为1,& c。通常会有比组更多的碎片(也就是说,每个组将提供多于一个碎片) ,以便负载能够以相当细的粒度进行转移。
- 从一个精简版的 Kvraft 服务器开始。
- 您应该实现对分片控制器的 RPC 的重复客户端请求检测。Shardctrler 测试不会测试这一点,但 shardkv 测试稍后将在不可靠的网络上使用 shardctrler; 如果 shardctrler 不过滤掉重复的 RPC,那么您可能难以通过 shardkv 测试。
- 执行分片重新平衡的状态机中的代码需要是确定性的。在 Go 中,map迭代顺序是不确定的。
- go 的 map 是引用,如果你把map类型的变量赋值给另一个变量,这两个变量指向的是同一个 map ,所以你基于之前的配置创建一个新的配置,你需要make一个新的map,并分别赋值kv
- 使用 go test -race 测试来检测 datarace
实现
- 
整体框架和 Lab3 保持一致,不用实现snapshot功能. 
- 
query 和 move 比较简单,join和leave之后要重新平衡shards,这里我的实现感觉比较蠢,也没看别人怎么做的,测试里面有一个测试leave的环节,join之后整个groups的gid数量会大于10个,然后leave之后会等于10个,leave之后要重新扫描groups把shards里面没有包含的shard给重新分配进去. 
- 
整体思路就是找到shards里面出现最多和最少的gid,然后优先把数量最多的gid分配一个给新的gid,如果都分配完了就把最多的分配给最少的,直到最多数量和最小数量差不超过1,leave的时候把删除的位置gid置0,rebalance计算最大最小数量的时候不考虑gid=0的情况,优先把0用别的gid替换. 
func reBalance(shards [NShards]int, newGroups map[int][]string) [NShards]int {
	DPrintf("reBalance shards:%v newGroups:%v", shards, newGroups)
	// 新的Groups增加的Gid
	newGid := make([]int, len(newGroups))
	newGidIndex := 0
	for k := range newGroups {
		newGid[newGidIndex] = k
		newGidIndex++
	}
	sort.Ints(newGid)
	// shards 包含0,把含有0的index填充为newGroups的Gid
	if len(newGroups) > 0 { // join
		zeroIndex := 0
		for i := 0; i < 10; i++ {
			if shards[i] == 0 {
				shards[i] = newGid[zeroIndex]
				if zeroIndex < len(newGid)-1 {
					zeroIndex++
				} else if zeroIndex == len(newGid)-1 {
					zeroIndex = 0
				}
			}
		}
	} else { // leave
		// 全为0且newGroups为0则返回
		var tempSum int
		for i := 0; i < NShards; i++ {
			tempSum += shards[i]
		}
		if tempSum == 0 {
			return shards
		}
		maxPair, minPair := shardGidNoZeroCnt(shards)
		for shardsContainInt(shards, 0) || maxPair.GidCnt-minPair.GidCnt > 1 {
			for i := 0; i < 10; i++ {
				if shards[i] == 0 {
					shards[i] = minPair.Gid
					maxPair, minPair = shardGidNoZeroCnt(shards)
					i = 10
				}
			}
		}
	}
	maxPair, minPair := shardGidCnt(shards)
	for _, v := range newGid {
		if !shardsContainInt(shards, v) {
			minPair.Gid = v
		 	minPair.GidCnt = 0
		}
		for maxPair.GidCnt-minPair.GidCnt > 1 {
			for i := 0; i < 10; i++ {
				if shards[i] == maxPair.Gid {
					shards[i] = minPair.Gid
					maxPair, minPair = shardGidCnt(shards)
					i = 10
				}
			}
		}
	}
	DPrintf("reBalance end shards:%v newGroups:%v", shards, newGroups)
	return shards
}

4B
实验任务
修改 shardkv/client.go、 shardkv/common.go 和 shardkv/server.go来构建构建容错的kv存储系统
提示
- 改写 server.go, 定时100ms从shardctrler get 最新配置,拒绝client发过来的key不是当前server负责的shard请求.
- 您的服务器应该用一个 ErrWrongGroup 错误来响应客户端 RPC,
 其中包含一个服务器不负责的密钥(例如,一个没有分配给服务器组的碎片的密钥).
 确保 Get、 Put 和 Append 处理程序在面对并发重新配置时正确地做出这个决定。
- 按顺序一次处理一个重配置
- 如果测试失败,检查 gob 错误(例如“ gob: type not register for interface...”)。Go 并不认为采空区错误是致命的,尽管它们对实验室来说是致命的。
- 同之前一样client的请求也要做重复检测,而且是跨分区的
- 考虑 shardkv 客户机和服务器应该如何处理 ErrWrongGroup。如果客户端接收到 ErrWrongGroup,是否应该更改序列号?如果服务器在执行 Get/Put 请求时返回 ErrWrongGroup,它是否应该更新客户端状态?
- 在服务器迁移到新配置之后,可以接受它继续存储不再拥有的碎片(尽管在实际系统中这会令人遗憾)。这可能有助于简化服务器实现。
- 当组 G1在配置更改期间需要来自 G2的碎片时,在处理日志条目时,G2在什么时候将碎片发送给 G1是否重要?
- 您可以在 RPC 请求或应答中发送整个映射,这可能有助于保持碎片传输代码的简单性。
- 如果您的一个 RPC 处理程序在其回复中包含一个映射(例如,键/值映射) ,这是服务器状态的一部分,您可能会因为竞争而得到 bug。RPC 系统必须读取映射才能将其发送给调用者,但是它没有持有覆盖映射的锁。但是,当 RPC 系统正在读取该映射时,您的服务器可能会继续修改该映射。解决方案是让 RPC 处理程序在应答中包含映射的副本。
- 如果您在 Raft 日志条目中放入了 map 或者 slice,那么您的 key/value 服务器随后会看到 applicyCh 上的条目,并在 key/value 服务器的状态下保存对 map/slice 的引用,那么您可能会遇到竞争。制作 map/slice 的副本,并将副本存储在键/值服务器的状态中。这场竞赛是在键/值服务器修改映射/切片和 Raft 在持久化其日志时读取它之间进行的。
- 在配置更改期间,一对组可能需要在两个方向之间移动碎片。如果你看到死锁,这是一个可能的原因
STEP 1
把LAB3的代码整体复制过来就可以通过第一个测试了
STEP 2
实现shardkv server定时向shardctrler定期请求最新配置,在server接受到client的请求和收到apply后都要检测key是否属于当前group
// 定期请求最新config
func (kv *ShardKV) detectConfiguration() {
	for {
		kv.latestConfig = kv.mck.Query(-1)
		time.Sleep(100 * time.Millisecond)
	}
}
// 检测发来的key是否属于当前group
func (kv *ShardKV) detectionKeyBelongCurrentGroup(key string) bool {
	shard := key2shard(key)
	return kv.latestConfig.Shards[shard] == kv.gid
}
STEP 3
放弃了,参考这个大佬的代码做的.
https://github.com/Sorosliu1029/6.824
实验总结
小半年,终于快把6.824跟完了,这是最后一个lab,也是唯一一个没有完成的Lab,参考了github上大佬的代码,醍醐灌顶...
整个实验跟下来,coding能力增长不少, 对于raft协议深入脑海,多线程的debug能力也增长不少.
下一步,CMU 15-445.
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号