Mit6.824-lab3a-2022

Mit6.824-lab3a-2022

写在前面

个人感觉lab3相比于lab2要简单不少,唯一的难点应该就是没有lab2那样明确的paper指导。对我来说花费时间最多的反而是对lab2的修补,用了一个大佬的lab2代码,两天就把lab3的代码全部通过测试,回过头来debug自己lab2的代码反反复复花了一个多星期。而且和lab2直接对raft进行debug不同,lab3要结合自己的逻辑然后和lab2一起debug。单跑其中一个测试,我这边就有10万多行的log信息,只能一点点翻看。几次想要重构自己的lab2的代码,最后还是修修补补继续用了。

实验目标

3a要求完成一个cs系统,包括数个server,每一个server对应一个Raft服务器,以及数个client,client向server发送get/put/append请求,server将其传递给Raft集群的leader,并等待其apply之后,更新本地数据,并吧结果返回给client。整个结构都很简单,唯一需要注意的是多client重复发起请求时,需要保证请求的线性一致。不过这个实验对一致性要求很低,由server保证即可。
这里借用一下这位的图,代码在这里
在这里插入图片描述

client

client需要满足两个方法

func (ck *Clerk) Get(key string) string
func (ck *Clerk) PutAppend(key string, value string, op string)
server

server需要满足对应的get和putappend方法,同时需要完成一个方法从Raft中接收apply以及更新自己的本地数据

client需要满足两个方法

func (kv *KVServer) Get(args *GetArgs, reply *GetReply)
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply)
func(kv *KVServer) apply(applyCh <-chan raft.ApplyMsg,  lastSnapshoterTriggeredCommandIndex int,snapshotTriger chan<- bool)

实验内容

定义部分

首先是一些常数和结构体的定义,没什么好说的,lab2里已经轻车熟路了,唯一要注意的是clientID是int64的,官方给的架构里就是这个。

package kvraft
const (
	OK             	= "OK"
	ErrNoKey       	= "ErrNoKey"
	ErrWrongLeader 	= "ErrWrongLeader"
	ErrShutdown     = "ErrShutdown"
	ErrInitElection = "ErrInitElection"
)
const (
	GetOperation   		= "Get"
	PutOperation       	= "Put"
	AppendOperation 	= "Append"
)
type Err string
// Put or Append
type PutAppendArgs struct {
	Key			string
	Value 		string
	ClientID	int64
	OPID 		int
	OP    		string // "Put" or "Append"
}
type PutAppendReply struct {
	Err Err
}
type GetReply struct {
	Err   Err
	Value string
}
type GetArgs struct{
	Key 		string
	ClientID	int64
	OPID 		int
}
client

寻找Leader的任务由client负责,因为server与Raft的对应是确定的,client找到Leader后记录一下,同时如果访问到的server不是Leader或者Raft集群正在选举,则要重复提交请求,直到成功提交或出现其他错误。

type Clerk struct {
	servers []*labrpc.ClientEnd
	// You will have to modify this struct.
	me 			int64
	leader 		int
	opID 		int
}
const (
	InitTime = 100	
)

func nrand() int64 {
	max := big.NewInt(int64(1) << 62)
	bigx, _ := rand.Int(rand.Reader, max)
	x := bigx.Int64()
	return x
}

func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {
	ck := new(Clerk)
	ck.servers = servers
	// You'll have to add code here.
	ck.me = nrand()
	ck.opID = 1
	ck.leader = 0
	log.Printf("1")
	return ck
}
func (ck *Clerk) Get(key stri
ng) string {
	//log.Printf("client : %d  start to get  %T", ck.me,key)
	args := &GetArgs{
		Key: 		key,
		ClientID: 	ck.me,
		OPID:		ck.opID,
	}

	ck.opID++
	serverID := ck.leader
	for{
		reply := &GetReply{}
		ok := ck.servers[serverID].Call("KVServer.Get", args, reply)
		if	!ok || reply.Err == ErrWrongLeader || reply.Err == ErrShutdown{
			serverID = (serverID + 1)%len(ck.servers)
			//log.Printf("client : %d  start to get  from %d", ck.me,serverID)
			continue
		}
		if reply.Err == ErrInitElection{
			time.Sleep(InitTime * time.Millisecond)
			continue
		}
		ck.leader = serverID
		if reply.Err == ErrNoKey{
			return ""
		} 
		if reply.Err == OK{
			return reply.Value
		}
	}
}

