Skip to content

如何解决分布式系统中的"幽灵复现"?

"幽灵复现"的问题本质属于分布式系统的"第三态"问题,即在网络系统里面,对于一个请求都有三种返回结果:成功,失败,超时未知。对于超时未知,服务端对请求命令的处理结果可以是成功或者失败,但必须是两者中之一,不能出现前后不一致情况。

"幽灵复现"问题

当前业界有很多分布式一致性复制协议,比如Paxos,Raft,Zab及Paxos的变种协议,被广泛用于实现高可用的数据一致性。但是考虑在一些极端异常,比如网络隔离,机器故障等情况下,Leader可能会经过多次切换和数据恢复,使用Paxos协议处理日志的备份与恢复时,可以保证确认形成多数派的日志不丢失,但是无法避免一种被称为"幽灵复现"的现象。

幽灵复现场景

在第一轮中,A成为指定Leader,发出1-10的日志,不过后面的6-10没有形成多数派,随机宕机。随后第二轮中,B成为指定Leader,继续发出6-20的日志。随机再次发生切换,A回来了,从多数派拿到的最大LogId为20,因此决定补空洞。针对Index 7到Index 10,因为多数派没有形成有效落盘,因此A随机以本地日志发起提议并形成多数派。相当于在第二轮并不存在的7~10,然后在第三列又重新出现了。

在数据库日志同步场景的情况,这个问题是不可接受的,一个简单的例子就是转账场景,用户转账时如果返回结果超时,那么往往会查询一下转账是否成功,来决定是否重试一下。如果第一次查询转账结果时,发现未生效而重试,而转账事务日志作为幽灵复现日志重新出现的话,就造成了用户重复转账。

基于Multi-Paxos解决"幽灵复现"问题

为了处理"幽灵复现"问题,基于Multi-Paxos实现的一致性系统,可以在每条日志内容保存一个epochID,指定Proposer在生成这条日志时以当前的ProposalID作为epochID。按logID顺序回放日志时,因为leader在开始服务之前一定会写一条StartWorking日志,所以如果出现epochID相对前一条日志变小的情况,说明这是一条"幽灵复现"日志,要忽略掉这条日志。

Multi-Paxos 解决幽灵复现

基于Raft解决"幽灵复现"问题

关于Raft日志恢复

在Raft中,每次选举出来的Leader一定包含已经Committed的数据(抽屉原理,选举出来的Leader是多数中数据最新的,一定包含已经在多数节点上Commit的数据),新的Leader将会覆盖其他节点上不一致的数据。

Raft中增加了一个约束:对于之前Term的未Committed数据,修复到多数节点,且在新的Term下至少有一条新的Log Entry被复制或修复到多数节点之后,才能认为之前未Committed的Log Entry转为Committed。

Raft算法要求Leader当选后立即追加一条Noop的特殊内部日志,并立即同步到其它节点,实现前面未Committed日志全部隐式提交。从而保证了两个事情:

  • 通过最大Commit原则保证不会丢数据;
  • 保证不会读到未Committed的数据,因为只有Noop被大多数节点同意并提交了之后,服务才会对外正常工作。

Raft解决"幽灵复现"问题

Raft 解决幽灵复现场景一

针对第一小节的场景,Raft中是不会出现第三轮A当选leader的情况。首先对于选举,候选人对比的是最后一条日志的任期号(lastLogTerm)和日志的长度(lastLogIndex)。B、C的lastLogTerm和lastLogIndex都比A的大,因此leader只能出现在B、C之内。

Raft 解决幽灵复现场景二

Raft 写入 noop 日志

Raft里面加入了新Leader必须写入一条当前Term的Log Entry就可以解决这个问题,其实和MultiPaxos提到的写入一个StartWorking日志是一样的做法。当B成为Leader后,会写入一个Term 3的noop日志。

基于Zab解决"幽灵复现"问题

关于Zab的日志恢复

Zab在工作时分为原子广播和崩溃恢复两个阶段。崩溃恢复又可以细分为Leader选举和数据同步两个阶段。早期的Zab协议选举出来的Leader满足:新选举的Leader节点含有本轮次所有竞选者最大的zxid。

zxid是64位高32位是epoch编号,每经过一次Leader选举产生一个新的leader,新的leader会将epoch号+1,低32位是消息计数器。这样设计的好处在于老的leader挂了以后重启,它不会被选举为leader。

Zab 日志恢复

Zab解决"幽灵复现"问题

Zab 解决幽灵复现

对于第1节的场景,根据ZAB的选举阶段的机制保证,每次选举后epoch均会+1,并作为下一轮次zxid的最高32位。所以A是不会被选为Leader的。

为了解决"幽灵复现"问题,最新Zab协议中,每次leader选举完成后,都会保存一个本地文件,用来记录当前EpochId(记为CurrentEpoch),在选举时,会先读取CurrentEpoch并加入到选票中。

进一步探讨

从服务端来看"幽灵复现"问题,就是在failover情况下,新的leader不清楚当前的committed index,也就是分不清log entry是committed状态还是未committed状态,所以需要通过一定的日志恢复手段,保证已经提交的日志不会被丢掉(最大commit原则),并且通过一个分界线(如MultiPaxos的StartWorking,Raft的noop,Zab的CurrentEpoch)来决定日志将会被commit还是被drop,从而避免模糊不一的状态。

在客户端中,请求收到超时,那么客户端是不知道当前底层是处于什么状况的,成功或失败都不清楚,所以一般客户端的做法是重试,那么底层apply的业务逻辑需要保证幂等性,不然重试会导致数据不一致。

用心记录,持续成长