6.824 raft 实验

信静

首先阅读raft论文,地址如下:
https://github.com/maemual/ra...

raft的要点在于过半票决,这需要服务器数量是奇数,当出现网络分区时,网络不再对称。然后为了完成任何操作,必须凑够过半的服务器来批准相应的操作,因此另一个分区就不能完成任何操作。raft使用任期号来区分leader,每个任期最多一个leader,follower只需要知道当前的任期。

注意raft的易失性状态,确保这部分字段的值取决于raft的持久性状态就行。

lab 2a

根据论文内容构思raft节点的结构:

type Raft struct {
    mu        sync.Mutex          // Lock to protect shared access to this peer's state
    peers     []*labrpc.ClientEnd // RPC end points of all peers
    persister *Persister          // Object to hold this peer's persisted state
    me        int                 // this peer's index into peers[]
    dead      int32               // set by Kill()

    currentTerm    int  
    votedFor       int
    heartbeatTimer *time.Timer
    electionTimer  *time.Timer
    state          NodeState
    log            LogEntry

    
需要心跳和选举两个定时器,state表示自身的角色:领导者、追随者、候选者。还需要保存日志条目,注意这里的日志条目在论文图6有提到,大概意思是:日志需要记录索引,任期和指令。我们用数组下标代替索引,结构如下:
    type LogEntry struct {
        term int
        command interface{}
    }
接下来是转换角色的函数,在此之前,需要完成两个工具类函数,randTimeDuration和resetTimer,一个产生范围内的随机数,用来选举计时,为了避免分割选票,raft采用为选举计时器随机选择超时时间达到这一点。选举超时时间下界的选择要大于心跳,否则在收到正常的心跳之前就会触发选举,考虑到网络丢包,选举计时的下界应该为心跳计时的倍数,选举计时的随机时间差还应该极有可能大于rpc往返时间(可能是10ms),使得第一个开始选举的节点能完成一轮选举,因此上界应该足够大,但过大的问题在于选举时间可能会很长,因此过长可能会导致选举超时,我们应该合理的设置时间,根据测试的表现可能需要调小上界,确保leader能快速恢复,以下为设置心跳计时和选举计时上下界的代码:
    const (
        HeartbeatInterval    = time.Duration(120) * time.Millisecond
        ElectionTimeoutLower = time.Duration(300) * time.Millisecond
        ElectionTimeoutUpper = time.Duration(400) * time.Millisecond
    )

下面是转换角色的函数,该函数由于涉及到节点状态,因此调用该函数需要加锁。

func convertTo(rf *Raft, s NodeState) {

    if s == rf.state {
        return
    }
    DPrintf("Term %d:server %d convert from %v to %v\n",
        rf.currentTerm, rf.me, rf.state, s)
    pres := rf.state
    rf.state = s
    switch s {
    case Follower:
        if pres == Leader {
            rf.heartbeatTimer.Stop()
        }
        resetTimer(rf.electionTimer, randTimeDuration(ElectionTimeoutLower, ElectionTimeoutUpper))
        rf.votedFor = -1
    case Leader:
        rf.electionTimer.Stop()
        rf.broadcastHeartbeat()
        resetTimer(rf.heartbeatTimer, HeartbeatInterval)
    case Candidate:
        rf.startElection()
    }
}

接下来实现主要逻辑,对于raft的状态改变,都需要加锁,避免 race condition,以下实现startElection:

var count int64
    rf.currentTerm += 1
    args := &RequestVoteArgs{
        Term:        rf.currentTerm,
        CandidateId: rf.me,
    }
    for i := range rf.peers {
        if i == rf.me {
            rf.votedFor = rf.me
            atomic.AddInt64(&count, 1)
            continue
        }
        go func(server int) {

            reply := &RequestVoteReply{}
            if rf.sendRequestVote(server, args, reply) {
                rf.mu.Lock()
                //must lock after rpc
                DPrintf("%+v state %v got RequestVote response from node %d, VoteGranted=%v, Term=%d",
                    rf, rf.state, server, reply.VoteGranted, reply.Term)
                if reply.VoteGranted && rf.state == Candidate {
                    atomic.AddInt64(&count, 1)
                    if atomic.LoadInt64(&count) > int64(len(rf.peers)/2) {
                        rf.convertTo(Leader)
                    }

                } else {

                    if rf.currentTerm < reply.Term {
                        rf.currentTerm = reply.Term
                        rf.convertTo(Follower)
                    }
                }
                rf.mu.Unlock()
            }
        }(i)
    }

这里有几个地方需要注意,在执行RPC调用时不要加锁,否则就起不到并行handle的作用了,在lock时,尽量在同一层unlock,不要重复加锁和解锁,我在之前喜欢用defer解锁,但是在复杂的加锁逻辑下容易出错,写代码时尽量仔细检查加锁解锁的时机。

对应的rpc函数为:

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {

   rf.mu.Lock()
   defer rf.mu.Unlock()
   DPrintf("RequestVote raft %+v args %+v", rf, args)
   if rf.currentTerm > args.Term {
      reply.Term = rf.currentTerm
      reply.VoteGranted = false
      return
   }

   rf.convertTo(Follower)

   if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
      rf.votedFor = args.CandidateId
      reply.VoteGranted = true
      reply.Term = rf.currentTerm // not used, for better logging
   }
   // reset timer after grant vote**
   resetTimer(rf.electionTimer,
      randTimeDuration(ElectionTimeoutLower, ElectionTimeoutUpper))

   // Your code here (2A, 2B).

}

