lab2A分析与实现

一、代码入手——读lab说明

1.根据课程lab指引上的说明,从raft/raft.go文件入手。
官方lab说明
2.代码需要实现下面的接口
在这里插入图片描述
3.其中的Make方法是返回了一个raft服务实例
peers参数是一个raft实例的网络id数组(包括当前的实例)。me参数是当前peer在peers中的下标。
4.Start(command)是要求raft启动一个进程来将coomand命令加入到副本日志中去。
Start方法应该立刻返回,不需要等待日志添加完成。
5.服务需要实现对于每一个新提交的log entry发送一个ApplyMsg给Make()的参数applych通道。
6.raft.go包括一个发送RPC的例子sendReduestVote(), 和处理一个传入RPC的例子RequestVote().
7.Raft实例必须通过RPC交互,不允许通过共享Go变量或文件。
8.lab2A
(1)2A部分的实验是实现了raft的领导选举和发送心跳信息(没有log entry的AppendEntries)
(2)2A的目的是选举出一个leader,在这个leader没有出现错误的情况下保持它的leader身份,和旧leader出错后新的leader接管。
(3)运行go test -run 2A来测试2A部分代码
(4)2A部分主要是论文Figure2的内容,发送和接收RequestVote RPC, 服务器和选举有关的规则,和领导选举相关的状态。
(5)在raft.go的Raft struct中增加Figure2中领导选举的状态。也需要定义一个结构体来保持每一个log entry的信息。
(6)填写RequestVoteArgs和RequestVoteReply结构体,修改Make()方法来创建一个后台协程,在它一段时间内没有收到另一个raft实例的消息时,就周期性的发送RequestVote RPC消息进行leader选举。通过这种方式,一个raft实例可以知道谁是leader,或者自己成为leader。
实现RequestVote() RPC处理方法,使得服务器能够为另一个投票
(7)为了实现心跳,定义一个AppendEntries RPC结构体,让leader周期性的发送它们。
写一个AppendEntries RPC处理方法,可以重置选举超时,这样当一个server已经被选举为leader时,其他的就不会当选了。
(8)要确定不同实例的选举超时不会同一时间发起,否则每个实例都会投票给自己,将永远不会有实例被当选为leader
(9)测试需要leader发送心跳RPC消息不会大于每秒十次。
(10)测试需要raft在旧leader失败5s内选出新的leader。
(11)超时时间虽然论文中是150ms-300ms,因为测试要求每秒最多10个心跳信息,所以超时时间需要比300ms大一点,但是不能太大,否则5s内无法完成新leader的选举。
(12)go中的rand是非常有用的
(13)对于一些需要周期性或者延迟一些时间运行的方法,可以使用time.Sleep()。不要用time.Timer或者time.Ticker。
(14)lock的参考
a. 当一个数据被多个协程使用,至少一个需要修改时,使用lock
b.当一个系列的修改不能让其他协程看到中间状态时,使用lock
c.当代码进行一系列读操作,可能会由其他协程读写时造成错误结果,使用lock
raft RPC处理方法会从头到尾持有一个lock
d.不能在做任何事的时候都加锁,一是效率低,二是会死锁。
e.要注意在释放锁和再次获得锁的地方。例子中在等待锁的期间,变量可能会有变化,导致执行错误。
(15)struct的参考
一个raft实例有两个时间驱动活动:leader发送心跳,和如果长时间未收到leader的信息跟随者发起选举。最好是这些活动各自有一个专用的长时间的协程,而不是都写到一个单独的协程。
管理选举超时,最简单的方式就是在raft结构体中设置一个变量来保留raft实例从leader获取信息的最后的时间。并且有一个选举超时的协程周期性的检查这些时间。一个简单的方式是使用time.Sleep()方法来驱动这些周期性的检查。
发送已提交log到applyCh需要使用一个单独的长时间运行的协程,因为发送到applyCh通道会阻塞,所以一定要是个单独的协程。一定要使用一个协程是因为否则会很难确认发送的log entries在日志中的顺序。推进coomitIndex的代码需要启动apply协程,最简单的方法是使用一个条件变量sync.Cond
RPC响应进程应该与发送进程在同一个Go协程中,而不是通过一个channel发送响应消息。
记住网络会延迟RPC及其响应消息。当发送同步RPC时,网络会对请求和回应重新排序。Figure 2 的RPC响应进程不详尽。leader必须小心处理响应进程;它必须检查term从发送RPC后有无改变,并且必须对给相同follower发送同步RPC响应时改变了leader的状态负责任。
(16)通不过测试返回读论文的Figure 2
(17)不要忘记实现GetState()方法
(18)测试会调用rf.Kill()方法来永久的关闭一个实例。你可以使用rf.killed()方法来检查是否调用过rf.Kill(). 在所有的循环中都应该调用这个,来避免死去的raft实例打印出混乱消息。
(19)一个好的debug方式是在实例发送或接收消息时插入打印语句,并且将这些使用go test -run 2A > out输出到一个文件中。util.go中的DPrintf()方法在debug不同问题时作为打印开关很有用。
(20)Go RPC只发送名字大写字母开头的结构体变量。labgob包会提醒这些。
(21)用go test -race检查代码,并修复它报告的任何冲突。

