Lab3需要在Lab2的基础上完成,因此大前提是要保证Raft层实现是正确的。
- Raft Part A | MIT 6.824 Lab2A Leader Election - 掘金 (juejin.cn)
- Raft Part B | MIT 6.824 Lab2B Log Replication - 掘金 (juejin.cn)
- Raft Part C | MIT 6.824 Lab2C Persistence - 掘金 (juejin.cn)
- Raft Part D | MIT 6.824 Lab2D Log Compaction - 掘金 (juejin.cn)
实验准备
- 实验代码:
git://g.csail.mit.edu/6.824-golabs-2021/src/kvraft
- 如何测试:
go test -run 3A -race
- 实验指导:6.824 Lab 3: Fault-tolerant Key/Value Service (mit.edu)
实验目标
- 通过
TestBasic3A
测试,在网络可靠且服务器可靠的情况下,实现Put
、Append
、Get
操作,使用Op
结构描述指令,并传递给Start()
。 - 通过所有测试,考虑unreliable net、partitions、crash等不可靠的情况,例如同一客户端的重复的请求只应该被处理一次。
一些提示
- 调用
Start()
后需要等待Raft层Apply后再回复客户端。 - 如果是非majority的分区,不应该处理Get请求(不回复旧数据),一个简单的解决方案是将Get指令写入日志序列(这样Raft层不会Apply,回复超时给客户端)。
- 当日志标记为
commit
状态前,失去了Leader身份,需要重发请求给新的Leader,考虑如何处理之前的Leader Apply的日志。 - 允许非majority分区的服务器和客户端无限期等待分区恢复。
- 唯一的表示客户端请求,保证每一个请求只执行一次,注意该方案的内存占用。
Clerk
需要添加leaderId
、clientId
,sequenceId
,并且Put
、Append
、Get
都调用同一个RPC。
提示5:
clientId
和sequenceId
唯一标识客户端的请求。
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()
都要去同步日志,否则通过心跳同步太慢了。
本文代码没有体现数据同步,记得为临界资源上锁。
最后,为了证明我不是在乱写,附上我的测试结果。