在投票后重置选举计时。

下面是broadcastHeadbeat的代码:

for i := range rf.peers {
   if i == rf.me {
      continue
   }
   go func(server int) {
      rf.mu.Lock()

      if rf.state != Leader {
         rf.mu.Unlock()
         return
      }
      args := &AppendEntriesArgs{
         Term:     rf.currentTerm,
         LeaderId: rf.me,
      }
      rf.mu.Unlock()
      reply := &AppendEntriesReply{}
      if rf.sendAppendEntries(server, args, reply) {
         rf.mu.Lock()
         if rf.state != Leader {
            rf.mu.Unlock()
            return
         }
         DPrintf("%+v state %v got appendEntries response from node %d, success=%v, Term=%d",
            rf, rf.state, server, reply.Success, reply.Term)
         if reply.Success {

         } else {
            if reply.Term > rf.currentTerm {
               rf.currentTerm = reply.Term
               rf.convertTo(Follower)
            }

         }
         rf.mu.Unlock()
      }

   }(i)
}

对应的handle为:

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
   // Your code here (2A, 2B).
   if args.Term < rf.currentTerm {
      reply.Success = false
      reply.Term = rf.currentTerm
      return
   }
   if args.Term > rf.currentTerm {
      rf.currentTerm = args.Term //update its own term
      rf.convertTo(Follower)
      // do not return here.
   }
   resetTimer(rf.electionTimer,
      randTimeDuration(ElectionTimeoutLower, ElectionTimeoutUpper))

   reply.Success = true
   reply.Term = rf.currentTerm //for debug

}

收到心跳后需要重置选举计时器。

最后是raft的make函数:

func Make(peers []*labrpc.ClientEnd, me int,
   persister *Persister, applyCh chan ApplyMsg) *Raft {
   rf := &Raft{}
   rf.peers = peers
   rf.persister = persister
   rf.me = me

   // Your initialization code here (2A, 2B, 2C).
   rf.currentTerm = 0
   rf.votedFor = -1
   rf.heartbeatTimer = time.NewTimer(HeartbeatInterval)
   rf.electionTimer = time.NewTimer(randTimeDuration(ElectionTimeoutLower, ElectionTimeoutUpper))
   rf.state = Follower

   go func(node *Raft) {
      for {
         select {
         case <-node.electionTimer.C:
            node.mu.Lock()
            node.electionTimer.Reset(randTimeDuration(ElectionTimeoutLower, ElectionTimeoutUpper)) //need to be reset because election may fail
            if node.state == Follower {
               node.convertTo(Candidate)
            } else {
               node.startElection() //Avoid electoral defeat
            }
            node.mu.Unlock()
         case <-node.heartbeatTimer.C:
            node.mu.Lock()
            if node.state == Leader {
               node.broadcastHeartbeat()
               node.heartbeatTimer.Reset(HeartbeatInterval)
            }
            node.mu.Unlock()
         }
      }
   }(rf)

   // initialize from state persisted before a crash
   rf.readPersist(persister.ReadRaftState())

   return rf
}

