Introduction
该实验是mit 6.824课程的第3个实验,基于raft协议完成一个key-value系统
实验分为A和B两个部分,在Part A中:我们不考虑日志的大小,在Part B中会完成快照功能
完整的代码地址
课程地址
实验地址
已经有的实验地址:
Lab 1: MapReduce:6.824 Lab 1: MapReduce(2016)
Lab 2: Raft:raft 系列解读(2) 之 测试用例
Lab 3: KV Raft Part-A:6.824 Lab 3: Fault-tolerant Key/Value Service Part-A
Part A: Key/value service without log compaction
支持3个操作
Put(key, value):改变key的值
Append(key, arg):给key的值新增value
Get(key):返回值
任务
当没有丢包和servers fail的情况下进行实现,需要提供客户端顺序一致性的api,调用Put,Append和Get3个api,在所有的server以相同的顺序执行,并且具有at-most-once的语义
一个建议的计划是:先完成
server.go
中的Op
结构,然后完成server.go
中的PutAppend()
和Get()
操作,在操作中,应该先调用Start()
,当日志commit的时候,回复客户端
提示
- 调用
Start()
后,kvraft servers 会等待raft log达成一致,通过applyCh
获取一致的命令,我们需要考虑怎么安排代码,才能持续读取applyCh
,而其他命令也能执行- 我们需要处理case:leader调用了
Start()
,但是在log commit之前,丢失了leadership,这种情况下,代码应该将请求重新发送给新的leader。一种方式是,server需要检测出自己已经不是leader了,通过查看相同的start在index上返回一个不用的请求,另一种方式是通过调用GetState()
,但是如果出现网络分区,可能不知道自己已经不是leader了,这种情况下client和server都处在网络分区中,因此可以无限的等待下去,直到网络恢复- A kvraft server不应该完成
Get()
操作如果得不到majority,因为这样子可能会得不到最新的数据
任务:
需要处理重复请求,保证满足at-most-once的语义
提示:
- 需要对每个client请求编号
- 要保证快速的释放内存,因此可以在下一个请求带上下一个请求
实际设计中出现的问题
频繁变化leader
func (ck *Clerk) Get(key string) string {
args := GetArgs{Key:key}
for {
for _,c := range ck.servers {
time.Sleep(time.Millisecond*1000)
reply := GetReply{}
ok := c.Call("RaftKV.Get", &args, &reply)
if ok && !reply.WrongLeader {
return reply.Value
}
}
}
// You will have to modify this function.
return ""
}
此处如果没有sleep的话,相当于客户端一直不断的在START,导致的一个问题是:server不断在处理START命令,导致正常的心跳都完成不了了,就出现了频繁的变化leader了,问题很严重,那应该怎么做呢?
后来做了优化,对于读操作不走 chan,这就没问题了
index := -1
term := -1
isLeader := true
if rf.state != StateLeader {
isLeader = false
return index, term, isLeader
}
这样就有个初判断了
通过labrpc传递的数据不对
func StartKVServer(servers []*labrpc.ClientEnd, me int, persister *raft.Persister, maxraftstate int) *RaftKV {
// call gob.Register on structures you want
// Go's RPC library to marshall/unmarshall.
gob.Register(Op{})
如果没有 gob.Register(Op{}) 这就错误,为什么要加上这句话呢?
出现阻塞
分析:此处阻塞了为什么呢?因为在get上的时候,有一个没有收到apply?好奇怪
// TODO:优化超时的逻辑
select {
case op := <-ch:
commited := op == entry
kv.logger.Debug("index:%d commited:%v",index,commited)
return commited
case <- time.After(AppendTimeOut):
kv.logger.Info("index:%d %s timeout after %v",index, entry.Type,AppendTimeOut)
return false
}
加上上面的超时逻辑后,就可以解决阻塞的问题,但是一旦超时
2016/10/26 14:37:45 I index:323 Append timeout after 1s
2016/10/26 14:37:45 0: client new get 0
2016/10/26 14:37:45 get wrong value, key 0, wanted:
就会出现问题,会重复执行 Append操作,因为其实已经apply了这个请求了
那怎么解决呢?我现在去除这个超时限制,在获取Apply的时候逻辑变为下面的:
// 通知结果
ch, ok := kv.result[index]
if ok {
select {
case <-ch:
default:
}
}else {
// 没人读就有了数据?
ch = make(chan Op,1)
kv.result[index] = ch
}
ch <- op
此时就不会有超时的问题了,为什么呢?
很反人类的问题:因为当调用
func (kv *RaftKV)AppendLog(entry Op) bool {
index, _, isLeader := kv.rf.Start(entry)
此时可能没等到执行下面的去读chan的时候,已经apply成功了,因此我们就需要事先往chan里面存入数据
TestUnreliable
☁ kvraft [master] ⚡ go test -run TestUnreliable
Test: unreliable ...
2016/10/26 14:59:42 get wrong value, key 3, wanted:
x 3 0 yx 3 1 y
, got
x 3 0 yx 3 0 yx 3 1 y
很容易看出问题:一个请求重复执行了,我们需要在客户端去重
对于每个客户端都给编号,然后每个请求都顺序增长
TestManyPartitionsManyClients
测试出现阻塞
select {
case op := <-ch:
commited := op == entry
kv.logger.Debug("index:%d commited:%v", index, commited)
return commited
// 此处超时其实也很好理解,因为刚开始是leader,但是在log得到commit之前,丢失了leadership,此时
// 如果没有超时机制,则会一直阻塞下去
// 或者由于此时的leader是一个分区里面的leader,则只可能一直阻塞下去了
// 因此也需要超时
case <-time.After(AppendTimeOut):
//kv.logger.Info("index:%d %s timeout after %v", index, entry.Type, AppendTimeOut)
return false
}