二、lab2A实现

1.完善raft结构体

代码中读取term、currentTerm、votedFor等可以读写的变量时要加锁,否则出现不一致。

//
// A Go object implementing a single Raft peer.
//
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()

   // Your data here (2A, 2B, 2C).
   // Look at the paper's Figure 2 for a description of what
   // state a Raft server must maintain.
   currentTerm       int           //当前任期
   heartBeatInterval time.Duration //心跳时间间隔
   state             RaftState     // 0表示为跟随者 1为候选者 2为leader
   votedFor          int           // 为哪个实例投票
   log               []LogEntry    // 同步的日志
   commitIndex       int           // 提交日志的index
   lastApplied       int           // 已应用日志的index
   nextIndex         []int         // 所有实例下一个需要同步的日志
   matchIndex        []int         // 所有实例匹配的日志
   //lastResponse      time.Time     // 上一次收到leader响应的时间(用于管理选举超时)
   electionTime time.Time //选举超时时间,是一个时间点
}

2.Make方法,返回raft实例

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.state = Follower
   rf.currentTerm = 0
   rf.votedFor = -1
   rf.resetElectionTimer()
   rf.heartBeatInterval = 50 * time.Millisecond

   n := len(peers)
   rf.nextIndex = make([]int, n)
   rf.log = make([]LogEntry, 0)
   rf.log = append(rf.log, LogEntry{0, 0, -1}) //增加第一条日志,此处是为了防止发送请求投票的RPC请求时,传入的最后一个日志下标不合法
   rf.matchIndex = make([]int, n)

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

   // start ticker goroutine to start elections
   go rf.ticker()

   return rf
}

3.ticker方法及相关实现

创建raft实例时调用的ticker方法:
周期性的判断是否应该进行选举(400ms内没有收到心跳信息)
下面的代码有两个坑:①不要在循环中写defer解锁,不然可能会死锁。②不要这里在加锁的代码中写RPC请求,不然可能会一直发送。具体分析参考另外一篇博客lab2A调试过程中错误记录

// The ticker go routine starts a new election if this peer hasn't received
// heartsbeats recently.
//在该协程中周期性的检查是否大于electionTime时间点,如果超时,触发选举
func (rf *Raft) ticker() {
   for rf.killed() == false {
      //间隔心跳周期检查状态
      time.Sleep(rf.heartBeatInterval)
      rf.mu.Lock()

      // Your code here to check if a leader election should
      // be started and to randomize sleeping time using
      // time.Sleep().

      if rf.state == Leader { //如果当前实例是Leader,发送心跳信息
         rf.appendEntry(false)
      }

      //timeNoResponse := time.Now().Sub(rf.lastResponse)
       如果当前实例间隔electionTimeout ms以内没有收到心跳,则开始选举流程(这里每个实例都选择的不同值,防止同时发起选举)
      //var electionTimeout int64 = (int64)(rand.Intn(100) + 300)
      if time.Now().After(rf.electionTime) {
         rf.leaderElection()
      }
      rf.mu.Unlock()
   }
}

(1)ticker方法中appendEntry的实现

心跳信息和发送日志信息用的是同一个方法appendEntry,参数表示是否有日志,如果日志为空参数为false,表示该RPC消息是心跳信息。