注意需要初始化选票为-1,节点在选举计时器到期后需要重置,此时可能有leader已经被选出,但可能心跳还未到达,但也可能选举失败,考虑到失败情况需要重新开始新一轮选举,在后续接受到心跳包时节点会转为follower。

lab 2b

接下来是2b的实验内容,需要同步日志信息到其他节点。在raft结构体下加入以下字段:

applyCh     chan ApplyMsg
nextIndex   []int
matchIndex  []int
commitIndex int
lastApplied int

这里注意nextIndex是乐观估计,matchIndex是保守估计。

注意事项

PrevLogIndex 如何选取:PrevLogIndex 其实就是指目前 Leader 认为该 Follower 已经同步的位置。因此设置为 nextIndex[Follower] 的前一位即可,这里用aggressive 的估计。

注意不能少了applyCh 。

在收到了其他candidate的 RequestVote时,并且该candidate 的 Term 较高,需要成功投票再更新 term,没有忠实实现这一条导致节点很难转成 Follower,无法重置自己的 votedFor。导致一直自己给自己投票并发起选举。这显然无法迅速选举出一个新的 Leader。

我们在对心跳包发送的 LogEntries进行 encoding 的时候,由于我们没有对RPC加锁,LogEntries作为引用类型,可能另一个地方正在对这些 entries 进行修改,这时,就可能出现 Leader 的 log 同时被读写的情况。为了避免这种情况,我们需要在 encoding 的时候, 将 entries 拷贝出来。

prevLogIndex := rf.nextIndex[server] - 1

clogs := make([]LogEntry, len(rf.logs[prevLogIndex+1:]))

copy(clogs, rf.logs[prevLogIndex+1:])
args := &AppendEntriesArgs{
   Term:         rf.currentTerm,
   LeaderId:     rf.me,
   PrevLogIndex: prevLogIndex,
   PrevLogTerm:  rf.logs[prevLogIndex].Term,
   LogEntries:   clogs,
   LeaderCommit: rf.commitIndex,
}

log 同步采用渐进法,论文说follower给leader返回ConflictTerm或许不会加速性能,因此这里一行代码解决

rf.nextIndex[server] -= 1

注意在setCommitIndex的时候,需要将log entry的command发送到applyMsg,这部分对应的是3a,在提交之后,发布applyMsg给kv server。

lab 2C:

这部分比较简单,实现持久化的时候,在rf的相关状态改变时调用persist就行。

lab 3a:

这部分是利用raft实现一个分布式的kv存储。

lab3 究竟要干嘛

通过 lab2 我们已经建立起了一个维护一致 log 的 Raft。 而 lab3 就是要基于 Raft 来实现一个简单的容错 key-value 存储服务。 它会用到 Raft 暴露出的以下接口:

  • 利用 Start() 来开始在 Raft 中同步一个操作
  • 利用 applyCh 来获取同步结果

对于从 applyCh 中获取的已经同步的操作,我们就可以执行了。 这些操作都是由构建在 Raft 之上的的服务定义的。比如在 lab3 中我们就定义了三种操作: Put,Get 和 Append。

client, server 和 raft 之间的关系

有个不错的图可以看看:

image.png

基本架构:

  • 每个 server 同时也是一个 Raft 节点
  • 多个 client 都往当时 Leader 所在的 server 发送请求

基本流程如下:

  1. client 向 server 发送 Put, Get, Append 请求
  • 每个 client 同时只发一个请求,不需要任何同步
  1. server 收到后调用 Start() 开始同步所有 raft 节点
  • server 需要能并行处理不同 client 发起的并发的请求
  1. 同步结束,raft 节点通过 applyCh 通知所在 server
  2. server 了解到已经同步成功,开始执行 client 请求的操作

