MIT 6.824 Lab2: Raft Leader Election 总结

504 阅读4分钟

MIT 6.824 Lab2A: Leader Election

简介

从 Lab2A 开始到 Lab2D 要逐步的实现 Raft 算法:

  • Lab2A:领导人选举

  • Lab2B:日志复制

  • Lab2C:持久化

  • Lab2D:快照

完成了前两个 Lab 之后,其实后两个需要写的内容非常少,但是细节非常多,大多数 bug 都是因为 A、B 中的实现出现了问题,并且 A、B 的测试全部通过了,但是隐藏的 bug 可以被后面的测试检测出来。

Lab2A 的实验目标是实现 Raft 领导人选举和心跳(暂时不需要添加日志)。选出一名Leader,如果 Leader 没有宕机,那么它继续担任 Leader,如果旧 Leader 宕机,或者 Follower 在选举间隔内没有收到旧 Leader 的心跳包,将发起一轮新的选举,选出新的 Leader。

在写 Lab2 的时候一定要按照论文中给出的规则来实现,不能自己随意的更改,并且课程说明给出的提示同样非常重要。

实现

思路梳理

  • 根据图二将实现领导选举需要的结构体中的属性补充好(见结构体设计部分)

  • 需要实现两个 RPC 调用:

    • RequestVote RPCs,用于 Candidate 向其他所有服务器发起投票请求

    • AppendEntries RPCs,用于 Leader 向其他所有服务器发送心跳包(暂时不需要附带日志)

  • 需要实现两个计时器:

    • 心跳时间:当心跳时间超时,Leader 会发起一轮新的心跳。心跳时间固定,我选的是 50 ms

    • 选举时间:在选举时间超时,Follower 会转变成 Candidate 并发起一轮新的选举。选举时间需要选取一个随机数,按照课程说明上应该从 150 - 300 ms 的范围选取,这样可以避免多个服务器同时发起选举,导致一直无法选出领导人。

  • 方法中具体的逻辑和服务器状态的转换要严格遵守图二中的规则,并且要结合整个第五节的内容来实现

结构体设计

Raft

type Raft struct {
   mu               sync.Mutex 
   peers            []*labrpc.ClientEnd
   persister        *Persister
   me               int
   dead             int32
   currentTerm      int
   voteFor          int
   log              []*Entry
   commitIndex      int
   lastApplied      int
   nextIndex        []int
   matchIndex       []int
   state            State
   electionTimeout  *time.Timer
   heartBeatTimeout *time.Timer
   applyCh          chan *ApplyMsg
}

全都是根据论文中的描述来进行结构体的设计,添加了几个必要的属性:

  • 服务器状态state
  • 选举时间计时器electionTimeout
  • 心跳时间计时器heartBeatTimeout
  • 提交通道applyCh

注:实验说明中并不建议使用 timer 来实现超时检查。

Entry

type Entry struct {
   Command interface{}
   Term    int
   Index   int
}

日志结构体中有:

  • 指令
  • 这条日志所处的任期
  • 日志的索引

剩下的两种 RPC 请求的参数结构体就按照图二来设计即可,此处不再列出。

实现细节

Ticker

我在Ticker中完成了超时选举和发送心跳两个功能,利用timer.C结合for-select结构实现。如果心跳时间超时,则判断是否是 Leader,是的话就发送心跳,并且重置心跳时间;如果选举时间超时,则判断是否是 Leader,如果不是就发一起一轮新的选举。

func (rf *Raft) ticker() {
   for rf.killed() == false {
      select {
      case <-rf.electionTimeout.C:
         rf.mu.Lock()
         // ... 省略部分代码
         rf.mu.Unlock()
      case <-rf.heartBeatTimeout.C:
         rf.mu.Lock()
         // ... 省略部分代码
         rf.mu.Unlock()
      }
   }
}

Leader 定期发送心跳

在心跳时间超时后,Leader 就会发起一轮心跳,用于同步日志或者维持 Leader 的地位。在 Lab2A 中要处理回复的情况只有当回复的任期大于当前任期,该 Leader 转变为 Follower。

func (rf *Raft) broadcastHeartBeat() {
   for peer := range rf.peers {
      if peer == rf.me {
         rf.electionTimeout.Reset(rf.getElectionTimeout())
         continue
      }
      args := &AppendEntriesArgs{
         // ... Leader 实现选举只需要发送任期和自己的服务器 Id 即可
      }
      go 使用协程来批量发送并处理 RPC {
         reply := &AppendEntriesReply{}
         // ...
         if rf.sendAppendEntries(peer, args, reply) {
            // ... 根据图二处理回复
         }
         // ...
      }(peer)
   }
}

AppendEnries RPC

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
   rf.mu.Lock()
   defer rf.mu.Unlock()
   reply.Success = false
   // 如果 term < currentTerm 就返回 false
   if rf.currentTerm > args.Term {
      return
   }
   if args.Term > rf.currentTerm {
      // 如果心跳的任期大于当前的任期,则更新状态至 Follower  
   }
   reply.Term = rf.currentTerm
   if args.Term == rf.currentTerm {
      if rf.state == Candidate {
         rf.state = Follower
      }
      rf.electionTimeout.Reset(rf.getElectionTimeout()) // 重置选举时间
      // ... 2B,2C,2D
   }
}

Follower 转变为 Candidate 并发起选举

func (rf *Raft) beginElection() {
   rf.state = Candidate
   rf.currentTerm = rf.currentTerm + 1 // 增加任期
   rf.votedFor = rf.me                 // 给自己投票
   votes := 1
   for peer := range rf.peers {
      if idx == rf.me {
         continue
      }
      args := &RequestVoteArgs{
         // 暂时只需要任期和自己的Id
      }
      go 使用协程来发送并处理RPC {
         reply := &RequestVoteReply{}
         // ...
         if rf.sendRequestVote(peer, args, reply) {
            // ... 根据图二处理回复
         }
      }(peer)
   }
}

RequestVote RPC

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
   rf.mu.Lock()
   defer rf.mu.Unlock()
   reply.VoteGranted = false
   if args.Term > rf.currentTerm {
      // 如果请求的任期大于当前任期,则转变为 Follower 状态
   }
   if rf.currentTerm == args.Term && (rf.votedFor == -1 || rf.votedFor == args.CandidateId) {
      // 如果任期相同,并且当前服务器的 voted 为空或等于 CandidateId 则投票
      // 重置选举时间
      // 暂时不考虑日志一样新的条件
   }
   reply.Term = rf.currentTerm
}

注意点

Lab2A的细节不是特别多,一定要多使用DPrint来打日志,因为没有办法打断点来调试。难点在于各种状态之间的转换,要仔细阅读第五章的内容,因为图二也只是一个概括,有些细节在其中并没有体现。

还有一个要十分注意的地方,就是每次发送、接收 RPC 时,一定要检查 RPC 请求里的任期跟当前的任期是否相同,还要检查身份是否发生变化,比如说发送心跳 RPC 的时候,身份一定是 Leader。如果任期或者身份转变了,那么就要抛弃这一批中的后面的 RPC,否则会造成很多 Bug 。