【MIT 6.824 Lab4】——Sharded Key/Value Service

Lab4 Sharded Key/Value Service


PartA

本部分实验需要完成分配服务器和分片之间的控制器

  • 实验流程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnDtRDNF-1654833524682)(MIT-6.824.assets/image-20220331163207643.png)]

  • 对象设计

    主要的对象为服务器ShardCtrler,指令Op,客户端Clerk

    • ShardCtrler

      服务器需要记录自己的编号和对应的Raft服务,以及相应的ApplyCh用于Raft服务器应用,也可以通知自身日志已经存储完成。存储所有的config的信息,并且设置通道用于通知服务器对应的指令是否已经在Raft处存储完成,为了防止不同的客户端请求不重复,设置lastRequestId记录每个客户端上次请求的ID。

      type ShardCtrler struct {
      	mu      sync.Mutex
      	me      int
      	rf      *raft.Raft
      	applyCh chan raft.ApplyMsg
      
      	// Your data here.
      	configs       []Config // indexed by config num
      	waitApplyCh   map[int]chan Op
      	lastRequestId map[int64]int
      }
      
    • Op

      指令需要记录类型,并且记录请求的客户端的id和请求的id,确保请求不重复。并且设置不同的值用于存储不同请求的参数。

      type Op struct {
      	// Your data here.
      	OperationType string //操作类型,put.get,append
      	ClientId      int64
      	RequestId     int
      	QueryNum      int
      	JoinServers   map[int][]string
      	LeaveGids     []int
      	MoveShard     int
      	MoveGid       int
      }
      
    • Clerk

      客户端有自己的编号,新的请求的编号防止重复请求,并存储所有的服务器和可能时leader的服务器id

      type Clerk struct {
      	servers []*labrpc.ClientEnd
      	// Your data here.
      	clientId       int64
      	requestId      int
      	recentLeaderId int
      }
      
  • 流程设计

    • 服务器启动

      初始化参数,后台启动线程持续读取已经被Raft提交应用的命令

      func StartServer(servers []*labrpc.ClientEnd, me int, persister *raft.Persister) *ShardCtrler {
      	sc := new(ShardCtrler)
      	sc.me = me
      
      	sc.configs = make([]Config, 1)
      	sc.configs[0].Groups = map[int][]string{}
      
      	labgob.Register(Op{})
      	sc.applyCh = make(chan raft.ApplyMsg)
      	sc.rf = raft.Make(servers, me, persister, sc.applyCh)
      
      	// Your code here.
      	sc.waitApplyCh = make(map[int]chan Op)
      	sc.lastRequestId = make(map[int64]int)
      
      	go sc.ReadRaftApplyCommand()
      	return sc
      }
      
    • 读取已经应用的命令

      根据已经应用的程序进行分类处理,然后把信息发送给WaitChan通知处理完成

      func (sc *ShardCtrler) GetCommand(msg raft.ApplyMsg) {
      	op := msg.Command.(Op)
      
      	if !sc.ifRequestRepetition(op.ClientId, op.RequestId) {
      		if op.OperationType == "Join" {
      			sc.ExecuteJoinOnConfig(op)
      		}
      		if op.OperationType == "Leave" {
      			sc.ExecuteLeaveOnConfig(op)
      		}
      		if op.OperationType == "Move" {
      			sc.ExecuteMoveOnConfig(op)
      		}
      	}
      
      	sc.SendMessageToWaitChan(op, msg.CommandIndex)
      }
      
    • 通知操作完成

      func (sc *ShardCtrler) SendMessageToWaitChan(op Op, index int) {
      	//修改之前需要上锁
      	sc.mu.Lock()
      	defer sc.mu.Unlock()
      	//检查waitChan是否已经初始化对应位置的值
      	ch, exist := sc.waitApplyCh[index]
      	if exist {
      		ch <- op
      	}
      }
      
    • 启动客户端

      func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {
      	ck := new(Clerk)
      	ck.servers = servers
      	// Your code here.
      	ck.clientId = nrand()
      	ck.recentLeaderId = mathrand.Intn(len(servers))
      	return ck
      }
      
    • 客户端发起Query请求

      设置需要查看的config的Id,发起rpc请求

      func (ck *Clerk) Query(num int) Config {
      
      	// Your code here.
      	ck.requestId++
      	sev := ck.recentLeaderId
      	for {
      		args := QueryArgs{
      			Num:       num,
      			ClientId:  ck.clientId,
      			RequestId: ck.requestId,
      		}
      		reply := QueryReply{}
      		ok := ck.servers[sev].Call("ShardCtrler.Query", &args, &reply)
      
    • 服务器接收请求

      需要自身连接的Raft服务器是leader才能返回请求。将请求信息填入操作中,发给Raft存储,等待指令的完成。如果等待超时,则检查是不是已经提交的指令,若是则直接执行Query操作,否则返回WrongLeader的错误。如果从WaitChan中收到信息,则需检查是否与请求信息一致,如是则执行Query,不是则返回错误。

      func (sc *ShardCtrler) Query(args *QueryArgs, reply *QueryReply) {
      	// Your code here.
      	_, ifLeader := sc.rf.GetState()
      	if !ifLeader {
      		reply.WrongLeader = true
      		return
      	}
      	op := Op{
      		OperationType: "Query",
      		ClientId:      args.ClientId,
      		RequestId:     args.RequestId,
      		QueryNum:      args.Num,
      	}
      	rfIndex, _, _ := sc.rf.Start(op)
      	sc.mu.Lock()
      	chForWaitCh, exist := sc.waitApplyCh[rfIndex]
      	if !exist {
      		sc.waitApplyCh[rfIndex] = make(chan Op, 1)
      		chForWaitCh = sc.waitApplyCh[rfIndex]
      	}
      	sc.mu.Unlock()
      	select {
      	case <-time.After(time.Millisecond * RfTimeOut):
      		if sc.ifRequestRepetition(op.ClientId, op.RequestId) {
      			reply.Config = sc.ExecuteQueryOnConfig(op)
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	case rfCommitOp := <-chForWaitCh:
      		if rfCommitOp.ClientId == op.ClientId && rfCommitOp.RequestId == op.RequestId {
      			reply.Config = sc.ExecuteQueryOnConfig(op)
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	}
      	sc.mu.Lock()
      	delete(sc.waitApplyCh, rfIndex)
      	sc.mu.Unlock()
      	return
      }
      
    • 服务器执行Query操作

      检查Query的参数若为-1或者大于自身config的最大值,则返回最新的config的信息。否则直接返回请求的config。

      func (sc *ShardCtrler) ExecuteQueryOnConfig(op Op) Config {
      	sc.mu.Lock()
      	defer sc.mu.Unlock()
      	sc.lastRequestId[op.ClientId] = op.RequestId
      	if op.QueryNum == -1 || op.QueryNum >= len(sc.configs) {
      		DPrintf("Server:%d,Query Config:%v", sc.me, sc.configs[len(sc.configs)-1])
      		return sc.configs[len(sc.configs)-1]
      	} else {
      		return sc.configs[op.QueryNum]
      	}
      }
      
    • 客户端收到Query回复

      如果错误则重新选择一个服务器发起请求,若正确则返回结果

      		if !ok || reply.WrongLeader == true {
      			sev = (sev + 1) % len(ck.servers)
      			continue
      		}
      		if reply.Err == OK {
      			ck.recentLeaderId = sev
      			return reply.Config
      		}
      		time.Sleep(100 * time.Millisecond)
      
    • 客户端发起Join请求

      在请求的参数中设置希望新增的服务器,初始化相关的参数

      ck.requestId++
      	sev := ck.recentLeaderId
      	// Your code here.
      	for {
      		args := JoinArgs{
      			Servers:   servers,
      			ClientId:  ck.clientId,
      			RequestId: ck.requestId,
      		}
      		reply := JoinReply{}
      		ok := ck.servers[sev].Call("ShardCtrler.Join", &args, &reply)
      
    • 服务器接收Join请求

      利用Join请求的信息生产新的指令Op发送给Raft服务器,等待Raft存储提交。

      func (sc *ShardCtrler) Join(args *JoinArgs, reply *JoinReply) {
      	// Your code here.
      	_, ifLeader := sc.rf.GetState()
      	if !ifLeader {
      		reply.WrongLeader = true
      		return
      	}
      	op := Op{
      		OperationType: "Join",
      		ClientId:      args.ClientId,
      		RequestId:     args.RequestId,
      		JoinServers:   args.Servers,
      	}
      	rfIndex, _, _ := sc.rf.Start(op)
      	sc.mu.Lock()
      	chForWaitCh, exist := sc.waitApplyCh[rfIndex]
      	if !exist {
      		sc.waitApplyCh[rfIndex] = make(chan Op, 1)
      		chForWaitCh = sc.waitApplyCh[rfIndex]
      	}
      	sc.mu.Unlock()
      
    • 服务器执行Join操作

      服务器接收到应用的Join操作后,更新对应client的RequestId,生成新的config。

      func (sc *ShardCtrler) ExecuteJoinOnConfig(op Op) {
      	sc.mu.Lock()
      	sc.lastRequestId[op.ClientId] = op.RequestId
      	sc.configs = append(sc.configs, sc.MakeJoinConfig(op.JoinServers))
      	DPrintf("[ExecuteJoinOnConfig Success],Server:%d,ClientId:%d,RequestId:%d", sc.me, op.ClientId, op.RequestId)
      	sc.mu.Unlock()
      	sc.DprintfConfig()
      }
      
    • 服务器生成新的Join后的config

      服务器首先读取旧的config信息,然后将新增的服务器的gid信息加入以前的gid信息,生成新的group。然后读取旧的config中的gid对应拥有的shard的数量的信息,作为平衡新增后gid和shard数量函数的输入。然后对shard和gid进行平衡,得到新的config

      func (sc *ShardCtrler) MakeJoinConfig(servers map[int][]string) Config {
      	//函数外面已经加锁,所以不需要再加锁
      	//获取以前的config信息
      	oldConfig := sc.configs[len(sc.configs)-1]
      	//利用新加的服务器和以前的group信息生成新的Group
      	newConfigGroup := make(map[int][]string)
      	//读取旧的Group
      	for gid, server := range oldConfig.Groups {
      		newConfigGroup[gid] = server
      	}
      	//读取新的Group
      	for gid, server := range servers {
      		newConfigGroup[gid] = server
      	}
      	//读取现有的shard与group的关系
      	gidsToShards := make(map[int]int) //shard->groupNum
      	//全部初始化为0
      	for gid := range newConfigGroup {
      		gidsToShards[gid] = 0
      	}
      	//读取旧的shard信息
      	for _, gid := range oldConfig.Shards {
      		//注意可能删除了部分shards或者刚刚初始化都为0
      		if gid != 0 {
      			gidsToShards[gid] += 1
      		}
      	}
      	//生产新的config,重新均匀分配shard和group的关系
      	return Config{
      		Num:    len(sc.configs),
      		Shards: sc.reBalanceShards(gidsToShards, oldConfig.Shards),
      		Groups: newConfigGroup,
      	}
      }
      
    • 平衡gid和shard

      首先根据新的config中的服务器数量确定平均每个服务器应分得的平均shard数,然后再算出余下来的分片。然后对所有的group按照所拥有的shard从小到大的方式(如果相同则gid小的在前面)进行排序,确保不同服务器在平衡后能够得到同样的结果,并且从后往前遍历可以先遍历到具有多的shard的group,可以先退再补。然后从后往前遍历,首先计算应该分得的分片数,为了减少迁移数,新产生的config应该也是从小到大,所以排在后面remainder个服务器可以分得average+1的分片。然后对比实际分得的分片,如果多了,则从旧的shard中遍历将前面的该group分得的shard退出(设为0)。如果少了,则从前往后遍历,将遇到的第一个没有分配的shard分给他。因为后面的分得分片更多,所以需要先从后面遍历分出多于的分片留给新加入的组。

      func (sc *ShardCtrler) reBalanceShards(gidsToShards map[int]int, oldShards [NShards]int) [NShards]int {
      	groupNum := len(gidsToShards)   //group个数
      	average := NShards / groupNum   //每个group的shard的平均个数
      	remainder := NShards % groupNum //余数
      	DPrintf("")
      	//排序后确保从后往前遍历时,可以先遍历到拥有多的分片的gid让其先退分片
      	//被退出来的分片则可以用于分配给排序在前面的新加入的分组
      	sortGids := SortGids(gidsToShards)
      	DPrintf("-----sortres:%v", sortGids)
      	//修改以前的shards
      	//从后往前遍历,确保先看到有多的从而能先退给补留出空闲的
      	for i := groupNum - 1; i >= 0; i-- {
      		//应该分得的分片数
      		resNum := average
      		//把余数分给后面的
      		if !ifAvg(groupNum, remainder, i) {
      			resNum += 1
      		}
      		//多退
      		if resNum < gidsToShards[sortGids[i]] {
      			fromGid := sortGids[i]
      			changNum := gidsToShards[sortGids[i]] - resNum
      			for shard, gid := range oldShards {
      				if changNum <= 0 {
      					break
      				}
      				if gid == fromGid {
      					oldShards[shard] = 0
      					changNum -= 1
      				}
      			}
      			//gidsToShards[sortGids[i]]=resNum,因为按照顺序只遍历了一次,所以不用更新已经遍历过的旧的映射关系
      		}
      		//少补(新加入的group)
      		if resNum > gidsToShards[sortGids[i]] {
      			toGid := sortGids[i]
      			changNum := resNum - gidsToShards[sortGids[i]]
      			for shard, gid := range oldShards {
      				if changNum <= 0 {
      					break
      				}
      				if gid == 0 {
      					oldShards[shard] = toGid
      					changNum -= 1
      				}
      			}
      		}
      	}
      	DPrintf("reBalanceShards result:%v", oldShards)
      	return oldShards
      }
      func SortGids(gidsToShards map[int]int) []int {
      	length := len(gidsToShards)
      
      	res := make([]int, 0, length)
      	for gid, _ := range gidsToShards {
      		res = append(res, gid)
      	}
      	//冒泡排序,保证按照分得的shard数量从小到大排序
      	for i := 0; i < length-1; i++ {
      		for j := length - 1; j > i; j-- {
      			if gidsToShards[res[j]] < gidsToShards[res[j-1]] || gidsToShards[res[j]] == gidsToShards[res[j-1]] && res[j] < res[j-1] {
      				res[j], res[j-1] = res[j-1], res[j]
      			}
      		}
      	}
      	return res
      }
      
    • 服务器返回Join请求

      	select {
      	case <-time.After(time.Millisecond * RfTimeOut):
      		if sc.ifRequestRepetition(op.ClientId, op.RequestId) {
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	case rfCommitOp := <-chForWaitCh:
      		if rfCommitOp.ClientId == op.ClientId && rfCommitOp.RequestId == op.RequestId {
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	}
      	sc.mu.Lock()
      	delete(sc.waitApplyCh, rfIndex)
      	sc.mu.Unlock()
      	return
      
    • 客户端收到Join的回复

      		if !ok || reply.WrongLeader == true {
      			sev = (sev + 1) % len(ck.servers)
      			continue
      		}
      		if reply.Err == OK {
      			ck.recentLeaderId = sev
      			return
      		}
      		time.Sleep(100 * time.Millisecond)
      	}
      
    • 客户端发起Leave的请求

      将需要删除的gids放在请求的参数中

      ck.requestId++
      	sev := ck.recentLeaderId
      	// Your code here.
      	for {
      		args := LeaveArgs{
      			GIDs:      gids,
      			ClientId:  ck.clientId,
      			RequestId: ck.requestId,
      		}
      		reply := JoinReply{}
      		ok := ck.servers[sev].Call("ShardCtrler.Leave", &args, &reply)
      
    • 服务器接收Leave请求

      // Your code here.
      	_, ifLeader := sc.rf.GetState()
      	if !ifLeader {
      		reply.WrongLeader = true
      		return
      	}
      	op := Op{
      		OperationType: "Leave",
      		ClientId:      args.ClientId,
      		RequestId:     args.RequestId,
      		LeaveGids:     args.GIDs,
      	}
      	rfIndex, _, _ := sc.rf.Start(op)
      	sc.mu.Lock()
      	chForWaitCh, exist := sc.waitApplyCh[rfIndex]
      	if !exist {
      		sc.waitApplyCh[rfIndex] = make(chan Op, 1)
      		chForWaitCh = sc.waitApplyCh[rfIndex]
      	}
      	sc.mu.Unlock()
      
    • 服务器执行Leave操作

      func (sc *ShardCtrler) ExecuteLeaveOnConfig(op Op) {
      	sc.mu.Lock()
      	sc.lastRequestId[op.ClientId] = op.RequestId
      	sc.configs = append(sc.configs, sc.MakeLeaveConfig(op.LeaveGids))
      	DPrintf("[ExecuteLeaveOnConfig Success],Server:%d,ClientId:%d,RequestId:%d", sc.me, op.ClientId, op.RequestId)
      	sc.mu.Unlock()
      	sc.DprintfConfig()
      }
      
    • 服务器生产Leave产生的新的config

      服务器从旧的config的group中删去请求中包含的gid,然后生成gid和shard的对应关系,读取就config中非需要删除的gid所拥有的shard的数量,然后进行shard平衡,生成新的config

      func (sc *ShardCtrler) MakeLeaveConfig(gids []int) Config {
      	//函数外面已经加锁,所以不需要再加锁
      	//获取以前的config信息
      	oldConfig := sc.configs[len(sc.configs)-1]
      	//利用新加的服务器和以前的group信息生成新的Group
      	newConfigGroup := make(map[int][]string)
      	//标注需删除的gid
      	needleave := make(map[int]bool)
      	for _, gid := range gids {
      		needleave[gid] = true
      	}
      	//读取旧的Group
      	for gid, server := range oldConfig.Groups {
      		newConfigGroup[gid] = server
      	}
      	//生成新的group
      	for _, gid := range gids {
      		delete(newConfigGroup, gid)
      	}
      	//读取现有的shard与group的关系
      	gidsToShards := make(map[int]int) //shard->groupNum
      	//全部初始化为0
      	for gid := range newConfigGroup {
      		gidsToShards[gid] = 0
      	}
      	//读取旧的shard信息
      	for shard, gid := range oldConfig.Shards {
      		//注意为0则表示没有对应的gid,即没有对应关系
      		if gid != 0 {
      			if needleave[gid] {
      				oldConfig.Shards[shard] = 0
      			} else {
      				gidsToShards[gid] += 1
      			}
      		}
      	}
      	//生产新的config,重新均匀分配shard和group的关系
      	//注意可能所有group都走了,则无法执行重新分配
      	if len(newConfigGroup) == 0 {
      		return Config{
      			Num:    len(sc.configs),
      			Shards: [10]int{},
      			Groups: newConfigGroup,
      		}
      	}
      	return Config{
      		Num:    len(sc.configs),
      		Shards: sc.reBalanceShards(gidsToShards, oldConfig.Shards),
      		Groups: newConfigGroup,
      	}
      }    
      
    • 服务器返回Leave请求

      select {
      	case <-time.After(time.Millisecond * RfTimeOut):
      		if sc.ifRequestRepetition(op.ClientId, op.RequestId) {
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	case rfCommitOp := <-chForWaitCh:
      		if rfCommitOp.ClientId == op.ClientId && rfCommitOp.RequestId == op.RequestId {
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	}
      	sc.mu.Lock()
      	delete(sc.waitApplyCh, rfIndex)
      	sc.mu.Unlock()
      	return
      }
      
    • 客户端接收Leave请求

      		if !ok || reply.WrongLeader == true {
      			sev = (sev + 1) % len(ck.servers)
      			continue
      		}
      		if reply.Err == OK {
      			ck.recentLeaderId = sev
      			return
      		}
      		time.Sleep(100 * time.Millisecond)
      	}
      
    • 客户端发起Move请求

      ck.requestId++
      	sev := ck.recentLeaderId
      	// Your code here.
      	for {
      		args := MoveArgs{
      			Shard:     shard,
      			GID:       gid,
      			ClientId:  ck.clientId,
      			RequestId: ck.requestId,
      		}
      		reply := JoinReply{}
      		ok := ck.servers[sev].Call("ShardCtrler.Move", &args, &reply)
      
    • 服务器接收Move请求

      	// Your code here.
      	_, ifLeader := sc.rf.GetState()
      	if !ifLeader {
      		reply.WrongLeader = true
      		return
      	}
      	op := Op{
      		OperationType: "Move",
      		ClientId:      args.ClientId,
      		RequestId:     args.RequestId,
      		MoveGid:       args.GID,
      		MoveShard:     args.Shard,
      	}
      	rfIndex, _, _ := sc.rf.Start(op)
      	sc.mu.Lock()
      	chForWaitCh, exist := sc.waitApplyCh[rfIndex]
      	if !exist {
      		sc.waitApplyCh[rfIndex] = make(chan Op, 1)
      		chForWaitCh = sc.waitApplyCh[rfIndex]
      	}
      	sc.mu.Unlock()
      
    • 服务器执行Move操作

      func (sc *ShardCtrler) ExecuteMoveOnConfig(op Op) {
      	sc.mu.Lock()
      	sc.lastRequestId[op.ClientId] = op.RequestId
      	sc.configs = append(sc.configs, sc.MakeMoveConfig(op.MoveGid, op.MoveShard))
      	DPrintf("[ExecuteMoveOnConfig Success],Server:%d,ClientId:%d,RequestId:%d", sc.me, op.ClientId, op.RequestId)
      	sc.mu.Unlock()
      	sc.DprintfConfig()
      
      }
      
    • 服务器生成新的Move后的config

      将旧的config中的对应的shard的gid设置为请求的值

      func (sc *ShardCtrler) MakeMoveConfig(movegid, moveshard int) Config {
      	oldConfig := sc.configs[len(sc.configs)-1]
      	oldConfig.Num += 1
      	oldConfig.Shards[moveshard] = movegid
      	return oldConfig
      }
      
    • 服务器返回Move请求

      select {
      	case <-time.After(time.Millisecond * RfTimeOut):
      		if sc.ifRequestRepetition(op.ClientId, op.RequestId) {
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	case rfCommitOp := <-chForWaitCh:
      		if rfCommitOp.ClientId == op.ClientId && rfCommitOp.RequestId == op.RequestId {
      			reply.Err = OK
      		} else {
      			reply.WrongLeader = true
      		}
      	}
      	sc.mu.Lock()
      	delete(sc.waitApplyCh, rfIndex)
      	sc.mu.Unlock()
      	return
      
    • 客户端收到Move回复

      if !ok || reply.WrongLeader == true {
      			sev = (sev + 1) % len(ck.servers)
      			continue
      		}
      		if reply.Err == OK {
      			ck.recentLeaderId = sev
      			return
      		}
      		time.Sleep(100 * time.Millisecond)
      	}
      
PartB

本部分需要基于PartA完成分片K-V数据库

  • 实验流程
    在这里插入图片描述

  • 对象设计

    本实验的主要对象有ShardKV,Op,Clerk

    • ShardKV

      ShardKV代表分片服务器,类似于普通的K-V服务器,需要自己的编号,连接的Raft对象,应用通道,等待通知的通道这些基本信息,还需要存储一个用于访问最新的配置新的客户端,以及设置一个数组用于对正在迁入迁出的数据进行上锁。此外由于是分片数据库,所以在存储数据时需要重新建立一个对象,除了分片的数据,还需要存储该分片对应的编号,以及对该编号请求的各个客户端的最新的请求ID,用于防止重复请求。

      type ShardKV struct {
      	mu           sync.Mutex
      	me           int
      	rf           *raft.Raft
      	applyCh      chan raft.ApplyMsg
      	make_end     func(string) *labrpc.ClientEnd
      	gid          int
      	ctrlers      []*labrpc.ClientEnd
      	maxraftstate int // snapshot if log grows this big
      
      	// Your definitions here.
      	mck               *shardctrler.Clerk //询问最新的配置
      	data              []ShardComponent
      	waitApplyCh       map[int]chan Op    //等待raft应用后通知给server
      	LastIncludedIndex int                //snapshot最新下标
      	config            shardctrler.Config //存储读取的最新的config
      	migratingShard    [Nshards]bool      //更替数据时对相应的shard进行上锁
      }
      
      type ShardComponent struct {
      	ShardIndex    int               //分片下标
      	ShardData     map[string]string //改分片的数据
      	LastRequestId map[int64]int     //请求该分片的客户端信息,防止重复请求
      }
      
    • Op

      Op代表服务器向Raft发送的指令,与普通K-V数据库不同在于多了几个新的变量,一是NewConfig存储更新的配置信息对应新的操作类型,以及MigrateData和MigrateConfigNum用来存储迁移需要修改的数据和config编号。

      type Op struct {
      	// Your definitions here.
      	// Field names must start with capital letters,
      	// otherwise RPC will break.
      	OperationType    string //操作类型,put.get,append
      	Key              string
      	Value            string
      	ClientId         int64
      	RequestId        int
      	NewConfig        shardctrler.Config //新的config信息
      	MigrateData      []ShardComponent   //需要更新的数据
      	MigrateConfigNum int                //更新的配置编号
      }
      
    • Clerk

      代表客户端,与普通K-V数据库存储的对象一致。

      type Clerk struct {
      	sm       *shardctrler.Clerk
      	config   shardctrler.Config
      	make_end func(string) *labrpc.ClientEnd
      	// You will have to modify this struct.
      	clientId  int64
      	requestId int
      }
      
  • 流程设计

    • 服务器启动

      初始化服务器信息,启动三个后台进程,分别读取已提交的日志,获取最新的配置信息,以及检测是否需要发送分片信息进行数据迁移。

      func StartServer(servers []*labrpc.ClientEnd, me int, persister *raft.Persister, maxraftstate int, gid int, ctrlers []*labrpc.ClientEnd, make_end func(string) *labrpc.ClientEnd) *ShardKV {
      	// call labgob.Register on structures you want
      	// Go's RPC library to marshall/unmarshall.
      	labgob.Register(Op{})
      
      	kv := new(ShardKV)
      	kv.me = me
      	kv.maxraftstate = maxraftstate
      	kv.make_end = make_end
      	kv.gid = gid
      	kv.ctrlers = ctrlers
      	// Your initialization code here.
      	kv.data = make([]ShardComponent, Nshards)
      	for shard := 0; shard < Nshards; shard++ {
      		kv.data[shard] = ShardComponent{
      			ShardIndex:    shard,
      			ShardData:     make(map[string]string),
      			LastRequestId: make(map[int64]int),
      		}
      	}
      	kv.mck = shardctrler.MakeClerk(kv.ctrlers)
      	kv.waitApplyCh = make(map[int]chan Op)
      	// Use something like this to talk to the shardctrler:
      	// kv.mck = shardctrler.MakeClerk(kv.ctrlers)
      	snapshot := persister.ReadSnapshot()
      	if len(snapshot) > 0 {
      		kv.ReadSnapShotToApply(snapshot)
      	}
      	kv.applyCh = make(chan raft.ApplyMsg)
      	kv.rf = raft.Make(servers, me, persister, kv.applyCh)
      	go kv.ReadRaftApplyCommand()
      	go kv.GetNewConfig()
      	go kv.SendShardToOtherGroup()
      	return kv
      }
      
    • 服务器读取已应用的命令

      服务器首先根据已经应用的指令类型分类处理。如果是指令则进入执行命令的处理,不然则是快照命令。快照的执行处理方式于普通K-V数据库一致。

      func (kv *ShardKV) ReadRaftApplyCommand() {
      	for msg := range kv.applyCh {
      		if msg.CommandValid {
      			kv.GetCommand(msg)
      		}
      		if msg.SnapshotValid {
      			kv.GetSnapShot(msg)
      		}
      	}
      }
      
    • 服务器执行命令

      func (kv *ShardKV) GetCommand(msg raft.ApplyMsg) {
      	//类型转换
      	op := msg.Command.(Op) //断言
      	//DPrintf("[Command from raft],Server:%d,OpType:%v,OpClientId:%d,OpRequestId:%d,OpKey:%v,OpValue:%v,NewConfig:%v", kv.me, op.OperationType, op.ClientId, op.RequestId, op.Key, op.Value, op.NewConfig)
      	if msg.CommandIndex <= kv.LastIncludedIndex {
      		return
      	}
      	if op.OperationType == NEWCONFIG {
      		kv.ExecuteNewConfigOnServer(op)
      	}
      	if op.OperationType == MIGRATE {
      		kv.ExecuteMigrateOnServer(op)
      	}
      	if !kv.ifRequestRepetition(op.ClientId, op.RequestId, key2shard(op.Key)) {
      		if op.OperationType == PUT {
      			kv.ExecutePutOnServer(op)
      		}
      		if op.OperationType == APPEND {
      			kv.ExecuteAppendOnServer(op)
      		}
      	}
      	//发送消息给WaitChan通知server可以返回结果
      	if kv.maxraftstate != -1 {
      		kv.CheckForSnapShot(msg.CommandIndex, numerator, denominator)
      	}
      	kv.SendMessageToWaitChan(op, msg.CommandIndex)
      }
      
    • 服务器执行快照

      执行方式于普通K-V数据库一致,不同在与需要存储的数据有data,配置信息config,已经迁移时的锁的数组信息migratingShard。

      func (kv *ShardKV) MakeSnapShot() []byte {
      	kv.mu.Lock()
      	defer kv.mu.Unlock()
      	w := new(bytes.Buffer)
      	e := labgob.NewEncoder(w)
      	e.Encode(kv.data)
      	e.Encode(kv.config)
      	e.Encode(kv.migratingShard)
      	return w.Bytes()
      }
      
    • 读取新的Config

      获取当前的config信息,然后通过shardCtrler的client查找是否有自己的configNum+1的配置信息,如果没有则休眠一段时间重新请求,否则由leader发送含有newconfig 的指令给raft服务器。

      func (kv *ShardKV) GetNewConfig() {
      	for {
      		kv.mu.Lock()
      		//获取当前配置的信息
      		oldConfigNum := kv.config.Num
      		_, ifLeader := kv.rf.GetState()
      		kv.mu.Unlock()
      		//由leader发起更新新配置的信息,因此不是leader不能处理
      		if !ifLeader {
      			time.Sleep(CONFIGCHECK_TIMEOUT * time.Millisecond)
      			continue
      		}
      		//查询是否有当前配置的下一个的新配置消息
      		newConfig := kv.mck.Query(oldConfigNum + 1)
      		//如果有则给raft存储
      		if newConfig.Num == oldConfigNum+1 {
      			op := Op{
      				OperationType: NEWCONFIG,
      				NewConfig:     newConfig,
      			}
      			kv.mu.Lock()
      			if _, ifLeader := kv.rf.GetState(); ifLeader {
      				kv.rf.Start(op)
      			}
      			kv.mu.Unlock()
      		}
      		time.Sleep(CONFIGCHECK_TIMEOUT * time.Millisecond)
      	}
      
    • 服务器执行更新新的配置信息

      服务器首先检查指令中的config的编号是不是自己当前编号的下一个,以及检查是不是正在进行数据的迁移,如果不满足条件则不执行,等待下一次的请求。不然则锁住需要迁移的数据,并且更新config的信息。

      func (kv *ShardKV) ExecuteNewConfigOnServer(op Op) {
      	kv.mu.Lock()
      	defer kv.mu.Unlock()
      	//检查新的config的是不是为现有的config的num+1
      	newConfig := op.NewConfig
      	if newConfig.Num != kv.config.Num+1 {
      		return
      	}
      	//检测是不是正在进行迁移,如果是则不能执行
      	for shard := 0; shard < Nshards; shard++ {
      		if kv.migratingShard[shard] {
      			return
      		}
      	}
      	//锁住需要迁移的分片
      	kv.lockMigratingShard(newConfig.Shards)
      	//修改为新的config
      	kv.config = newConfig
      	//DPrintf("[Update NewConfig]:Server:%d,Config:%v", kv.me, kv.config)
      }
      
    • 锁住需要迁移的数据

      如果旧的config中的shard对应的gid是自己而新的不是,则说明需要发送自己的信息。同理如果旧的config中的shard对应的gid不是自己而新的是,则需要接受数据。这两种都需要锁定对应的分片的数据信息,等待迁移完成后解锁。

      func (kv *ShardKV) lockMigratingShard(newShard [Nshards]int) {
      	oldShards := kv.config.Shards
      	//检测新的config是否需要迁移或接受新的shard
      	for shard := 0; shard < Nshards; shard++ {
      		//需要发送自己的信息
      		if oldShards[shard] == kv.gid && newShard[shard] != kv.gid {
      			//如果是直接删除则不用发送信息
      			if newShard[shard] != 0 {
      				kv.migratingShard[shard] = true
      			}
      		}
      		//需要接受新的信息
      		if oldShards[shard] != kv.gid && newShard[shard] == kv.gid {
      			//如果是原本就没有新增的则无需等待输入
      			if oldShards[shard] != 0 {
      				kv.migratingShard[shard] = true
      			}
      		}
      	}
      }
      
    • 分片数据迁移

      通过检查Migrate上锁的数组,发现需不需要迁移数据,如果需要则再检查是否需要发送数据,并生成对应需要发送数据对象。如果不是则直接不用处理。

      func (kv *ShardKV) SendShardToOtherGroup() {
      	//
      	for {
      		kv.mu.Lock()
      		_, ifLeader := kv.rf.GetState()
      		kv.mu.Unlock()
      		if !ifLeader {
      			time.Sleep(SENDSHARDS_TIMEOUT * time.Millisecond)
      			continue
      		}
      		//检查需不需要迁移数据(接受或者发送)
      		migrateFlag := true
      		kv.mu.Lock()
      		for shard := 0; shard < Nshards; shard++ {
      			if kv.migratingShard[shard] {
      				migrateFlag = false
      			}
      		}
      		kv.mu.Unlock()
      		//如不需要则休眠一段时间再重新调用
      		if migrateFlag {
      			time.Sleep(SENDSHARDS_TIMEOUT * time.Millisecond)
      			continue
      		}
      		//检查需不需要发送数据
      		ifNeedSend, sendData := kv.ifHaveSendData()
      		//不需要发送则直接休息并重新调用
      		if !ifNeedSend {
      			time.Sleep(SENDSHARDS_TIMEOUT * time.Millisecond)
      			continue
      		}
      		//发送数据
      		kv.sendShardComponent(sendData)
      		time.Sleep(SENDSHARDS_TIMEOUT * time.Millisecond)
      	}
      }
      
    • 生成发送的数据对象

      通过检测上锁的数据,以及新的config的shard对应的gid如果不等于自己,则说明需要发送该数据给对方。

      func (kv *ShardKV) ifHaveSendData() (bool, map[int][]ShardComponent) {
      	sendData := kv.MakeSendShardComponent()
      	if len(sendData) == 0 {
      		return false, make(map[int][]ShardComponent)
      	}
      	return true, sendData
      }
      func (kv *ShardKV) MakeSendShardComponent() map[int][]ShardComponent {
      	kv.mu.Lock()
      	defer kv.mu.Unlock()
      	sendData := make(map[int][]ShardComponent)
      	for shard := 0; shard < Nshards; shard++ {
      		nowOwner := kv.config.Shards[shard]
      		//如果上了锁且新的config中该shard对应的不是自己的gid,则说明需要发送
      		if kv.migratingShard[shard] && kv.gid != nowOwner {
      			tempComponent := ShardComponent{
      				ShardIndex:    shard,
      				ShardData:     make(map[string]string),
      				LastRequestId: make(map[int64]int),
      			}
      			CloneComponentData(&tempComponent, kv.data[shard])
      			sendData[nowOwner] = append(sendData[nowOwner], tempComponent)
      		}
      	}
      	return sendData
      }
      
    • 发送分片数据的Rpc请求

      将生成的需要发送的数据生产对应的RPC请求的参数,发送给所有需要接受分片信息的服务器。如果返回Ok,则发送包含新的Migrate信息的指令给Raft更新分片信息。

      func (kv *ShardKV) sendShardComponent(sendData map[int][]ShardComponent) {
      	for gid, shardComponent := range sendData {
      		kv.mu.Lock()
      		args := &MigrateShardArgs{
      			ConfigNum:   kv.config.Num,
      			MigrateData: shardComponent,
      		}
      		groupServers := kv.config.Groups[gid]
      		kv.mu.Unlock()
      		go kv.callMigrateRPC(groupServers, args)
      	}
      }
      func (kv *ShardKV) callMigrateRPC(groupServers []string, args *MigrateShardArgs) {
      	for _, groupMember := range groupServers {
      		srv := kv.make_end(groupMember)
      		reply := MigrateShardReply{}
      		ok := srv.Call("ShardKV.MigrateShard", args, &reply)
      		if ok && reply.Err == OK {
      			//检查是不是已经修改了状态
      			if kv.checkMigrateState(args.MigrateData) {
      				return
      			} else {
      				kv.rf.Start(Op{
      					OperationType:    MIGRATE,
      					MigrateData:      args.MigrateData,
      					MigrateConfigNum: args.ConfigNum,
      				})
      			}
      		}
      		//not ok,则可能网络不通或者对面config不够新所以先不执行,等待下一次
      	}
      }
      
    • 接受需要修改分片数据

      接受到MIgrate的RPC请求后,首先需要检查请求中包含的配置的编号,如果大于自己的编号,则可能自己还没有更新到最新的,则直接返回给自己更新数据一段缓冲时间。如果小于自身,则可能已经执行成功但是返回的信息失败导致对方的信息没有更新,则直接返回OK。如果相等,首先检查自己是否还上着锁,如果没有则说明自己已经更新直接返回OK。不然则发送给Raft执行含有Migrate的指令信息。

      func (kv *ShardKV) MigrateShard(args *MigrateShardArgs, reply *MigrateShardReply) {
      	kv.mu.Lock()
      	myConfigNum := kv.config.Num
      	kv.mu.Unlock()
      	//自己这边没有更新到最新的config,则直接返回等待自己为最新的才执行
      	if args.ConfigNum > myConfigNum {
      		return
      	}
      	//第一次成功接收并且执行但回复失败,那边重新发送,则需要返回ok让那边同步
      	if args.ConfigNum < myConfigNum {
      		reply.Err = OK
      		return
      	}
      	//检测是不是自己已经修改了
      	if kv.checkMigrateState(args.MigrateData) {
      		reply.Err = OK
      		return
      	}
      	op := Op{
      		OperationType:    MIGRATE,
      		MigrateData:      args.MigrateData,
      		MigrateConfigNum: args.ConfigNum,
      	}
      	rfIndex, _, _ := kv.rf.Start(op)
      	//DPrintf("[[Get StartToRf],Server:%d,OpType:%v,OpClientId:%d,OpRequestId:%d,OpKey:%v,OpValue:%v", kv.me, op.OperationType, op.ClientId, op.RequestId, op.Key, op.Value)
      	//检查waitApplyCh是否存在
      	kv.mu.Lock()
      	chForWaitCh, exist := kv.waitApplyCh[rfIndex]
      	if !exist {
      		kv.waitApplyCh[rfIndex] = make(chan Op, 1)
      		chForWaitCh = kv.waitApplyCh[rfIndex]
      	}
      	kv.mu.Unlock()
      }
      
    • Raft执行Migrate的指令

      首先检测是不是和自身的config一致,然后通过上锁数组检查是不是自身需要修改的切片,然后新增一个空的数据对象,然后检测是不是需要接受的那一方,如果是则更新为指令的数据,否则就还是为空代表删除。

      func (kv *ShardKV) ExecuteMigrateOnServer(op Op) {
      	kv.mu.Lock()
      	defer kv.mu.Unlock()
      	myConfig := kv.config
      	//op中的config的num与自己的不一致则直接返回
      	if op.MigrateConfigNum != myConfig.Num {
      		return
      	}
      	//更新相关的切片信息
      	for _, shardComponent := range op.MigrateData {
      		//检查是不是自己需要修改的切片
      		if !kv.migratingShard[shardComponent.ShardIndex] {
      			continue
      		}
      		kv.migratingShard[shardComponent.ShardIndex] = false
      		//新建一个空的分片数据
      		kv.data[shardComponent.ShardIndex] = ShardComponent{
      			ShardIndex:    shardComponent.ShardIndex,
      			ShardData:     make(map[string]string),
      			LastRequestId: make(map[int64]int),
      		}
      		//如果是需要接受的,则存入数据,不是的话则为空表示删除原有信息
      		if myConfig.Shards[shardComponent.ShardIndex] == kv.gid {
      			CloneComponentData(&kv.data[shardComponent.ShardIndex], shardComponent)
      		}
      	}
      }
      
    • 服务器返回Migrate的Rpc请求

      如果超时,则检查是不是已经修改了,则直接返回OK,否则返回WrongLeader,如果从waitCh中接收到信息,需要检测configNum是否一致以及是否已经执行,通过则返回ok

      select {
      	case <-time.After(time.Millisecond * RfTimeOut):
      		if kv.checkMigrateState(args.MigrateData) {
      			reply.Err = OK
      		} else {
      			reply.Err = ErrWrongLeader
      		}
      	case rfCommitOp := <-chForWaitCh:
      		if rfCommitOp.MigrateConfigNum == args.ConfigNum && kv.checkMigrateState(args.MigrateData) {
      			reply.Err = OK
      		} else {
      			reply.Err = ErrWrongLeader
      		}
      	}
      	kv.mu.Lock()
      	delete(kv.waitApplyCh, rfIndex)
      	kv.mu.Unlock()
      	return
      
    • Put、Get、Append

      总体流程与普通K-V数据库基本一致。不同在于,客户端需要根据key得出相应的分片,然后通过配置信息找到管理该分片的服务器组,然后去请求该组的服务器。服务器也需要根据key找到分片,然后读取该分片的信息去修改或读取该key的值

      func key2shard(key string) int {
      	shard := 0
      	if len(key) > 0 {
      		shard = int(key[0])
      	}
      	shard %= shardctrler.NShards
      	return shard
      }
      
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值