MIT6.824 Lab 3: Fault-tolerant Key/Value Service (1)

来源:互联网 发布:订单系统数据库设计 编辑:程序博客网 时间:2024/05/29 23:47

Introduction
  在这次实验中,我们将使用Lab2的Raft库来实现容错的key-value存储服务。
  存储系统将由客户端和key/value服务器,每个key/value服务器使用Raft节点。客户端发送Put(), Append()和Get()的RPCs 到key/value服务器(kvraft),然后将这些RPC调用存入Raft的log中并按序执行。客户端可以向任何kvraft服务器发送RPC请求,但是如果该服务器不是Raft领导者或者请求失败超时,则需要重新发送给另外一个。假如操作被committed到Raft的log中并应用到状态机中,那么它的结果需要反馈到客户端。假如commit操作失败(比如领导者被替换),客户端必须重新发送请求。
  本次实验分为2部分。在Part A,我们要实现key/value服务,无需考虑日志长度。在Part B,我们需要考虑日志长度,实现Snapshot。

Part A: Key/value service without log compaction
  该服务支持3种RPC调用:Put(key, value),Append(key, arg)和Get(key)。Put()函数替换key对应的value,Append(key, arg)函数将arg增加到key对应的value,Get()函数获取key对应的value。Append值到不存在的key,就理解为Put函数。

具体实现
  与前面博客一样,我们先从测试代码出发,一步步实现具体函数。
  首先是TestBasic测试函数,该测试函数检查在无Fault情况下单客户端操作的正确性。其中涉及到了1个重要的测试函数GenericTest。

