A-A+

Raft One-Server成员变更

2017年04月13日 分布式 暂无评论 阅读 1,634 次

前面介绍了Paxos成员组变更,现在根据Raft博士论文介绍一下Raft是怎么做成员组变更的。前者更多描述几个核心的思想,而后者更加实用,介绍了实践过程中遇到的一些实际问题。

Raft博士论文描述了两种变更方案:

  1. One-Server变更:一阶段变更,要求每次成员组从G1变成G2时,G2相比G1加一个成员或者减一个成员。
  2. Joint Consensus:支持任意的变更,即从成员组G1变成G2,不要求G1和G2有什么关联,比如可以完全没有交集。

从实用角度看,第一种简单,而且经过多轮One-Server变更实际上也能最终完成任意成员组变更。Raft博士论文也主要是描述第一种方案。之前的博文描述了会议论文中的Joint Consensus,这里只描述第一种。

先明确一个思路,成员变更Safety的要点是不会出现双主,也就是新旧成员组不能各形成一个quorum,各自选出一个leader。

如果每次变更增加或者删除多个server,直接切换成员组就会出现双主,如图。

disjoint majorities

图中,从Cold={S1,S2,S3}变更为Cnew={S1,S2,S3,S4,S5},如果直接切换成员组,那么在变更过程中,{S1,S2}形成Cold的多数派,{S3,S4,S5}形成Cnew的多数派,这两个多数派可能分别选出leader并确认client的新command,造成不一致。

One-Server成员变更

Raft最终采用的是每次变更一个server的变更方案来避免出现上图中的disjoint majorities:当增加(或者减少)一个server的时候,新旧成员组的任意多数派必然有交集。这个结论可以简单地从四种情况穷举证明(从奇/偶数个成员组中增/删一个成员),比如原来成员组大小是2k,增加一个成员后2k+1,如果新旧成员组形成了disjoint majorities,则(k+1) + (k+1) = 2k+2 > 2k+1,即至少2k+2个成员才可能出现disjoint majorities。

Raft成员变更使用的思路还是由状态机决定成员组的思路,即Paxos成员变更一文中的思路一。成员组变更的确认使用成员变更日志来确认,成员变更日志在确认过程中,允许接收客户端新的command并进行同步,即不停服务。

具体地,当leader收到成员组变更的命令后,从Cold加或减一个成员生成Cnew,Cnew记录在成员变更日志中,并像其他普通日志一样同步给至少多数派。一个副本在收到Cnew的变更日志,本地持久化了该日志之后,就立刻使用这个新的成员组来工作,而不是等待成员变更日志确认。成员变更日志的确认也使用Cnew来完成。

当Cnew成员变更日志确认之后,本次成员变更结束。此时Cnew已经同步到多数派,多数派成员已经开始使用Cnew了。因为One-Server变更不会出现disjoint majorities,所以没有Cnew中某些成员的同意,未同步到Cnew的几个成员不可能确认任何command,也不可能独立选出leader。

本次成员变更结束之后,就可以响应客户端了,说本次成员变更请求完成。如果本次变更是减掉一个server,那么这台server就可以关机下线了。

考虑一个问题:什么时候允许开启下一次成员变更呢?博士论文中的说法是,当Cnew确认了,就可以发起下一次成员变更。另外论文里面也有一句话,只有当Cold中的多数派已经转到Cnew下开始工作时,才可以开启下一次成员变更。这个条件跟Cnew确认的条件并不等价,Cnew确认可以推出Cold中不会有多数派未使用Cnew,但未必是多数派已使用Cnew。只要Cnew形成多数派(在Cnew成员组中形成多数派),则下一次成员变更Cnew2如果跟Cnew还是满足只相差一个成员的话,Cnew2和Cnew想要各自构成一个多数派来选出双主是不可能的。

需要注意的是,这里Cnew和Cnew2只差一个成员的前提要保证,Raft One-Server成员变更中最初版本的协议中有一个bug,在该Bug下,Cnew2跟Cnew只相差一个成员这个条件不成立,造成正确性问题,稍后我们描述这个bug。读者也可以从这个bug中琢磨什么时候开启下一次成员变更是安全的。