putAppend和get一样,这里就不放了,代码可以看我的git

server

server负责维护本地数据,也就是lab2对应的state ,同时保证接收与返回client请求的一致性。这里KVServer主要维护三个map.也是只在这放put代码

//每一个元素对应一个请求,key为Raft返回的index,value为一个chan以阻塞等待apply结果
commandApply	map[int]commandEntry
//每个元素对应一个Client最后请求的结果
ClientSequence	map[int64]applyResult
// 维护的本地数据
DB 				map[string]string
type Op struct {
	// Your definitions here.
	// Field names must start with capital letters,
	// otherwise RPC will break.
	Command 	string
	Key 		string
	Value 		string
	ClientID   	int64
	OPID 	int

}
type applyResult struct{
	Err 	Err
	Value 	string
	OPID	int
}
type commandEntry struct{
	op 				Op
	replyChannel 	chan applyResult
}
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.
	// 最后一个提交给applyCh的index
	lastApplied		int
	// commandndex to commandntry
	commandApply	map[int]commandEntry
	// Client to RPC结果
	ClientSequence	map[int64]applyResult
	// 维护的本地结果s
	DB 				map[string]string
}

func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
	// Your code here.

	if kv.killed(){
		reply.Err = ErrShutdown
		return
	}
	op := Op{
		Command:	GetOperation,	
		Key:		args.Key,
		Value:		"",
		ClientID:	args.ClientID,
		OPID:		args.OPID,
	}

	kv.mu.Lock()
	index,term,isleader := kv.rf.Start(op)
	if term == 0{
		kv.mu.Unlock()
		reply.Err = ErrInitElection
		return
	}
	if !isleader{
		kv.mu.Unlock()
		reply.Err = ErrWrongLeader
		return
	}
	channel := make(chan applyResult)
	kv.commandApply[index] = commandEntry{
		op:				op,
		replyChannel:	channel,
	}
	kv.mu.Unlock()
CheckTerm:
	for !kv.killed() {
		select{
			case result,ok := <- channel:
				if !ok{
					reply.Err = ErrShutdown
					return
				}
				reply.Err = result.Err
				reply.Value = result.Value
				return
			case  <-time.After(CheckTermInterval * time.Millisecond):
				tempTerm,_ :=kv.rf.GetState()
				if tempTerm != term{
					reply.Err = ErrWrongLeader
					break CheckTerm
				}
		}
	}

	go func(){<-channel}()
	if kv.killed(){
		reply.Err = ErrShutdown
		return
	}
}

在这里跟别人学到一个很好用的结构,用chan来通讯就不用发出一个goroutine来占用资源

select{
		case result,ok := <- channel:
		case  <-time.After(CheckTermInterval * time.Millisecond):
	}
apply

这里用一个无限循环读取applyCh里提交的log,在这里client可能对一个命令重复提交,但不会在上一个未返回时就提交下一个命令,所以每次server要判断是否是重复的命令,如果是则不更新本地数据,直接返回,否则更新本地后读取本地数据再返回。

