【MIT 6.824 Lab2】——Raft

Lab2 Raft


因为以前的版本很多细节出现问题,导致后面的lab4出现很多bug,所以对raft进行重写。

PartA

本部分需要完成Raft的leader选举和发送心跳包(log为空的AE)。

  • 实验流程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyInNatp-1654832766292)(MIT-6.824.assets/image-20220309154651021.png)]

  • 对象设计

    本次实验的主要对象有Raft,ApplyMsg,LogEntry。其中Raft代表了一个Raft对等节点,LogEntry则代表日志。

    • Raft

      每次进行leader选举,每个对等节点peer需要从自身的Raft对象中读取自身的term,以及最后一条日志的信息,然后将自己的投票结果存在votedFor中。在不同的阶段,peer将自己的角色存储在role中从而执行不同的任务。每个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()
      
      	// 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        //当前任期
      	votedFor         int        //记录当前投票的服务器
      	logs             []LogEntry //日志
      	commitIndex      int        //提交的日志编号(优化)
      
      	//易失变量
      	lastApplied int   //已应用的日志下标
      	nextIndex   []int //follower中的同步起点,初始为lastLogIndex+1,server->Index
      	matchIndex  []int //follower已同步的索引,初始为0,server->Index
      
      	//其他
      	currentState string //当前状态,leader,follower,candidate
      	grantVotes   int    //获得的票数
      
      	//选举计时器
      	electionTimer     *time.Timer
      	electionTimerLock sync.Mutex
      
      	//心跳计时器
      	heartbeatTimer     *time.Timer
      	heartbeatTimerLock sync.Mutex
      
      	applyCh   chan ApplyMsg //应用层的提交队列
      	applyCond *sync.Cond    //唤醒应用消息的进程发送信息,在commit时唤醒
      }
      
    • LogEntry
      日志标号可以通过快照(2D)存储的编号加上当前日志数组的下标计算得到。

      type LogEntry struct {
             
      	Term    int         //该日志所处的任期
      	Command interface{
             } //该日志包含的命令
      }
      
  • 流程设计

    • 初始化Raft对等节点

      Raft节点启动主要需要将初始化term为0,设置为follower,并且为日志分配足够的空间,投票人初始化为未投票状态,以及重启计时器。并且初始化随机种子,用于随机选择选举计时,随机种子在初始化时选择一次就行,不用在每次随机选举时再选。

      	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.logs = make([]LogEntry, 0)
      	rf.commitIndex = 0
      	rf.nextIndex = make([]int, len(peers))
      	rf.matchIndex = make([]int, len(peers))
      	rf.currentState = FOLLOWER
      	rf.applyCh = applyCh
      	rf.applyCond = sync.NewCond(&rf.mu)
      
      	rand.Seed(time.Now().Unix())
      
      	rf.electionTimerLock.Lock()
      	rf.electionTimer = time.NewTimer(time.Duration(rand.Int())%electionTimeoutInterval + electionTimeOutStart)
      	rf.electionTimerLock.Unlock()
      
      	rf.heartbeatTimerLock.Lock()
      	rf.heartbeatTimer = time.NewTimer(heartbeatInterval)
      	rf.heartbeatTimerLock.Unlock()
      	// initialize from state persisted before a crash
      	go rf.electionTicker()
      	go rf.appendEntriesTicker()
      	return rf
      
  • 随机数补充
    之前出现一个bug选举经常出现碰撞导致不能选举出leader,后来发现是因为在重置选举计时器时又重置了随机种子,go语言的随机是伪随机数,如果随机种子相等则第一个数是相同的,而后面再出现的数会不一样,所以在初始化每个peer的第一轮选举超时时间是一样的,但后面的就不一样了,而如果每次重启计时器就重置随机种子,因为第一轮超时时间相同会发生碰撞且在相同的时间发起第二轮,所以这时的重新生成的随机种子又是一样的,导致又是相同的超时时间,也就导致一直碰撞选不出leader。下面是对rand的测试。

     package main
    
    import (
    	"fmt"
    	"math/rand"
    	"time"
    )
    
    func main() {
         
    	for i := 0; i < 5; i++ {
         
    		go func(x int) {
         
    			rand.Seed(1)
    			fmt.Println(x, rand.Int())
    			fmt.Println(x, rand.Int())
    		}(i)
    	}
    	time.Sleep(3 * time.Second)
    }
    //结果如下,可以看到第一轮是相同的,第二个就不一样了。
    4 5577006791947779410
    4 8674665223082153551
    0 5577006791947779410
    0 6129484611666145821
    3 5577006791947779410
    3 4037200794235010051
    1 5577006791947779410
    1 3916589616287113937
    2 5577006791947779410
    2 6334824724549167320
    

    在当初遇到这个bug我考虑在进入选举goroutine之前打印日志查看,结果2A跑通了,而如果不打印就又会选举不出来,后来猜想是因为多个goroutine调用fmt函数导致每个peer执行选举函数有了先后顺序,先打印先执行选举的可以成为leader,即使超时时间相同,定时器几乎同时返回,但是由于在启动选举之前共同调用fmt会发生冲突,就会强制有一个先后的顺序,在后面的等待前面的fmt打印的时候,可能更前一个的peer已经发来了投票请求。为了验证这个想法,我直接把重置的超时时间设为一个定值,然后在超时调用选举函数前加上一个fmt,然后测试2A还是能够成功。

    	//将重置选举时间改为定时长度
     	func (rf *Raft) resetElectionTimer() {
          	
    	rf.electionTimer.Reset(electionTimeOutStart)
    }
    
    	//在开始选举前打印一个.
     	func (rf *Raft) electionTicker() {
         
    	for rf.killed() == false {
         
    		<-rf.electionTimer.C
    		fmt.Print(".")
    		go rf.startElection()
    		rf.resetElectionTimer()
    	}
    }
    

    2A测试通过

    Test (2A): initial election ...
    ............  ... Passed --   3.7  3   62   15244    0
    Test (2A): election after network failure ...
    .............................................  ... Passed --   7.1  3  186   33678    0
    Test (2A): multiple elections ...
    .........................................................................  ... Passed --   7.1  7  816  145568    0
    PASS
    ok      6.824/raft      18.139s
    
    
  • leader选举

    计时器超时后就会进入选举

    func (rf *Raft) electionTicker() {
         
    	for rf.killed() == false {
         
    		<-rf.electionTimer.C
    		go rf.startElection()
    		rf.resetElectionTimer()
    	}
    }
    

    如果自身就是leader直接退出选举过程,否则需要进入竞选人candidater。因为需要修改raft的信息所以需要在执行前上锁,防止两个peer在RPC通信时互相持有锁导致死锁。

    	rf.mu.Lock()
    	//注意RPC调用前要释放锁防止多个拥有锁的peer之间形成死锁
    	//这里能够使用defer释放是因为这里的rpc都使用的是开辟新的goroutine异步调用,所以主函数可以快速释放
    	defer rf.mu.Unlock()
    	if rf.currentState != LEADER {
         
    	//竞选
    	}
    

    开始选举首先需要将投票给自己,并且获得的票数也加一。然后将任期加一作为竞选Leader的新一轮term,然后准备发起新一轮选举的RPC请求,请求包含了新的term,自己的id,以及自己的最后的一个日志下标和term,这两个信息主要是为了确保竞选成功的peer拥有最新的日志(term最大或者term相同但是日志更长)

    rf.currentState = CANDIDATE
    	rf.votedFor = rf.me
    	rf.currentTerm++
    	rf.persist()
    	//DPrintf("[StartElection] Server:%d In term:%d try to elect", rf.me, rf.currentTerm)
    	rf.grantVotes = 1
    	args := RequestVoteArgs{
         
    		Term:         rf.currentTerm,
    		CandidateId:  rf.me,
    		LastLogIndex: rf.getLastIndex(),
    		LastLogTerm:  rf.getLastTerm(),
    	}
    

    通过RPC发送RequestVote给各个对等点,这里采用开启多个goroutine异步发送,加快效率和减少阻塞。然后根据接收结果进行下一步的处理。

    for server := range rf.peers {
         
    	if server == rf.me {
         
    		continue
    	}
    	go func(server int
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值