【MIT 6.824 Lab3】——Fault-tolerant Key/Value Service

Lab3 Fault-tolerant Key/Value Service


PartA

本部分需要基于Lab2的Raft建立K-V数据库,实现get,put,append的功能。

  • 实验流程

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

  • 对象设计

    主要的对象有服务器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
      	}
      }
      
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值