MIT 6.824 Lab3A Key/Value Service

1,657 阅读2分钟

Lab3需要在Lab2的基础上完成,因此大前提是要保证Raft层实现是正确的。

  1. Raft Part A | MIT 6.824 Lab2A Leader Election - 掘金 (juejin.cn)
  2. Raft Part B | MIT 6.824 Lab2B Log Replication - 掘金 (juejin.cn)
  3. Raft Part C | MIT 6.824 Lab2C Persistence - 掘金 (juejin.cn)
  4. Raft Part D | MIT 6.824 Lab2D Log Compaction - 掘金 (juejin.cn)

实验准备

  1. 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src/kvraft
  2. 如何测试:go test -run 3A -race
  3. 实验指导:6.824 Lab 3: Fault-tolerant Key/Value Service (mit.edu)

实验目标

  1. 通过TestBasic3A测试,在网络可靠且服务器可靠的情况下,实现PutAppendGet操作,使用Op结构描述指令,并传递给Start()
  2. 通过所有测试,考虑unreliable net、partitions、crash等不可靠的情况,例如同一客户端的重复的请求只应该被处理一次。

一些提示

  1. 调用Start()后需要等待Raft层Apply后再回复客户端。
  2. 如果是非majority的分区,不应该处理Get请求(不回复旧数据),一个简单的解决方案是将Get指令写入日志序列(这样Raft层不会Apply,回复超时给客户端)。
  3. 当日志标记为commit状态前,失去了Leader身份,需要重发请求给新的Leader,考虑如何处理之前的Leader Apply的日志。
  4. 允许非majority分区的服务器和客户端无限期等待分区恢复。
  5. 唯一的表示客户端请求,保证每一个请求只执行一次,注意该方案的内存占用。

Clerk

需要添加leaderIdclientIdsequenceId,并且PutAppendGet都调用同一个RPC。

提示5:clientIdsequenceId唯一标识客户端的请求。

func (ck *Clerk) Request(key string, value string, act string) string {
	args := Args{
		Key:   key,
		Value: value,
		Act:   act,
		Cid:   ck.cid,
		Seq:   atomic.AddInt64(&ck.seq, 1),
	}
	reply := Reply{}

	for {
		ok := ck.servers[ck.lid].Call("KVServer.Request", &args, &reply)
		if ok && reply.Err == OK {
			break
		} else if !ok || reply.Err == ErrWrongLeader {
			ck.mu.Lock()
			ck.lid = (ck.lid + 1) % len(ck.servers)
			ck.mu.Unlock()
		}
	}

	return reply.Value
}

KVServer

Server中需要添加3个map,1个用于存储数据,1个用于记录每个Clerk已处理的最大的SequenceId,1个用于在Raft层Apply后通知RPC。

对于每一个RPC,如果args.Seq <= maxSeq,说明这是一个重复的请求,对于读请求可以返回,但是写请求需要忽略。

if args.Seq <= kv.mseq[args.Cid] {
    reply.Err = OK
    // 写请求不需要reply.Value,所以不需要判断是读还是写
    reply.Value = kv.data[args.Key]
    return
}

读写请求都只有Leader才能处理,非Leader需要重发请求给其余节点。

index, _, isLeader := kv.rf.Start(Op{
    Key: args.Key,
    Value: args.Value,
    Cid: args.Cid,
    Seq: args.Seq,
    Act: args.Act,
})

if !isLeader {
    reply.Err = ErrWrongLeader
    return
}

使用index唯一的标记一个RPC请求,在日志Apply后,通过对应的channel通知该RPC,注意不能使用clientId + sequenceId,因为重复的请求也是要通知的。

kv.recv[index] = make(chan Op)

接下来就需要等待Raft层Apply日志,这里需要设置一个超时时间,超时后让Clerk重发请求。

select {
case op := <-kv.recv[index]:
    if op.Cid != args.Cid || op.Seq != args.Seq {
        reply.Err = ErrRetry
    } else {
        reply.Err = OK
        kv.mu.Lock()
        reply.Value = kv.data[op.Key]
        kv.mu.Unlock()
    }
case <-time.After(1 * time.Second):
    reply.Err = ErrRetry
}

close(kv.recv[index])
delete(kv.recv, index)

提示3:使用op.Cid != args.Cid || op.Seq != args.Seq判断Old Leader Apply的日志。

另一个goroutine中,不断处理ApplyMsg,并通知RPC。

注意这里还是要判断一下op.Seq > kv.mseq[op.Cid],虽然Request中判断过,但是到了这里maxSeq是可能会变的。

for !kv.killed() {
    msg := <-kv.applyCh
    if !msg.CommandValid {
            continue
    }
    op := msg.Command.(Op)

    if op.Seq > kv.mseq[op.Cid] {
        kv.mseq[op.Cid] = op.Seq
        switch op.Act {
        case PUT:
                kv.data[op.Key] = op.Value
        case APPEND:
                kv.data[op.Key] += op.Value
        }
    }
    
    // 不ok的情况,考虑提示3
    if _, ok := kv.recv[msg.CommandIndex]; ok {
        kv.recv[msg.CommandIndex] <- op
    }
}

实验总结

代码不多,但难理解的地方比较多。还有一个点是TestSpeed3A可能会超时,具体测试是单客户端1000个Append最后一个Get,这里需要修改Raft层,每一次Start()都要去同步日志,否则通过心跳同步太慢了。

本文代码没有体现数据同步,记得为临界资源上锁。

QQ截图20211115203756.png

最后,为了证明我不是在乱写,附上我的测试结果。