RAFT提供了一个颇具实践意义的分布式一致性协议的工程实现模板,具有很高的知名度。其大部分设计和viewstamp基本类似,而系统成员变更这一部分可以算作其真正的原创工作,但这一部分相比于论文其他部分,确实讲解最不详细的地方,最近在设计OCEANBASE的分布式系统,涉及到了成员变更的问题,仔细分析了RAFT的成员变更做法,和大家分享一下。
成员变更指的是系统成员变化,即server的上下线,这和由于宕机故障导致的上下线是不同的。宕机或者重启导致的上下线,是不会影响系统的注册的成员数量的,也就不会影响到一致性判断的所谓majority的生成,众所周知,majority是所有一致性的基础。成员变更时,会修改注册的成员数量,比如在实际应用中,为了提高安全等级,就很可能出现需要把备机数量由三台扩充到五台,在这种情况下,就发生了成员变更。
成员变更有多种实现方案,不论何种做法,都需要先将新成员网络接通,并把之前的日志数据同步给新成员,让其和现有leader日志保持同步。
完成这一基础步骤之后,就有多种选择了。比较丑陋的变更方案是,先停止写服务,然后写一条成员变更的同步给原有集群的majority,让原有集群下线,然后启动新集群,提供写服务。这种实现是简单,但可用性不高。
RAFT中给出的方案,提出了通过一个中间过渡阶段,joint consensus,逐步把数据写入的新的group中。
其具体做法是2阶段提交式的:
- 先写一条<Cold, Cnew>同步到新旧两个group的多数派,写入这条日志后,系统中的任何写入请求,都要同步到Cold和Cnew 两个group的多数派才算写入成功
当<Cold, Cnew>同步成功后,再写一条<Cnew>,同步给新group,然后就可以完成切换了。
这个过程看起来比较复杂,其实从宏观来理解,可以把Cnew,看作原有集群的一个热备,在joint consensus阶段,一个请求要写入原有系统和热备,之后,不论<Cold, Cnew>这条日志是否写成功到多数派,不论热备出现了何种故障,原有系统会一直保证数据是一致的。
当写入<Cnew>到多数派备机成功后,就保证了新group和备有了一致且完备的数据,在这种情况下,新group就可以接替原有集群工作了。
再深入考虑下,<Cold, Cnew>和<Cnew>写失败的处理方式:
- 假设<Cold, Cnew>写失败,需要再写入<Cold>,回滚那些写入成功的少数派。如果在这之前,发生了重新选主,新主不论如果已经收到了<Cold, Cnew>,则其会将这条日志同步给备机,并将成员变更继续完成;如果新主没有收到<Cold, Cnew>,则这条日志会被覆盖,因此不会有任何问题。
- 如果<Cold, Cnew>已经写成功,而<Cnew>写失败,需要再写一条<Cold>将之前操作回滚。如果在回滚成功前,发生了重新选主,新主如果没有<Cnew>,则会覆盖<CneW>日志,继续处于joint consensus状态,如果有<Cnew>,则会将日志同步给新集群,然后进入一个新集群状态。 此外,还有可能新集群中和老集群中都出现一个leader的情况,但此时,老集群中的leader由于还处于joint
consensus状态,即必须把日志同步给新集群中的多数派才会成功,而此时若新集群已经有主,是不会应答老集群的日志同步请求的。
RAFT中成员变更过程以及失败回滚分析