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的程度,只是结构需要花点时间理解一下。