WQhuanm
我写的代码明明是有BUG的,为什么他跑了100遍也没有出现Fail啊~~~,唉,烂完了

我写的代码明明是有BUG的,为什么他跑了100遍也没有出现Fail啊~~~,唉,烂完了

最近在做MIT6.824的实验,raft的实现真是让人切身体会到并发情况的多样性!
我本以为只要代码让脚本持续跑一个100遍测试数据,没有问题,应该,大概率,99.99999%是没问题的吧!!! 对的,我昨天还是这么想的,我甚至今晚之前也是这么想的,直到刚才重构检测完毕后重复测试的40+次!

但是出现了一个几乎不可能出现的并发问题,终究还是出现了,我运气还是好的,测出BUG了(苦笑)

当时对raft理解的一些误区:

  1. 我原本自作聪明的认为节点只需要记录所属term是否投票即可,无需记录投票给谁,这是我当时对votedfor的理解
    • 所以当时的策略是votedFor是bool变量,加锁控制term更新时对votedfor的重置
  2. 我认为每个peer节点,只要接收到的RPC请求/响应结果携带的term比自身大,就应该立刻将自身term更新为对方更大的term(同时执行相应的切换策略)

正是上述误区产生如下BUG:

  1. followerA未及时接收到Raft心跳,将自身term升级为termA并发起了投票请求
  2. followerB发现对方请求携带的term比自身大,将自身term切换为termA
  3. 不久后(大概10ms左右,很神奇吧),未及时收到leader心跳的followerB,定时器触发,将自身term升级为termB后(termB>termA),也发起了选举!!!
  4. 并且,此时的termA还未选举完成,然而他收到了termB的投票请求
    • A发现对方term比自己大,切换自身term为termB,votedFor重置为false
    • A升级后,甚至给B的投票请求投了赞成票,理所当然的!(x
  5. 最终双方都结束了选举,A,B均成为了处在termB的leader,违背了raft无论何时每个term只能有一个leader的原则
    • 因为A实际是termA的leader

排查许久发现这个问题我,我深刻的写下了问题记录和修改方案,此时看见这最让人崩溃的一幕:

  1. 左下角是我重新执行脚本跑我那BUG代码,想要看看还有没有其他BUG的运行结果。
    • 然而它居然过了!!!100遍!居然给他跑过了!
    • 我写的代码明明是有BUG的,为什么他跑了100遍也没有出现Fail啊~~~,唉,烂完了
  2. 左上角是无情的日志输出,一个Fail也没有出现~
  3. 右上角是我更改写完的问题记录和修改方案
  4. 右下角,是我装死的女鹅~~~,我也想似了

确定出问题后,解决方案也很容易得出了(毕竟难的是找出问题,或复现出问题本身),幸好我日志记录完善,不用再进行一次复现(它可是足足跑了100遍,都没有问题啊!)

解决方案大致如上图:

  1. 发起选举时,记录自身当前的term(选举时使用的term)
    • 如果投票通过后,发现自身当前的term和选举term不一致,则不进入leader
  2. votedfor改为:记录其投票过的最大term(这应该才是真正的含义),votedfor做出承诺,不会向低于或等于当前投票的term的请求进行投票
  3. 在成功向对方投票时才尝试更新自身term(不满足要求的投票请求,不应该用来更新term)
  4. term的更新需要获取锁,leaderinit时也要获取该锁避免term更新,这样term更新时,如果是leader,才能降级为follower
本文作者:WQhuanm
本文链接:https://wqhuanm.github.io/Issue_Blog/2025/11/20/50_我写的代码明明是有BUG的,为什么他跑了100遍也没有出现Fail啊~~~,唉,烂完了/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可