//appendEntry Leader用该方法发送日志或心跳信息,hasLogs为true表示是日志
func (rf *Raft) appendEntry(hasLogs bool) {
   for peer, _ := range rf.peers {
      if peer == rf.me {
         rf.resetElectionTimer()
         continue
      }
      if !hasLogs {
         args := RequestAppendEntryArgs{
            Term:     rf.currentTerm,
            LeaderId: rf.me,
         }
         reply := RequestAppendEntryReply{}
         go rf.leaderSendEntries(peer, &args, &reply)
      }
   }
}

(a)因为该段代码有用到锁mu,所以发送RPC的消息封装到一个协程中单独执行leaderSendEntries

//leader发送心跳,按照lab说明,使用一个单独长时间运行的协程(心跳信息不会大于每秒10次)
func (rf *Raft) leaderSendEntries(serverId int, args *RequestAppendEntryArgs, reply *RequestAppendEntryReply) {
   ok := rf.sendRequestAppendEntry(serverId, args, reply)
   if !ok {
      DPrintf("[leaderSendEntries]sendRequestAppendEntry failed, leader id is : %d, term is : %d, target server id is : %d\n", rf.me, rf.currentTerm, serverId)
      return
   }
   rf.mu.Lock()
   defer rf.mu.Unlock()
   if reply.Term > rf.currentTerm {
      rf.currentTerm = reply.Term
      return
   }
}

(b)sendRequestAppendEntry封装了发送心跳的RPC请求

func (rf *Raft) sendRequestAppendEntry(serverId int, args *RequestAppendEntryArgs, reply *RequestAppendEntryReply) bool { // server是发送投票请求的目的实例id
   ok := rf.peers[serverId].Call("Raft.RequestAppendEntry", args, reply)
   return ok
}

RPC请求的实现(暂时只实现了接收心跳信息,还未实现接收日志的部分):

type RequestAppendEntryArgs struct {
   Term         int
   LeaderId     int
   PrevLogIndex int
   PrevLogTerm  int
   Entries      []LogEntry
   LeaderCommit int
}

type RequestAppendEntryReply struct {
   Term    int
   Success bool
}

func (rf *Raft) RequestAppendEntry(args *RequestAppendEntryArgs, reply *RequestAppendEntryReply) {
   rf.mu.Lock()
   defer rf.mu.Unlock()
   DPrintf("[%d]: (term %d) follower 收到 [%v] RequestAppendEntry %v, prevIndex %v, prevTerm %v", rf.me, rf.currentTerm, args.LeaderId, args.Entries, args.PrevLogIndex, args.PrevLogTerm)
   reply.Success = false
   reply.Term = rf.currentTerm
   if args.Term < rf.currentTerm {
      return
   }
   rf.resetElectionTimer()
   //if rf.state == Candidate {
   rf.state = Follower
   //}
   if args.Entries == nil { //说明收到的是心跳信息,直接返回即可
      reply.Success = true
      reply.Term = rf.currentTerm
      return
   } else { // 收到的是日志同步信息
      DPrintf("收到日志信息")
   }
}

(2)ticker中的leader选举leaderElection()方法

这里使用了sync.Once类型的变量保证成为leader的方法只执行一次。当前的实例作为候选人。

func (rf *Raft) leaderElection() {
   DPrintf("%d服务器开始成为候选者\n", rf.me)
   rf.state = Candidate
   rf.currentTerm += 1 //新的term
   args := RequestVoteArgs{
      Term:         rf.currentTerm,
      CandidateId:  rf.me,
      LastLogIndex: len(rf.log) - 1,
      LastLogTerm:  rf.log[len(rf.log)-1].LogTerm,
   }

   //先给自己投一票
   okSum := 1
   rf.votedFor = rf.me
   rf.persist() //投票信息要持久化,持久化方法暂时未实现
   rf.resetElectionTimer()
   var becomeLeader sync.Once //用sync.Once类型的变量保证切换为leader的方法只执行一次
   // 给其他的服务器发送投票RPC
   for i := 0; i < len(rf.nextIndex); i++ {
      if i == rf.me {
         continue
      }
      go rf.candidateRequestVote(i, &args, &okSum, &becomeLeader)
   }
}

(a) 封装了candidateRequestVote进行请求投票的RPC消息和返回值处理

