美文网首页程序员
6.824 Lab 3: Fault-tolerant Key/

6.824 Lab 3: Fault-tolerant Key/

作者: 小聪明李良才 | 来源:发表于2016-10-26 15:54 被阅读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
    
    }
    
    

    相关文章

      网友评论

        本文标题:6.824 Lab 3: Fault-tolerant Key/

        本文链接:https://www.haomeiwen.com/subject/vmoduttx.html