func(kv *KVServer) apply(applyCh <-chan raft.ApplyMsg,  lastSnapshoterTriggeredCommandIndex int){
	var result string 
	for message := range applyCh{
		if message.SnapshotValid{
			continue
		}
		if !message.CommandValid{
			continue
		} 
		op := message.Command.(Op)
		kv.mu.Lock()
		kv.lastApplied = message.CommandIndex
		lastResult,ok := kv.ClientSequence[op.ClientID]
		if lastResult.OPID >= op.OPID {
			result = lastResult.Value		 
		}else{
			switch op.Command{
				case GetOperation:
					result = kv.DB[op.Key]
				case PutOperation:
					kv.DB[op.Key] = op.Value
					result = ""
				case AppendOperation:
					kv.DB[op.Key] = kv.DB[op.Key] + op.Value
					result = ""
			}
			kv.ClientSequence[op.ClientID] = applyResult{Value:result,OPID:op.OPID}	 
		}
		lastCommand,ok := kv.commandApply[message.CommandIndex]
		if ok{
			delete(kv.commandApply, message.CommandIndex)
			kv.mu.Unlock()
			if lastCommand.op != op{
				lastCommand.replyChannel <- applyResult{Err:ErrWrongLeader}	 
			}else{
				lastCommand.replyChannel <- applyResult{Err:OK,Value:result,OPID:op.OPID}		 
			}
		}else{
			kv.mu.Unlock()
		}
	}
	kv.mu.Lock()
	defer kv.mu.Unlock()
	for _,ce := range kv.commandApply{
		close(ce.replyChannel)
	}
}

测试

测试部分其实没什么好说的,逻辑代码没什么问题的话应该都顺利通过,唯一可能有问题的是Raft的提交部分,可能有些lab2能通过的测试,通不过lab3a,这个就需要回头去debug自己lab2的代码了。唯一需要说的测试是TestSpeed3A,这个测试的通过时间是33秒左右,要求在这个时间内提交一千个命令。如果Raft的结构没有专门设计优化过的话,这个地方可能要出问题。但是如果做点针对性的优化的话,这个测试最快可以3-4秒通过,但是会让逻辑变得比较复杂。这里提两个最简单的优化方案

1.快速AppendLog

每次在Leader获取到log后,不等待心跳时间,直接尝试发送一个appendentry,由于我之前的appendentry逻辑几乎是串行发送,所以这个地方要很注意nextIndex和matchIndex的修改,以及AppendEntriesArgs里参数也不能直接照抄appendentry里的参数,这个要直接尝试添加到log最后一个位置,这样只有与Leader同步的peer可以直接添加。我的代码在这里就刚好可以勉强通过这个测试。

func (rf *Raft) AppendNow(command interface{}){
	for i := 0;i<len(rf.peers);i++{
		if i == rf.me{
			continue
		}
		peer := i
		var data = make([]Entry,0)
		data = append(data, Entry{command,rf.currentTerm,false})

		args:=&AppendEntriesArgs{
			Term:rf.currentTerm,
			LeaderId:rf.me,
			PrevLogIndex:rf.LastIndex() - 1,
			PrevLogTerm:rf.ScalerTermToReal(rf.LastIndex() - 1),
			Entries:data,
			LeaderCommit:rf.commitIndex,
			IsEmpty:false,
		}
		reply:=&AppendEntriesReply{
			Term:0,
			Success:false,
			UpNextIndex:0,
		}

		ok := false
		go func(){
			ok = rf.sendAppendEntry(peer, args, reply)
			if ok{
				if args.Term != rf.currentTerm{
					return
				}				
				rf.mu.Lock()
				if reply.Success{
					rf.nextIndex[peer]  = Max(len(data) + args.PrevLogIndex + 1,rf.nextIndex[peer])
					rf.matchIndex[peer] = Max(rf.nextIndex[peer] - 1,	rf.matchIndex[peer])
					//log.Println("now",rf.nextIndex,len(data),peer)
					rf.mu.Unlock()
					
				}else{
					if reply.Term<=rf.currentTerm{
						if len(args.Entries) !=0 && rf.nextIndex[peer] >0{
							rf.nextIndex[peer] = reply.UpNextIndex
						}
						rf.mu.Unlock()
					}else{
						rf.mu.Unlock()
						rf.BeFollower(reply.Term, NILL)
						return
					}
				}
			}
		}()
	}
}
2.快速commit

不再使用一个goroutine进行apply,而是在每次更新了commitIndex之后立刻进行apply。但是这样写逻辑的话,需要一个更严谨的通讯和协调,所以如果是用goroutine进行apply的话,可以缩短其每次检查所需要的等待时间,我把每次尝试提交的时间从30ms缩短到10ms,这个测试稳定在10s左右通过。

总结

3a没什么好说的,只要lab2写得好,这个实验大概只有easy或者normal的程度,只是结构需要花点时间理解一下。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值