Cnew落盘之后,不等形成多数派,就可以立刻使用最新的成员组。这种方式借助了Raft已有的日志同步机制:只要Cnew确认了,那么至少Cnew的多数派已经使用了Cnew。不考虑实现性能,其实server可以在等Cnew确认之后才开始使用Cnew, 只是Raft已有机制不方便追踪哪些server已经获知Cnew commit了。这种方式也有一个问题,某些server在落盘了Cnew之后,开始使用Cnew,但是Cnew在确认之前,如果发生主备切换,未确认的Cnew是有可能被覆盖的,此时server还要回滚成员组。

另一个需要注意到的地方是,Raft中,是请求的发起方的成员组来判断多数派,比如发起append entry的leader,或者是尝试拉票的server。请求的接收方并不关心自己维护的成员组。

实践问题

在协议大致描述完成后,我们看几个实践中的问题。

Catching up new servers

首先如果想加入新的server,通常新的server上是空日志,此时如果立刻加到成员组里面,要参与投票的话,因为Raft顺序确认的问题,新server要花比较长的时间同步比较旧的日志,直到同步到很新才能开始贡献多数派投票。这期间其他server宕机很可能导致日志服务停止。

解决方法是新加入的server先作为learn的角色追日志,等追的差不多了再加入成员组中。具体怎么叫追的差不多了呢?Raft引入了一个round的方法,即一轮开始的时候,先看leader上的日志写到哪了,追到这个位置第一轮结束,第一轮结束的时候,leader可能在新server追第一轮的过程中可能又写入了一批日志,此时开始第二轮,以此类推,如果连续追了10轮,最后一轮的耗时在一个选举周期内,就算追上了。

Removing the current leader

另外一个问题是,如果要下线当前leader这台server怎么做呢?

旧主等到Cnew确认之后再下线。这个方法在Joint Consensus和不支持leadership transfer的时候是必要的。

在旧主使用Cnew到Cnew确认这段时间内,旧主仍负责同步日志并参与选举,这段时间内旧主使用不包含自己的成员组工作(同步日志和选举)。当Cnew确认之后,旧主就不必再参与日志同步和选举了。

比如,从{S1(Leader),S2}中下线S1。此时S1如果立刻停止工作,则系统无法推进。在S2同步到Cnew之前,S2无法被选为leader(S1的日志比S2新所以不会给S2投票,Cold={S1,S2}中只有S2的投票不够多数派)。

所以,要S1先写Cnew={S2}然后同步给S2,S1发现S2已经构成了Cnew={S2}的多数派,这之后才能下线S1。

Disruptive Servers

还有一个问题是当Cnew开始使用之后,不在Cnew中的老server就不再收到新leader的心跳,老server就会误以为leader已经挂了,于是尝试增大term去抢leadership。这个过程可能会持续,导致集群无法工作。

比较容易想到的解决方法是一个server在发起拉票前,先看自己的日志是不是最新,只有本机日志在多数派中是最新的,才能有机会做leader,如果这个条件不满足,那这个server发起拉票就是浪费时间。这个过程叫做Pre-Vote阶段。但是这个方法并不总是有效,比如{ABCD}->{ABC}的时候,A是Leader,在尝试复制Cnew到BC的时候,D可能发起了Pre-Vote,因为D在{BCD}中是能够完成Pre-Vote过程的,所以disruptive servers问题还是没解决。

Raft使用的是另外一种方式,如果一个备机发现自己还处于臣服某个leader的租约周期内,就不向其他server选举投票,哪怕是更高term的请求。此时如果要中途打断leadership,就要给这种选举请求加上标记,开个绿灯。

 

Raft成员变更补丁

前面描述的Raft One-Server成员变更的方法,社区后来爆出一个bug。Raft作者详细描述了问题以及修复方案。

bug例子

Raft中成员变更如果保证是只变一个server这个前提,就不会出现双主问题。如果在同一term下,可以由leader确定这件事,但是如果有两个并发的成员变更,并且同时发生了切主,就有可能保证不了这一前提。

考虑如下的例子:

case1

问题在于新主接收到客户端下一次成员变更请求的时候,可能集群中还可能有未确认的之前的成员变更日志,这个日志在将来可能会确认。

修复方案

Raft中,新主上任前要先确认一条NOP日志,然后才开始对外提供服务。

一旦新主在本term内确认了一条日志,那么说明新主已经确认了最新的成员组,并且集群中不会出现未确认的成员变更日志将来会被确认的情况了,在NOP确认后再开启成员变更就能保证每次只变一个server的前提。

 

给我留言