在前两篇文章中已经介绍了Raft算法的正常流程和对异常的处理,但是还有一些问题没有解决,比如当集群中有新的节点加入或者退出的时候,集群又该如何保证安全的提供服务呢?

增删集群节点

  最容易想到的方法就是暂停整个集群,更新配置,然后重启集群。但是问题显而易见,在更新配置期间集群是不可用的,而手工操作配置文件,而且是操作多个节点的配置文件,也会造成很大的风险。为了避免发生这些风险,Raft算法添加了自动化配置变更的内容。
  从旧的配置直接变更到新的配置的各种方法都是不安全的,其中最大的问题就是容易出现脑裂集群分裂,举个例子;

  旧配置有 1, 2, 3号节点,候选人只需要两张选票就可以变为领导者,除了自己的一张选票,还需要等待一个节点投票给自己即可,但是当集群增加2个节点的时候,旧节点之间是无法感知有几个节点加入网络的,所以还会按照旧配置投票,即收集到两张选票就可以成为候选人。而新节点是可以感知到集群中是有5个节点的,所以新节点要成为领导者需要3张选票,必然有一个时间点,既可满足旧节点的候选人选举要求,又可满足新节点的选举要求,脑裂就这样发生了。

  显而易见,出现脑裂的问题是由于同一时间新旧配置节点各自单方面的作出了选领导者的决定。

  停集群,更新配置,重启集群其实目的就是保证了同一时间只有一种状态,为了解决集群的可用性,Raft采取了两段提交来保证安全的变更日志。

配置变更流程

  首先当一个领导者收到一个改变配置从C-oldC-new的请求时,它会merge(C-old, C-new)并且保存到自己的日志中,然后复制到集群中的其他节点,在C-new提交之前,所有节点的决定都会基于C-old,new的配置做决定。
  在C-old, new被提交以后,领导者创建一条C-new的配置复制到集群,当C-new被提交以后,就旧配置指定的节点就无关紧要了,在集群中不可见了,可以从集群移除。

异常处理流程

节点宕机

  但是在这个过程中还是会有节点宕机的异常情况发生,Raft又是如何保证整个增删节点过程的安全性呢?
  如果领导者在复制包含配置文件的日志时候崩溃了,跟随者节点只有两种配置状态,C-old,new或者C-old,主要观察C-old,new是否能被复制到了大多数节点。但是无论哪种状态,C-new都不会单方面做出决定。

空白节点加入

  当一个新的服务器加入集群,新服务器本身是没有存储任何日志,是无法提交集群中的任何一条日志的,需要一段时间来追赶,Raft为了避免这种可用性的时间间隔太长,采取了节点静默加入集群,但是没有投票权,只是同步日志,当新节点已经可以跟上集群日志的时候再投票加入集群。

旧节点干扰

  当C-new被提交以后,就需要移除不在C-new中的节点。在C-new被提交后,需要移除的节点就接收不到领导者的心跳消息,这个时候这些节点认为领导者可能出现了故障,会发起选举,正常执行的领导者收到投票请求后会退回到跟随者状态等待新领导者被选出,虽然最终正确的领导者会被选出,但是频繁的选举流程会扰乱集群的可用性。
  为了避免这个问题,Raft采用了最小选举超时时间的机制,当服务器在当前最小选举超时时间内收到一个请求投票 RPC,他不会更新当前的任期号或者投出选票,这样就避免了频繁的状态切换。

领导者不在新集群中

  还有一种可能是领导者不在新集群中,当配置文件从C-old,new变更到C-new时,领导者不在C-new中,这个时候就会在一段时间内发生旧节点管理新集群的情况。
  Raft中解决方法很简单,当提交C-new成功的时候,自己的状态变为跟随者,这样领导者节点就只能在新集群中选出。

日志压缩

  Raft算法在运行的过程中,日志是不断累积的,但是在实际的系统中,无论是从日志占用的磁盘空间,还是新节点加入集群,同步日志的网络消耗来看,日志都不能无限的增长。
  Raft采用快照的方法来压缩日志,快照时间点前的日志全部丢弃。

  每个服务器根据已经提交的日志,独立创建快照,快照中包含;

  • 状态机最后应用的日志;
  • 状态机最后应用日志的任期号;
  • 状态机最后应用的配置文件内容;

  领导者周期性的发送一些快照给跟随者,与领导者,保持同步的节点已经提交了快照的内容,会直接丢弃,而运行缓慢或者新加入集群的服务器则不会有这个条目,就会接受并且应用的自己的状态机中。