Lab4 Sharded Key/Value Service
PartA
本部分实验需要完成分配服务器和分片之间的控制器
-
实验流程
-
对象设计
主要的对象为服务器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 }
-