如何设计 server 线程

首先,对于每个请求,handler 阻塞式等待运行结束。这个过程是并行的,即可以同时处理多个请求。 处理请求时,通过Start() 发送操作给 Raft 同步,并等待。

另外,在初始化时开启一个 goroutine 不断读取 applyCh 中的消息,并执行这些消息中的操作。 执行结束后通知对应的等待的 handler。

值得注意的是,对于非 Leader 的 server,他们同样需要执行这些操作,但是不需要通知 handler 操作已经结束。

go func() {
  for msg := range kv.applyCh {
    if msg.CommandValid == false {
      continue
    }
    op := msg.Command.(Op)
    DPrintf("kvserver %d applied command %s at index %d", kv.me, op.Name, msg.CommandIndex)
    kv.mu.Lock()
    lastAppliedRequestId, ok := kv.lastAppliedRequestId[op.ClientId]
    if ok == false || lastAppliedRequestId < op.RequestId {
      switch op.Name {
      case "Put":
        kv.db[op.Key] = op.Value
      case "Append":
        kv.db[op.Key] += op.Value
        // Get() does not need to modify db, skip
      }
      kv.lastAppliedRequestId[op.ClientId] = op.RequestId
    }
    ch, ok := kv.dispatcher[msg.CommandIndex]
    kv.mu.Unlock()

    if ok {
      notify := Notification{
        ClientId:  op.ClientId,
        RequestId: op.RequestId,
      }
      ch <- notify
    }
  }
}()

为了能够支持同时响应多个请求,我们引入了一个 dispatcher 来分发执行结束的通知给对应的等待中的 client。 它的 key 是 raft 中 log 的 index,value 是操作。我们每次在开始等待的时候新建一个 channel, 在同步完成后,从 channel 获取回复并将其删除。

func (kv *KVServer) waitApplying(op Op, timeout time.Duration) bool {
    // return common part of GetReply and PutAppendReply
    // i.e., WrongLeader
    index, _, isLeader := kv.rf.Start(op)
    if isLeader == false {
        return true
    }

    var wrongLeader bool
    defer DPrintf("kvserver %d got %s() RPC, insert op %+v at %d, reply WrongLeader = %v",
        kv.me, op.Name, op, index, wrongLeader)

    kv.mu.Lock()
    if _, ok := kv.dispatcher[index]; !ok {
        kv.dispatcher[index] = make(chan Notification, 1)
    }
    ch := kv.dispatcher[index]
    kv.mu.Unlock()
    select {
    case notify := <-ch:
        kv.mu.Lock()
        delete(kv.dispatcher, index)
        kv.mu.Unlock()
        if notify.ClientId != op.ClientId || notify.RequestId != op.RequestId {
            // leader has changed
            wrongLeader = true
        } else {
            wrongLeader = false
        }

    case <-time.After(timeout):
        wrongLeader = true
    }
    return wrongLeader
}

如何避免执行重复操作

由于我们会切换 Leader,client 也会重试发送请求,因此我们需要保证重试的请求不会被当作新的请求被执行。 因此我们对每一个 client 都维护了一系列的 sequence ID 来保证请求唯一。我们对于重试的请求, 是不会增加 sequence ID 的,通过 Start() 重复添加到 Raft 中, 在最后 server 处理的时候也可以通过 client ID 以及 sequence ID 来排除重复的操作。

如何验证请求返回

如要求所说,我们需要处理发起请求后,Leader 改变导致写入的 log 被覆盖的情况。 这时候请求返回的是另一个操作的结果。

我们对每个 client 维护一个操作的 sequence ID。对于 server 来说, 唯一的 client 号以及唯一的 sequence ID 对应了唯一的操作。

因此,我们只需要检查 client ID 和 sequence ID 就足以验证返回的是否当前请求的结果了。

参考:double-free/MIT6.824-2018-Chinese: A Chinese version of MIT 6.824 (distributed system) (https://github.com/double-fre...

阅读 1.4k

404 NOT FOUND

1 声望
0 粉丝
0 条评论

404 NOT FOUND

1 声望
0 粉丝
文章目录
宣传栏