Lab3 Fault-tolerant Key/Value Service
PartA
本部分需要基于Lab2的Raft建立K-V数据库,实现get,put,append的功能。
-
实验流程
-
对象设计
主要的对象有服务器KVServer,操作指令Op,客户端Clerk。
-
KVServer
K-V服务器需要记录自己的编号和对应的Raft服务,以及相应的ApplyCh用于Raft服务器应用,也可以通知自身日志已经存储完成。并且需要存储日志应用后得到的数据data,并且设置通道用于通知服务器对应的指令是否已经在Raft处存储完成,为了防止不同的客户端请求不重复,设置lastRequestId记录每个客户端上次请求的ID。
type KVServer struct { mu sync.Mutex me int rf *raft.Raft applyCh chan raft.ApplyMsg dead int32 // set by Kill() // Your definitions here. data map[string]string //存储数据 waitApplyCh map[int]chan Op //等待raft应用后通知给server lastRequestId map[int64]int //不同客户端上次请求的ID(防止重复请求) }
-
Op
当K-V服务器收到来自Clerk的请求后,需要将这次请求进行记录,然后发送给Raft进行容错存储。首先需要记录这次请求的操作类型(Put,Get,Append),以及这次请求的Key和Value。并且需要记录发起该请求的客户端ID和请求ID,防止重复请求。
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 }
-
Clerk
客户端都有一个自己的ID,然后通过请求ID来记录每次的请求防止重复请求,因为leader会发送改变并且每次回持续一段时间,所以需要记录最近发起成功过的服务器ID,方便一段时间内能快速请求。如果请求失败,则会选择下一个服务器继续请求。
type Clerk struct { servers []*labrpc.ClientEnd // You will have to modify this struct. clientId int64 requestId int recentLeaderId int }
-
-
流程设计
-
服务器启动
服务器启动后需要读取传入的参数,如自身的ID,声明并初始化applyCh,启动raft服务,初始化数据data,waitApplyCh,各个客户端上次请求的ID,后台启动读取applyCh的进程。
labgob.Register(Op{}) kv := new(KVServer) kv.me = me kv.maxraftstate = maxraftstate // You may need initialization code here. kv.applyCh = make(chan raft.ApplyMsg) kv.rf = raft.Make(servers, me, persister, kv.applyCh) // You may need initialization code here. kv.data = make(map[string]string) kv.waitApplyCh = make(map[int]chan Op) kv.lastRequestId = make(map[int64]int) go kv.ReadRaftApplyCommand() return kv
-
读取已应用的日志
从ApplyCh持续读取Raft发送过来的日志,然后进行处理。因为get对于服务器本身的data没有影响,而put和append需要修改data,所以只需要针对put和append进行分类处理。然后再将指令发送到waitChan
func (kv *KVServer) ReadRaftApplyCommand() { for msg := range kv.applyCh { if msg.CommandValid { kv.GetCommand(msg) } } } func (kv *KVServer) GetCommand(msg raft.ApplyMsg) { //类型转换 op := msg.Command.(Op) //断言 DPrintf("[Command from raft],Server:%d,OpType:%v,OpClientId:%d,OpRequestId:%d,OpKey:%v,OpValue:%v", kv.me, op.OperationType, op.ClientId, op.RequestId, op.Key, op.Value) //判断是不是重复请求 //如果不是则针对append以及put操作将命令中的数据操作更新到Server中 if !kv.ifRequestRepetition(op.ClientId, op.RequestId) { if op.OperationType == "Put" { kv.ExecutePutOnServer(op) } if op.OperationType == "Append" { kv.ExecuteAppendOnServer(op) } } //发送消息给WaitChan通知server可以返回结果 if kv.maxraftstate != -1 { kv.CheckForSnapShot(msg.CommandIndex) } kv.SendMessageToWaitChan(op, msg.CommandIndex) }
-
发送指令到等待命令通道
Raft完成提交且服务器处理完相关操作后就可以发送指令到waitChan通知putappend/get RPC响应程序继续处理。
func (kv *KVServer) SendMessageToWaitChan(op Op, index int) { //修改之前需要上锁 kv.mu.Lock() defer kv.mu.Unlock() //检查waitChan是否已经初始化对应位置的值 ch, exist := kv.waitApplyCh[index] if exist { ch <- op } //} else { // kv.waitApplyCh[index] = make(chan Op, 1) // kv.waitApplyCh[index] <- op //} }
-
启动客户端
客户端启动后读取并保存参数的服务器类别,生成一个随机的ID,随机选取一个服务器。
func MakeClerk(servers []*labrpc.ClientEnd) *Clerk { ck := new(Clerk) ck.servers = servers // You'll have to add code here. ck.clientId = nrand() ck.recentLeaderId = mathrand.Intn(len(servers)) return ck }
-
客户端发起Put/Append请求
客户端发起put和append请求设置的rpc请求除了操作类型不同,其余参数类似,所以可以一起处理。每次请求都需要递增自己的请求ID存入请求中,防止请求重复,然后在请求中给出key和value的值和自己的ID。
func (ck *Clerk) Put(key string, value string) { ck.PutAppend(key, value, "Put") } func (ck *Clerk) Append(key string, value string) { ck.PutAppend(key, value, "Append") } func (ck *Clerk) PutAppend(key string, value string, op string) { // You will have to modify this function. //发起新请求,递增请求id ck.requestId++ requestId := ck.requestId server := ck.recentLeaderId for { args := PutAppendArgs{ Key: key, Value: value, Op: op, ClientId: ck.clientId, RequestId: requestId, } reply := PutAppendReply{} ok := ck.servers[server].Call("KVServer.PutAppend", &args, &reply)
-
服务器处理Put/Append请求
服务器接受到rpc请求后,需要先看自身是不是leader,如果不是则直接返回错误。如果是,则读取指令的类型(Put,Append)、key、Value、客户端ID和请求ID生成指令op,把指令op发送给Raft服务器进行容错存储。在对应map的位置设置等待完成的通道waitch,等待Raft提交该日志。
_, isLeader := kv.rf.GetState() if !isLeader { reply.Err = ErrWrongLeader return } //生产put和Appned的指令准备发给rf op := Op{ OperationType: args.Op, Key: args.Key, Value: args.Value, ClientId: args.ClientId, RequestId: args.RequestId, } //发送给rf rfIndex, _, _ := kv.rf.Start(op) DPrintf("[[PUTAPPEND 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容错存储且服务器自身处理完成后,会发送指令到对应的waitChan中,收到后对指令进行验证,验证无误后则可以返回ok,如果RequestID和clientID对不上说明可能请求的服务器不是leader了,对应编号的日志已经被其他日志占领,所以返回wrongLeader的错误。此外可能长时间(超过一次达成共识的时间)waitChan都收不到指令,则首先检查是不是重复指令,如果不是则说明服务器并没有执行该指令(因为执行完成了就会设置对应client的RequestID为该指令的ID),则可能该服务器已经不是leader没有执行完成该任务。则返回false。如果是重复指令,则说明已经执行完成,只是没有从waitChan中接收到消息,则直接返回ok。执行完成后需要将对应位置的waitChan删去减少存储开销。
select { case <-time.After(time.Millisecond * RfTimeOut): //_, ifLeader := kv.rf.GetState() DPrintf("[PUTAPPEND TIMEOUT]:opType:%v,opClientId:%d,opRequestId:%d,Server:%d,opKey:%v,rfIndex:%v", op.OperationType, op.ClientId, op.RequestId, kv.me, op.Key, rfIndex) if kv.ifRequestRepetition(op.ClientId, op.RequestId) { reply.Err = OK } else { reply.Err = ErrWrongLeader } case rfCommitOp := <-chForWaitCh: DPrintf("[PUTAPPEND Msg From RfWaitChan]:opType:%v,opClientId:%d,opRequestId:%d,Server:%d,opKey:%v,opValue:%v,rfIndex:%v", op.OperationType, op.ClientId, op.RequestId, kv.me, op.Key, op.Value, rfIndex) if rfCommitOp.ClientId == op.ClientId && rfCommitOp.RequestId == op.RequestId { reply.Err = OK } else { reply.Err = ErrWrongLeader } } kv.mu.Lock() delete(kv.waitApplyCh, rfIndex) kv.mu.Unlock() return
-
Clerk收到Put/Append回复
如果收到WrongLeader的错误则直接重新选择一个服务器再次发送请求,如果收到ok则直接返回。
if !ok || reply.Err == ErrWrongLeader { server = (server + 1) % len(ck.servers) continue } if reply.Err == OK { ck.recentLeaderId = server return }
-
Clerk发送Get请求
客户端递增自己的请求ID,然后放入自己的ID和请求KEY生成RPC请求参数
requestId := ck.requestId server := ck.recentLeaderId for { //初始化请求和回复参数 args := GetArgs{ Key: key, ClientId: ck.clientId, RequestId: requestId, } reply := GetReply{} ok := ck.servers[server].Call("KVServer.Get", &args, &reply) }
-
服务器接收Get请求
服务器需要检查自己是不是leader,如果不是则同样返回错误(因为需要先写入日志才能读取,只有leader才能发送日志进行同步)。然后生成含有操作类型get,key,clientId和RequestID的请求发送给Raft进行容错存储。并生产等待结果的通道。
_, isLeader := kv.rf.GetState() if !isLeader { reply.Err = ErrWrongLeader return } //生成get的指令准备发给对应的raft执行 op := Op{ OperationType: "get", Key: args.Key, Value: "", ClientId: args.ClientId, RequestId: args.RequestId, } //发送给rf 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存储日志完成,从waitChan接收到消息后,先检查接收到指令的clientID和RequestID是否和自身发送的op对的上,对不上则返回wrongLeader的错误。验证通过后再执行get操作,从服务器data中取出对应key的值,如果不存在则返回Nokey的错误。同样需要设置超时时间,如果超时了需要先检查是不是重复请求看是不是已经完成操作了,如果不是则返回WrongLeader,否则就执行get操作。执行完成后需要删除对应waitChan减少存储开销。
select { case <-time.After(time.Millisecond * RfTimeOut): //_, ifLeader := kv.rf.GetState() DPrintf("[PUTAPPEND TIMEOUT]:opType:%v,opClientId:%d,opRequestId:%d,Server:%d,opKey:%v,rfIndex:%v", op.OperationType, op.ClientId, op.RequestId, kv.me, op.Key, rfIndex) //检测是不是重复请求,看是不是操作已经执行了 if kv.ifRequestRepetition(op.ClientId, op.RequestId) { reply.Err = OK } else { reply.Err = ErrWrongLeader } case rfCommitOp := <-chForWaitCh: DPrintf("[PUTAPPEND Msg From RfWaitChan]:opType:%v,opClientId:%d,opRequestId:%d,Server:%d,opKey:%v,opValue:%v,rfIndex:%v", op.OperationType, op.ClientId, op.RequestId, kv.me, op.Key, op.Value, rfIndex) if rfCommitOp.ClientId == op.ClientId && rfCommitOp.RequestId == op.RequestId { reply.Err = OK } else { reply.Err = ErrWrongLeader } } kv.mu.Lock() delete(kv.waitApplyCh, rfIndex) kv.mu.Unlock() return
-
客户端接收到get回复
客户端如果收到wrongLeader的回复则重新选择新的服务器进行请求,如果是Nokey的错误则返回空值,如果ok则返回回复中的值。
if !ok || reply.Err == ErrWrongLeader { //RPC请求失败或者发送的服务器不是leader,则重新选一个新的服务器发送 server = (server + 1) % len(ck.servers) continue } else if reply.Err == ErrNoKey { //如果返回没有对应的key,组返回空值 return "" } else if reply.Err == OK { //如果返回ok,则返回回复中的值,并修改recentLeaderId ck.recentLeaderId = server return reply.Value } } return ""
-
PartB
- 实验流程
-
对象设计
本部分的设计只需要在A部分的基础上,在KVserver中加入snap的Index的下标即可
type KVServer struct { mu sync.Mutex me int rf *raft.Raft applyCh chan raft.ApplyMsg dead int32 // set by Kill() maxraftstate int // snapshot if log grows this big // Your definitions here. data map[string]string //存储数据 waitApplyCh map[int]chan Op //等待raft应用后通知给server lastRequestId map[int64]int //不同客户端上次请求的ID(防止重复请求) //snapshot LastIncludedIndex int }
-
流程设计
-
应用快照的内容
读取快照内容,并应用到服务器中。主要读取快照中的数据信息和请求的ID。
func (kv *KVServer) ReadSnapShotToApply(snapshot []byte) { if snapshot == nil || len(snapshot) < 1 { // bootstrap without any state? return } r := bytes.NewBuffer(snapshot) d := labgob.NewDecoder(r) var data map[string]string var lastRequestId map[int64]int if d.Decode(&data) != nil || d.Decode(&lastRequestId) != nil { log.Fatal("readPersist err") } else { kv.data = data kv.lastRequestId = lastRequestId } }
-
检查是否需要快照
当设置最大日志长度后,需要每次从applyCh中读取到信息后就检查当前的日志的长度需不需要压缩。如果接近阈值,则将当前的数据进行压缩形成新的快照,并且同步日志调用snapshot进行压缩
if kv.maxraftstate != -1 { kv.CheckForSnapShot(msg.CommandIndex, numerator, denominator) } func (kv *KVServer) CheckForSnapShot(rfIndex, numerator, denominator int) { if kv.rf.GetRaftStateSize() > (kv.maxraftstate * numerator / denominator) { snapshot := kv.MakeSnapShot() kv.rf.Snapshot(rfIndex, snapshot) } } func (kv *KVServer) MakeSnapShot() []byte { kv.mu.Lock() defer kv.mu.Unlock() w := new(bytes.Buffer) e := labgob.NewEncoder(w) e.Encode(kv.data) e.Encode(kv.lastRequestId) return w.Bytes()
-
InstallSnapshot
当应用通道中收到snapshot的消息后,需要让raft执行conInstallSnapshot,如果执行成功即日志成功应用InstallSnapshot后则读取该消息里的snapshot信息应用到data中进行同步
func (kv *KVServer) GetSnapShot(msg raft.ApplyMsg) { kv.mu.Lock() defer kv.mu.Unlock() if kv.rf.CondInstallSnapshot(msg.SnapshotTerm, msg.SnapshotIndex, msg.Snapshot) { kv.ReadSnapShotToApply(msg.Snapshot) kv.LastIncludedIndex = msg.SnapshotIndex } }
-