lab2A调试过程中错误记录

1.defer不要写在循环里

ticker方法中,defer写在了循环里,应该尽量避免。导致在当前raft实例未被杀死前,defer不会执行,所以一直保持锁,造成了死锁。

for rf.killed() == false {
	rf.mu.lock()
    defer rf.mu.Unlock()
}

2.加锁的代码不要写RPC请求

加锁的代码之间尽量不要写RPC请求,否则RPC请求不成功,会一直重试,占用锁,造成死锁。
解决方法:将相关方法抽出来单独写一个协程,不再出现RPC请求部分一直占用锁问题。
在这里插入图片描述

3.心跳间隔和选举超时时间设置

心跳间隔和选举超时时间需要反复试,不然特别容易选举超时。或者所有实例同时成为候选者,所以导致一定时间内,无leader选出.
解决方法:修改超时时间,原时间 time.Duration(300+rand.Intn(100)) * time.Millisecond,修改为 time.Duration(150+rand.Intn(150)) * time.Millisecond,原心跳间隔100ms改为50ms,该问题解决。
在这里插入图片描述
在这里插入图片描述

4. raft日志数组的初始化

raft实例初始化时,在日志数组中增加一条默认日志,防止后面获取最后一个日志下标时出现-1。
在这里插入图片描述

5.多个leader的情况

第2、3个测试案例,网络故障和多次选举时,总是会有选举出多个leader的情况导致测试失败。
修改方法:当一个实例接收到心跳信息RPC请求时,不论是Leader还是Candidate,都把它的状态改为Follower,修改后测试通过。
在这里插入图片描述

6.sync.Once类型变量的使用

参考其他人方法的实现,选举成为leader部分的代码使用sync.Once类型的变量保证只执行一次。之前忽略掉了这个部分的实现。
总结sync.Once类型为什么可以保证只执行一次呢?在什么时间粒度只执行一次,怎么保证同一个term内只执行一次,而不影响下一个term中的选举代码执行呢?

type Once struct {
  done uint32
  m    Mutex
}

Once只有一个Do方法,结构体内有一个done无符号整数,用来记录Do方法的调用次数,有一个锁m,来进行原子化操作。
源码中的Do方法实现:

func (o *Once) Do(f func()) {
  if atomic.LoadUint32(&o.done) == 0 { 
    // 原子获取 done 的值,判断 done 的值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。
    o.doSlow(f)
  }
}

func (o *Once) doSlow(f func()) {
  // 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。
  o.m.Lock()
  defer o.m.Unlock()
  if o.done == 0 {
    // 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。
    defer atomic.StoreUint32(&o.done, 1)
    f()
  }
}

通过两次判断(第二次加锁),保证原子化判断done值为0时执行Do的参数f方法。
从源码中可知,锁m和done值都属于创建的Once对象,所以sync.Once保证的是在同一个Once对象下只执行一次。
回到我们的lab2A中,在选举成功将实例状态改为Leader时,使用sync.Once变量。

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)
      })
   }

这里使用的becomeLeader是leaderElection()方法中的一个局部变量,所以是保证每一个实例只执行一次,这里主要是为了防止重置nextIndex和matchIndex数组为初始态。

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)
   }
}

所以,开始的问题答案是,因为once.Do方法使用的是选举方法中局部变量的对象锁,这里只能保证每个实例在本次选举中调用一次。而在每个term中调用一次不是用这个once变量保证的,是用前面的if方法过半数投票来保证的。
参考资料

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值