func GenericTest(t *testing.T, tag string, nclients int, unreliable bool, crash bool, partitions bool, maxraftstate int) {    const nservers = 5    cfg := make_config(t, tag, nservers, unreliable, maxraftstate)    defer cfg.cleanup()    ck := cfg.makeClient(cfg.All())    done_partitioner := int32(0)    done_clients := int32(0)    ch_partitioner := make(chan bool)    clnts := make([]chan int, nclients)    for i := 0; i < nclients; i++ {        clnts[i] = make(chan int)    }    for i := 0; i < 3; i++ {        // log.Printf("Iteration %v\n", i)        atomic.StoreInt32(&done_clients, 0)        atomic.StoreInt32(&done_partitioner, 0)        go spawn_clients_and_wait(t, cfg, nclients, func(cli int, myck *Clerk, t *testing.T) {            j := 0            defer func() {                clnts[cli] <- j            }()            last := ""            key := strconv.Itoa(cli)            myck.Put(key, last)            for atomic.LoadInt32(&done_clients) == 0 {                if (rand.Int() % 1000) < 500 {                    nv := "x " + strconv.Itoa(cli) + " " + strconv.Itoa(j) + " y"                    // log.Printf("%d: client new append %v\n", cli, nv)                    myck.Append(key, nv)                    last = NextValue(last, nv)                    j++                } else {                    // log.Printf("%d: client new get %v\n", cli, key)                    v := myck.Get(key)                    if v != last {                        log.Fatalf("get wrong value, key %v, wanted:\n%v\n, got\n%v\n", key, last, v)                    }                }            }        })        if partitions {            // Allow the clients to perform some operations without interruption            time.Sleep(1 * time.Second)            go partitioner(t, cfg, ch_partitioner, &done_partitioner)        }        time.Sleep(5 * time.Second)        atomic.StoreInt32(&done_clients, 1)     // tell clients to quit        atomic.StoreInt32(&done_partitioner, 1) // tell partitioner to quit        if partitions {            // log.Printf("wait for partitioner\n")            <-ch_partitioner            // reconnect network and submit a request. A client may            // have submitted a request in a minority.  That request            // won't return until that server discovers a new term            // has started.            cfg.ConnectAll()            // wait for a while so that we have a new term            time.Sleep(electionTimeout)        }        if crash {            // log.Printf("shutdown servers\n")            for i := 0; i < nservers; i++ {                cfg.ShutdownServer(i)            }            // Wait for a while for servers to shutdown, since            // shutdown isn't a real crash and isn't instantaneous            time.Sleep(electionTimeout)            // log.Printf("restart servers\n")            // crash and re-start all            for i := 0; i < nservers; i++ {                cfg.StartServer(i)            }            cfg.ConnectAll()        }        // log.Printf("wait for clients\n")        for i := 0; i < nclients; i++ {            // log.Printf("read from clients %d\n", i)            j := <-clnts[i]            if j < 10 {                log.Printf("Warning: client %d managed to perform only %d put operations in 1 sec?\n", i, j)            }            key := strconv.Itoa(i)            // log.Printf("Check %v for client %d\n", j, i)            v := ck.Get(key)            checkClntAppends(t, i, v, j)        }        if maxraftstate > 0 {            // Check maximum after the servers have processed all client            // requests and had time to checkpoint            if cfg.LogSize() > 2*maxraftstate {                t.Fatalf("logs were not trimmed (%v > 2*%v)", cfg.LogSize(), maxraftstate)            }        }    }    fmt.Printf("  ... Passed\n")}

  该函数接收7个参数,nclients表示并发客户端个数,unreliable代表RPC调用的可靠性,crash表示是否发生节点down了的情况,partitions表示是否发生网络分区,maxraftstate代表log的最大长度。该函数中首选确定kvraft节点个数为5个,调用make_config函数来初始化kvraft系统。进入make_config函数可以发现与之前实验中的一样,最重要部分在于创建kvraft节点,即StartKVServer函数。

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{})    kv := new(RaftKV)    kv.me = me    kv.maxraftstate = maxraftstate    kv.persister = persister    // Your initialization code here.    kv.applyCh = make(chan raft.ApplyMsg)    kv.rf = raft.Make(servers, me, persister, kv.applyCh)    kv.data = make(map[string]string)    kv.pendingOps = make(map[int][]*P_Op)    kv.op_count = make(map[int64]int64)    go func() {        for msg := range kv.applyCh {            kv.Apply(&msg)        }    }()    return kv}

  在StartKVServer函数中进行初始化1个kvraft节点,涉及的结构体为RaftKV结构体即描述kvraft节点。

type RaftKV struct {    mu      sync.Mutex    me      int    rf      *raft.Raft    applyCh chan raft.ApplyMsg    maxraftstate int // snapshot if log grows this big    persister  *raft.Persister    data       map[string]string    pendingOps map[int][]*P_Op    op_count   map[int64]int64}

  在RaftKV结构体中有几个重要的成员,maxraftstate用于表示log的最大长度,persister表示用于永久存储,data表示存储key/value,pendingOps用于记录正在执行的操作,op_count用于记录每个节点最后1个已经执行操作的Id。而用于每个客户端操作的结构体如下所示:

type Op struct {    Type   int    Key    string    Value  string    Client int64    Id     int64}type P_Op struct {    flag chan bool    op   *Op}

  其中Op结构体为具体存储到log中的内容,包括了操作类型(get、put、append),键值对(Key/Value),客户端Id和该客户端的操作Id(2者组合标示1个操作Id)。而P_op结构用于表示服务器节点上正在执行的操作,使用chan来表示同步,因为客户端需要等raft节点commit后才能返回。
  回到StartKeyServer函数中,当raft节点commit时我们需要进行具体处理即接收消息来处理,这里使用goroutine来接受applyCh中的消息,调用Apply函数来处理。
  上面都是服务器端的初始化工作,在GenericTest函数中初始化server后调用makeClient函数来进行初始化client。进入函数发现重要的部分是MakeClerk函数。

type Clerk struct {    servers []*labrpc.ClientEnd    // You will have to modify this struct.    leader_id    int    client_id    int64    cur_op_count int64}func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {    ck := new(Clerk)    ck.servers = servers    // You'll have to add code here.    ck.client_id = nrand()    ck.leader_id = 0    ck.cur_op_count = 0    return ck}

  首先是客户端结构体Clerk的具体内容,leader_id用于记录上一次成功操作通信的服务节点即记住Leader节点。由于只有Leader节点才能接受操作请求,这里记住Leader节点避免了每次请求都要遍历服务集群节点来找到Leader节点。client_id表示客户端Id,在初始化函数中使用nrand()函数来生成随机值。cur_op_count用于记录最后一次发出的操作Id。
  再次回到GenericTest函数中,对服务器和客户端初始化后,进入for循环。在for循环中原子性地设置done_clients和done_partitioner变量的值为0,用于控制后面goroutine中的循环。goroutine执行spawn_clients_and_wait函数,创建指定个数的客户端并发地发出操作请求。在run_client函数中调用makeClient函数创建客户端,并执行fn函数。这里使用chan来实现同步通信,确保fn函数被执行。

func run_client(t *testing.T, cfg *config, me int, ca chan bool, fn func(me int, ck *Clerk, t *testing.T)) {    ok := false    defer func() { ca <- ok }()    ck := cfg.makeClient(cfg.All())    fn(me, ck, t)    ok = true    cfg.deleteClient(ck)}func spawn_clients_and_wait(t *testing.T, cfg *config, ncli int, fn func(me int, ck *Clerk, t *testing.T)) {    ca := make([]chan bool, ncli)    for cli := 0; cli < ncli; cli++ {        ca[cli] = make(chan bool)        go run_client(t, cfg, cli, ca[cli], fn)    }    // log.Printf("spawn_clients_and_wait: waiting for clients")    for cli := 0; cli < ncli; cli++ {        ok := <-ca[cli]        // log.Printf("spawn_clients_and_wait: client %d is done\n", cli)        if ok == false {            t.Fatalf("failure")        }    }}

  在GenericTest函数中,fn函数为匿名函数,其主要功能是先调用Put函数执行put操作,然后不断地调用Append函数来执行append操作,在此过程中会调用Get函数来执行get操作检查上一次操作的值是否正确被写入。
  所以重点还是实现3个RPC操作。
  我们先来看看客户端的调用函数。对于put和append操作,这2者的可以用1个flag来表示操作区别。主要实现的函数是PutAppend函数。

func (ck *Clerk) PutAppend(key string, value string, op string) {    // You will have to modify this function.    var args PutAppendArgs    args.Key = key    args.Value = value    args.Op = op    args.Client = ck.client_id    args.Id = atomic.AddInt64(&ck.cur_op_count, 1)    for {        var reply PutAppendReply        ck.servers[ck.leader_id].Call("RaftKV.PutAppend", &args, &reply)        if reply.Err == OK && reply.WrongLeader == false {            DPrintf("Call success\n")            break        } else {            ck.leader_id = (ck.leader_id + 1) % len(ck.servers)        }    }}

  该函数中我们需要构建rpc调用的请求参数和保存结果的参数。

// Put or Appendtype PutAppendArgs struct {    Key   string    Value string    Op    string // "Put" or "Append"    Client int64    Id     int64}type PutAppendReply struct {    WrongLeader bool    Err         Err}

  请求参数PutAppendArgs的内容为表示键值对的Key/Value,操作类型Op,客户端Id的Client和操作Id。结果参数PutAppendReply的内容为表示该服务节点是否是Leader节点的WrongLeader以及错误原因Err。
  在PutAppend函数中先封装这次操作请求的请求参数,然后循环调用Leader服务节点的RPC请求。如果返回结果没问题,则跳出循环,否则(该节点不为Leader)将请求发给下一个服务节点。
  客户端关于get请求的操作与上面类似,这里不再详述。
  接下来看一下,服务器端接收到请求后的处理流程。

func (kv *RaftKV) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {    // Your code here.    var op Op    if args.Op == "Put" {        op.Type = OpPut    } else {        op.Type = OpAppend    }    op.Key = args.Key    op.Value = args.Value    op.Client = args.Client    op.Id = args.Id    reply.WrongLeader = kv.execOp(op)    if reply.WrongLeader {        reply.Err = ErrNotLeader    } else {        reply.Err = OK    }}

  由于3种请求为简单操作,其具体实现类似,所以定义了1个通用的函数execOp来处理请求。在这之前将请求参数中内容提取封装成Op结构体,因为具体存储到log中的是Op结构体。

func (kv *RaftKV) execOp(op Op) bool {    op_idx, _, is_leader := kv.rf.Start(op)    if !is_leader {        DPrintf("This server is not Leader\n")        return STATUS_FOLLOWER    }    waiter := make(chan bool, 1)    DPrintf("Append to pendingOps op_idx:%v  op:%v\n", op_idx, op)    kv.mu.Lock()    kv.pendingOps[op_idx] = append(kv.pendingOps[op_idx], &P_Op{flag: waiter, op: &op})    kv.mu.Unlock()    var ok bool    timer := time.NewTimer(TIMEOUT)    select {    case ok = <-waiter:    case <-timer.C:        DPrintf("Wait operation apply to state machine exceeds timeout....\n")        ok = false    }    delete(kv.pendingOps, op_idx)    if !ok {        DPrintf("Wrong leader\n")        return STATUS_FOLLOWER    }    return STATUS_LEADER}

  在execOp函数中,调用raft节点的Start函数来达成共识提交本次操作。然后根据本次操作新建1个P_Op结构体实例表示这次操作正在执行,添加到等待执行操作的队列中。同时新建1个计时器来记录等待时间。使用select实现超时机制,当waiter chan中收到消息时表示本次操作有了具体结果。如果没有超时并且本次操作正确执行了则返回成功信息,否则返回错误信息。
  而waiter chan的消息主要是在前面提到的Apply函数中写入的。

func (kv *RaftKV) Apply(msg *raft.ApplyMsg) {    kv.mu.Lock()    defer kv.mu.Unlock()    var args Op    args = msg.Command.(Op)    if kv.op_count[args.Client] >= args.Id {        DPrintf("Duplicate operation\n")    } else {        switch args.Type {        case OpPut:            DPrintf("Put Key/Value %v/%v\n", args.Key, args.Value)            kv.data[args.Key] = args.Value        case OpAppend:            DPrintf("Append Key/Value %v/%v\n", args.Key, args.Value)            kv.data[args.Key] = kv.data[args.Key] + args.Value        default:        }        kv.op_count[args.Client] = args.Id    }    for _, i := range kv.pendingOps[msg.Index] {        if i.op.Client == args.Client && i.op.Id == args.Id {            DPrintf("Client:%v %v, Id:%v %v", i.op.Client, args.Client, i.op.Id, args.Id)            i.flag <- true        } else {            DPrintf("Client:%v %v, Id:%v %v", i.op.Client, args.Client, i.op.Id, args.Id)            i.flag <- false        }    }}

  在Apply函数中节点收到了日志提交的消息。先将Log的Command转换为Op结构体,判断该提交的操作是否是冗余的操作。这里涉及到了RPC调用请求丢失时的处理,对于这种情况我们在这里采取了简单的处理方式,就是重新接受操作请求但是在提交日志执行操作时判断一下是否是冗余的操作(通过Id标示)。如果是冗余操作则忽视但是操作结果仍视为成功,如果不是则进行相应操作(Put或者Append)。当执行操作之后需要消息反馈给等待的客户端,可以通过前面使用的waiter chan,即往里面写入成功的消息。至此整个操作流程就阐述完了。
  后面的一些测试函数其实检查的是raft的正确性。

0 0
原创粉丝点击