由于raft集群的server总是在三种状态之间切换,不同状态执行不同的任务,因此将使用状态机来实现。server之间互相发送的包是心跳包和requestVote包以及它们的reply。
leader:【发送心跳包给follower和candidate,收到不合法的心跳则拒收】向集群中其他所有成员定时发送heart beat,确认存活,同时接收其他成员反馈的reply信息。对于reply信息,有多种情况:
reply.Success = true:成员承认本leader;
reply.Success = false:成员拒绝承认本leader。原因是该成员的term>leader.term,本leader的任期已过,集群已经在新的term了。于是这台机器退位,降级为follower,并更新自身的term等信息,保持与集群同步。
follower:【从leader接收心跳包,从candidate接收requestVote包】
接收投票要求
如果投票的term大于自己,说明有人发现leader挂了,在发起新一轮的投票,投票,同时视为收到了心跳;
否则,拒绝投票,并且告诉通过reply.term告知candidate本机认为当前所处的term;
接收心跳:重置心跳超时计时器;
检查是否心跳超时:若超时,成为candidate,并且立即发起投票;
candidate:【从leader接收心跳,发送requestVote包给follower和其他candidate,从其他candidate接收requestVote包】
发起投票。对于投票结果:
若超过半数同意,则立即成为leader并且执行leader任务;
若有人拒绝:查看reply.term,如果reply.term>=自己,说明是自己out了,降级为follower,取消本轮投票;否则就是单纯的不投我,那就算了;
检查投票是否超时,若超时,重新发起投票;
接收心跳,如果在投票过程中收到term>=自己的心跳,说明现在已经有leader了,降级到follower状态,取消本轮投票。
关于投票取消的时候可能发生的异常讨论
follower同意投票的同时,将term更新,立即视为进入了新的term并且将这个candidate视为当前term的leader,这是没有问题的。如果candidate选举成功,显然是没问题的;如果candidate选举不成功,即,取消投票,有以下情况:收到reply.term>=candidate.term的选举回复,说明系统正在试图开启更大的term;收到term>=自己的心跳,说明当前系统中正处于更大的term,并且已经处于有leader的稳定状态。不论是试图开启还是已经达到,当这个更大的term达到稳定的状态时,其leader会发送心跳,心跳的term大于candidate的term,投票给candidate的server不会拒绝这些心跳,并且会立即响应进入新的term,从前的错误投票在新的term下毫无影响。
附:检测和修复data race https://www.sohamkamani.com/golang/data-races/
——————————————
(重构)
对于一台server,需要做的事情有三个方面:选举、日志复制、apply。其中,选举和apply两项是所有server都主动进行的,因此在初始化的时候使用两个goroutine来控制,日志复制应该是由client调用start来控制进行的。
timeout一直在倒计时,一旦超出了倒计时就称为candidate开始选举,倒计时期间,可能由于收到leader或者任期更大的server的消息而reset倒计时。
logApplier不断地推动lastApplied追上commitIndex,通过发送ApplyMsg给applyCh通道接口来apply日志,如果已经两者已经一致了,就wait直到有新的commit。如何检查有新的commit呢?可以使用sync.cond条件变量,等commitIndex更新的时候用broadcast唤醒这个cond,从而疏通堵塞。
附: 关于sync.cond https://ieevee.com/tech/2019/06/15/cond.html
【选举】
投票条件:
候选人最后一条Log条目的任期号大于本地最后一条Log条目的任期号;