func (rf *Raft) candidateRequestVote(serverId int, args *RequestVoteArgs, okSum *int, becomeLeader *sync.Once) {
   reply := RequestVoteReply{}
   ok := rf.sendRequestVote(serverId, args, &reply)
   if !ok {
      return
   }
   rf.mu.Lock() //接下来要比较返回的term和当前实例的term所以要加锁
   defer rf.mu.Unlock()
   //如果返回的term比实例本身的term还要大,说明当前leader已经过时
   if reply.Term > args.Term {
      DPrintf("[%d]: %d 在新的term,更新term,结束\n", rf.me, serverId)
      rf.setNewTerm(reply.Term) //这里面要把Leader状态改为Follower
      return
   }

   if reply.Term < args.Term {
      DPrintf("[%d]: %d 的term %d 已经失效,结束\n", rf.me, serverId, reply.Term)
      return
   }
   if !reply.VoteGranted {
      DPrintf("[%d]: %d 没有投给me,结束\n", rf.me, serverId)
      return
   }
   DPrintf("[%d]: from %d term一致,且投给%d\n", rf.me, serverId, rf.me)

   *okSum++
   if *okSum > len(rf.nextIndex)/2 && rf.currentTerm == args.Term && rf.state == Candidate {
      DPrintf(" [%d]: 当选为leader, term为%d\n", rf.me, args.Term)
      becomeLeader.Do(func() {
         rf.state = Leader
         //初始化两个数组
         lastLogIndex := args.LastLogIndex
         for i, _ := range rf.peers {
            rf.nextIndex[i] = lastLogIndex + 1
            rf.matchIndex[i] = 0
         }
         DPrintf("[%d]: leader - nextIndex %#v", rf.me, rf.nextIndex)
         //发送心跳
         rf.appendEntry(false)
      })
   }
}

(b)实现发送投票的RPC方法

func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool { // server是发送投票请求的目的实例id
   ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
   return ok
}

下面方法是投票实现的重点,要注意一定避免同一个实例在同一个term内同时给两个候选者投票,这里使用了判断votedFor的值来避免这种情况,除非候选者的term较大,否则已经投票过的实例不会再次投票。

//
// example RequestVote RPC arguments structure.
// field names must start with capital letters!
//
type RequestVoteArgs struct { //申请投票的RPC请求
   // Your data here (2A, 2B).
   Term         int //候选者任期
   CandidateId  int
   LastLogIndex int // 候选者的最后一条日志索引
   LastLogTerm  int //  候选者的最后一条日志任期
}

//
// example RequestVote RPC reply structure.
// field names must start with capital letters!
//
type RequestVoteReply struct {
   // Your data here (2A).
   Term        int  // 处理请求节点的任期号,用于候选者更新自己的任期
   VoteGranted bool // 候选者获得选票为true,否则为false
}

//
// example RequestVote RPC handler.
//
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
   // Your code here (2A, 2B).
   rf.mu.Lock()
   defer rf.mu.Unlock()

   voteTerm := args.Term
   candidateId := args.CandidateId
   reply.Term = rf.currentTerm
   reply.VoteGranted = false
   if voteTerm < rf.currentTerm {
      return
   }
   // 收到的请求比当前实例的任期大,更新自己的currentTerm,并设为非领导。但此时并不代表就会投票给候选者,还需要判断日志
   if voteTerm > rf.currentTerm {
      rf.currentTerm = voteTerm
      rf.votedFor = -1
   }
   // 在任期没问题的情况下,进一步判断日志来决定是否投票给候选者
   if rf.votedFor == -1 || rf.votedFor == candidateId {
      lastIndex := len(rf.log) - 1 //当前实例log日志最后一条的下标
      // 比较当前实例rf日志最后一条log entry和候选者的,如果rf的任期大或任期相同但rf的index大,则返回,不投票给候选者
      if rf.log[lastIndex].LogTerm > args.LastLogTerm || (rf.log[lastIndex].LogTerm == args.LastLogTerm && rf.log[lastIndex].Index > args.LastLogIndex) {
         return
      }

      rf.votedFor = candidateId
      rf.state = Follower
      reply.VoteGranted = true
      reply.Term = rf.currentTerm
      rf.resetElectionTimer()
      rf.persist()
      DPrintf("当前server:%d 投票给%d,任期为:%d\n", rf.me, candidateId, voteTerm)
   }
   return
}

三、lab2A测试结果

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值