程序员

6.824 Lab 3: Fault-tolerant Key/

2016-10-26  本文已影响964人  小聪明李良才

Introduction

该实验是mit 6.824课程的第3个实验,基于raft协议完成一个key-value系统

实验分为A和B两个部分,在Part A中:我们不考虑日志的大小,在Part B中会完成快照功能

完整的代码地址
课程地址
实验地址
已经有的实验地址:
Lab 1: MapReduce6.824 Lab 1: MapReduce(2016)
Lab 2: Raftraft 系列解读(2) 之 测试用例
Lab 3: KV Raft Part-A6.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的时候,回复客户端

提示

  1. 调用Start()后,kvraft servers 会等待raft log达成一致,通过applyCh获取一致的命令,我们需要考虑怎么安排代码,才能持续读取applyCh,而其他命令也能执行
  2. 我们需要处理case:leader调用了Start(),但是在log commit之前,丢失了leadership,这种情况下,代码应该将请求重新发送给新的leader。一种方式是,server需要检测出自己已经不是leader了,通过查看相同的start在index上返回一个不用的请求,另一种方式是通过调用GetState(),但是如果出现网络分区,可能不知道自己已经不是leader了,这种情况下client和server都处在网络分区中,因此可以无限的等待下去,直到网络恢复
  3. A kvraft server不应该完成Get()操作如果得不到majority,因为这样子可能会得不到最新的数据

任务:

需要处理重复请求,保证满足at-most-once的语义

提示:

  1. 需要对每个client请求编号
  2. 要保证快速的释放内存,因此可以在下一个请求带上下一个请求

实际设计中出现的问题

频繁变化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

}

上一篇下一篇

猜你喜欢

热点阅读