基于Raft 的分布式一致性协议是构建很多分布式服务的基础,某种程度上它充当了心脏的角色,为此有必要对Raft 的一些难点进行深入理解。
正确理解commited
一个常见的误解就是复制到多数副本的就可以视作commited, 其实还不够。缺少必须已经执行了对应的操作这个步骤。个人理解在实际 Raft使用过程中,就是存在某个Raft log 在append到多个副本的瞬间宕机了,由于还没有执行on_apply() ,其实还没有向上层用户ACK 这条日志已经成功。
因此这条日志可以truncate, 也可以后续随着后续的log commited 之后自己也被commited。
正确理解选主的几个比较条件
为什么需要把距离上次主更新的时间和election_timeout 比较
这个比较大规则如下:
某个follower 收到 vote 请求之后,会计算出收到请求的时间和上次收到主心跳的时间间隔,然后把这个间隔和一个约定的较小时间进行比较。如果前者还小(此时说明还有其他节点长时间没有收到主的心跳,发起了vote),拒绝这次vote 请求,否则投赞成票。
考虑三个互联互通的IDC A、B、C,B是主副本,如果由于某种原因B、C网络不通了,如果没有上面限制,B发起带有最高term信息的vote请求,A会同样vote请求,这样主就从B变成了C。同理,过了以后,主又很可能从C再变成B。如此 反复循环,整个复制组上没有办法提供持续稳定的主。
为什么需要使用LastLogIndex 和LastLogTerm进行新旧log的比较
这个规则如下:
某个follower 收到 vote 请求之后,在current term和请求中的term 相等的情况下:需要使用当前节点的LastLogIndex 、LastLogTerm和请求中的LastLogIndex、LastLogTerm进行比较,如果前者大,则拒绝请求;如果后者大,则同意请求。
考虑下,为啥要这样?还是考虑上面三个互联互通的IDC A、B、C,B是主副本,如果由于某种原因B、C网络不通了,C是划分节点。当网络划分之后,C的current Term是最高的,它很可能发起vote,这样把B Stepdown了,A和B的currentTerm 提升了;可是因为C的LogIndex很低,它发到A和B的Vote 请求,在同A、B的Last LogIndex 比较时发现自己太小,而没法拿到A、B的信任票,因此Vote失败。随后A或B再发起Vote , 就是用最高的Term(C的Term+1 )、最新的LastLogIndex(A 或B的LastLogIndex),一定会选主成功,并且拥有最新的数据。
正确理解成员变更的原理
Raft采用的成员变更单策略相对简单,每次只增删一个节点,这样就不会出现两个多数集合,不会造成决议冲突的情况。按照如下规则进行处理:
- Leader收到AddPeer/RemovePeer的时候就进行处理,而不是等到committed,这样马上就可以使用新的peer set进行复制AddPeer/RemovePeer请求。
- Leader启动的时候就发送AddPeer请求,防止上一轮AddPeer没有完成commit。
- Leader在删除自身节点的时候,会在RemovePeer被Committed之后,进行关闭。
按照上面的规则,可以实现安全的动态节点增删,因为节点动态调整跟Leader选举是两个并行的过程,节点需要一些宽松的检查来保证选主和AppendEntries的多数集合:
- 节点可以接受不是来自于自己Leader的AppendEntries请求
- 节点可以为不属于自己节点列表中的Candidate投票
为了避免同时有两个节点变更正在进行,在有未committed的change正在进行的时候,不允许进行节点变更。节点变更有一个问题,对一个只有两个节点的Cluster,发起RemovePeer。这个时候一个节点挂掉,另外一个节点没有收到RemovePeer请求,这样系统将停止工作。因此强烈建议集群节点数>=3个。