前言
前两篇文章分别解析了Raft中领导人选举和日志复制规范,对于一个固定的集群来说,有了这些规范的保护,就可以快乐的一致运行下去了。随着业务的变化,总归会出现集群规模变更的需求。最简单的处理方式当然是停掉整个集群服务,变更配置,重启服务。有时候,这对于在线业务是无法接受的。如果不停止服务,直接变更又存在问题,比如下图添加节点的情况:
开始只有Server1-3,这时候我们添加2个节点,但是配置变更不可能在所有节点上同时生效。如果在箭头所在的时间点刚好触发选举,server1 和server 2的保存的配置还是只有3个节点的集群,所以server1可以通过自己和server 2的选票成为Leader。而Server5通过3、4、5的选票也超过半数,也可以成为Leader。造成存在2个Leader的错误。
Raft的解决方案
Raft协议通过在新旧配置变更中间添加过渡阶段来处理这个问题,称之为联合共识(joint consensus)阶段。在联合共识阶段,集群会做如下的约束:
- 日志条目被复制给集群中新、老配置的所有节点
- 新加入节点和原有节点都可以成为 leader
- 达成一致(针对选举和提交)需要分别在两种配置上都超过半数
满足了上面的条件,集群就可以在切换的同时仍然对外提供服务。下面用具体场景来看下Raft的解决方案。
添加节点
当前有一个3个节点的集群,现在要往集群中添加2个节点。Raft通过发送日志的方式来发送配置变更日志指令。
发送联合共识日志
Leader首先向集群中的节点发送一条新的日志,日志内的指令就是配置变更。这条日志跟普通的日志一样复制给Follower节点,但是提交后不会对状态机的数据有任何改动,因为它不是数据操作指令。当前的状态如下图:
上图中C(o+n)的日志就是配置变更日志,告诉其它几点集群需要进入old和new共存的联合共识阶段。跟处理普通的日志条目不一样,节点在收到C(o+n)的日志后,立马就进入联合共识阶段,而不需要等到Leader提交这条日志。也就是说,上图中S1,S4和S5进入联合共识阶段,而S2,S3因为还没收到日志,所以还处于旧配置阶段。
按照前面讲的,进入联合共识阶段后,Raft要求任何决议的达成必须在新老配置中都达成半数。对于上图中的S1来说,因为它已经处于联合共识阶段,所以如果它要将配置变更的日志提交,必须在老的集群(S1,S2,S3)中超过半数,在新的5个节点的集群中也要超过半数。上图中日志已经复制到3个节点,满足新集群中超过半数的要求,但是在老的3个节点的集群中未超过半数,所以这条日志现在还不能提交。
提交联合共识日志
Phase2的图中,Leader提交了C(o+n)日志,现在超过半数节点都运行在联合共识阶段,即使Leader崩溃,新选出Leader的集群也仍然会运行在联合共识阶段。这时,Leader会复制一条新集群生效(图中的C(new))的日志到集群中,告诉Follower节点现在可以切换到新的集群模式下运行了。跟C(o+n)一样,Follower也不需要等到Leader提交C(new),只要一收到日志,可以立刻切换到新集群。这里有一点需要注意,在C(o+n)提交和复制C(new)日志的这一阶段,集群仍然是正常对外提供服务的,就是说在C(new)日志发出之前,客服端发给Leader的指令可以正常提交,只是提交的条件是日志被复制到新老两个集群的超过半数节点。
提交新集群日志
在开始复制C(new)日志后,Leader已经运行于新的集群中,所以只要C(new)被复制到不少于3个节点,就可以提交了。之后整个集群的扩容就完成了。
扩容安全性
- 在上面的Phase 1阶段,如果Leader崩溃了,会不会出现多个Leader?
Raft协议中规定,对于配置变更的日志,不需要等到提交,各个节点只要收到了就按新的配置运行。在Phase 1阶段,如果S1崩溃了,S2和S3处于旧配置阶段,假设它们中的S2变成候选人,收到S3的选票即超过半数,当选Leader。S4和S5处于联合共识阶段,假设S4变成候选人,那么它必须同时在新旧两个集群超过半数。假设现在S1崩溃后迅速重启并加入集群,那么在新集群中,S4可以收到S1和S5的选票,超过半数;在旧集群中,收到S1的选票,但是无法收到S2或者S3的选票,因为在同一轮选举中,S2和S3已经把票投给了S2。所以,S4无法赢得旧集群的选举,不会出现两个Leader的情况。 - 在Phase 1阶段,如果Leader崩溃,新当选的Leader会继续提交C(o+n)吗?
答案是不一定。如果是S2当选Leader,因为它没有C(o+n)的日志,按照上篇文章讲到的日志复制规范,S4和S5上的C(o+n)会被删除,所以不会提交。客户端必须重新发送配置变更指令。如果是S4当选Leader,会继续复制C(o+n)的日志,配置变更流程会继续进行。这两种情况都是符合Raft规范的。 - 在Phase 2阶段Leader崩溃了,已经提交的C(o+n)会受到影响吗?
不会。Raft协议承诺已提交的日志永久生效。S1在Phase 2崩溃,在老的集群节点中,因为S2有最新的日志,所以只有S2可以当选新的Leader。在新加到集群的节点中,无论是S4还是S5成为新的Leader,都会继续复制和提交C(o+n) -
在Phase 3中,按照Raft协议Leader只要把C(new)日志发出,后续的客户端指令就可以按新配置来提交,会不会造成提交的日志丢失。
答案是不会,如下图所示,S1发出C(new)的日志后,收到客户端日志:
这时候新日志被提交的条件是在新的集群中,超过半数节点确认收到日志,因为Raft要求日志必须是按顺序复制,所有收到新日志的节点必然也收到了C(new)的日志,如下图:
如果Leader在此时崩溃,S3和S5按照新旧集群都超过半数的原则进行选举,S2和S4按照新集群超过半数的原则进行选举。只有S2和S4可以当选,因为只有它们才有最新的日志。所以C(new)后提交的日志不会丢失。
删除节点
对于集群缩容的情况,由于缩容后的集群中可能不包含当前Leader节点,所以复杂度进一步上升,我们解析下Raft如何应对这种情况。
当前有一个5个节点的集群,现在将集群缩容至3个节点,如下图所示:
上图中,旧集群S1是Leader,新集群中将会把S1和S5移除。首先Leader发送C(o+n)日志至其它节点,当超过半数节点确实收到后,将C(o+n)提交。然后,Leader开始发送C(new)至其它节点,按照Raft的规范,只要C(new)日志一发出,新配置在Leader上就生效了。S1虽然已经不属于新集群,但是因为C(new)还没提交,所以S1仍然行驶Leader的职责,只是计算超过半数的时候,它自己不计算在内。如上图中的状态,S1已经将C(new)复制到2个节点,但是还不能提交,因为它自己不是新集群中的节点,不计算在内。直到S1将C(new)发送到新集群(S2,S3,S4)的超过半数节点,就可以提交C(new),提交之后S1就退位了,之后由新集群中的节点重新选举Leader。
缩容安全性
在上面的图中,如果客户端发来新的日志如何提交?
如果在S1复制C(new)之后提交C(new)之前,收到客户端的日志,S1工作在新的集群配置下,所以会将日志复制到新集群的节点上,当收到新集群(不包含S1)超过半数节点确认后,就可以提交日志。所以在缩容期间集群仍然可用。
可用性
Raft在集群配置变更时,为了提高可用性,还有以下两个问题需要解决:
- 新节点同步数据期间无法提交新日志,造成集群短时间不可用
这个问题发生在有新节点加入集群,并且初始化数据较多,从Leader同步数据需要时间很长的情况下。因为Raft中日志必须按顺序同步,在老的日志没有同步完成之前,不会接收Leader提交的新的日志。导致客户端新发来的指令迟迟达不到半数,无法提交。
针对这种情况,Raft规定在新节点同步数据期间,可以以没有投票权的方式加入进来(这里类似于ZooKeeper中的Observer节点),比如一共5个节点,有2个节点在同步数据阶段,那Leader可以按3个节点的集群来组织投票。等到数据同步完成后,再正式按正常节点来处理。 - 被删除的服务器在Leader进入C-new阶段后将不再收到心跳,会触发选举使Leader失效。
这就是上面讲的删除节点时图1中的情况。S5将要被集群移除,Leader在发送C(new)之后就进入新集群阶段,不会向S5发送心跳和日志。S5在等待一段时间后没有收到Leader的心跳和日志,会以为超时从而将自己作为候选人向其它节点发投票申请。对于图1中的S3和S4来说,因为还没收到C(new),可以当成是新一轮的选举开始,从而使新集群进入选举阶段。
为了避免这个问题,Raft规定,当节点确认当前领导人存在时,服务器会忽略请求投票的 RPCs。确认领导人存在的方法就是,收到投票RPC的时间距离上次收到Leader心跳或日志的时间还不到最小选举超时时间,那可以拒绝这个请求。最小选举超时时间请参考之前的Raft详解(一)。
总结
Raft协议对于集群变更的规范,充分体现了它的初衷,就是定义一种易于理解和工程实现的一致性算法。至此,用了3篇文章做了Raft协议的解析,后续会将重点放在基于Raft协议实现的系统,如etcd的源码解读。