基于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),一定会选主成功,并且拥有最新的数据。

正确理解成员变更的原理

  • 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个。

 

Raft 优化点

 

  • 静默模式

通常分布式集群环境中,复制组的实例数量比节点数量高几(2)个数量级。 leader 向follower的心跳通常用作主动leader election,一般这个超时时间设置为100ms粒度,如果有N个复制组,这个心跳相关的RPC数量就是:N*2(向除自己之外的副本)发送。但这个功能也可以通过外部主动发起trasnfer leader/election 来实现:业务主控节点监测每个节点都心跳,一旦宕机触发向其它节点transfer leader。因此, leader 向follower的心跳在复制组数量很多的环境中可以关掉。

  • 提供外部transfer leader 功能的API

业务场景种,有的节点上不能有主副本,有的节点上又适合放置主副本,此时,就需要把leader 迁移到指定节点上,这就是transfer leader的功能。

  • 外部vote不需要等election timeout超时

对于外部主动发起的vote()请求,通常是知道老主所在的节点已经宕机或者不可用了,这时应该尽快发起选主,不用等待。

  •  慢节点优化

三副本中,如果leader刚好是慢节点,且又要求 leader append log 之后在replicate 到从节点,形成多数才能返回,这样leader 所在的慢节点就很容易成了复制组中的瓶颈。对于这种情况,慢节点的leader可以在两个从 成功返回之后,直接利用还在内存(缓存,buffer)中的log entry 进行apply 。

  • 读的时候利用从节点进行读

首先理解committed 状态:raft log 复制到多数副本后、执行对应业务FSM之前的状态

其次理解applied状态:raft log 复制到多数副本后且对应业务FSM向前推进后的状态

读的时候带上对应复制组的committedId , 所在follow 上的log index 小于committedId的raft log,可以先变成applied状态,然后提供读取。这里需要特别强调的是:上面是没有考虑leader change的状态,如果考虑leader change, committed 状态还需要加上后面有新的log entry 被复制到多数节点的限制要求。

  • 批量写入

复制组直接可能会有元数据相关的log replication,如果每次都fsync , 势必影响性能,对这些数据可以攒到一起落盘。