本文选自我在知乎的回答。
《设计数据密集型应用》(即 DDIA)中提到 Raft 的一个问题,即,Raft 算法存在一种失去活性(liveness)的极端情况:如果有一条网络连接不可靠,Raft 当前领导者会不断被迫下台导致系统实质上毫无进展。
我们先来具体描述一下该问题。
如图所示的 4 节点 Raft 集群,其中有一个节点和其他三个网络不太稳定,假设它能发送消息给别的节点但收不到其他节点的消息,那么它就会一直收不到心跳消息,然后转为 candidate 自增任期并发起新的选举,来自更大任期的 RequestVote 请求会导致现在的 Leader 下台重新选举。这样一直反复,会导致集群无法正常工作。
Raft 大论文提到一种解决方式是加入一个新的 PreVote 阶段,etcd 就这么干了,为此增加了一种新的节点状态叫做 PreCandidate 状态。
PreVote 阶段作用是当一个节点想要发起选举时,首先要确认自己确实有资格赢得投票而不是在浪费时间,才会真的自增任期发起新的选举。
PreVote 阶段的具体流程是,在发起真正的选举之前,先发送 PreVote 消息给所有节点, PreVote消息和 RequestVote 消息一样,但节点不会自增自己的任期,只会增加消息中的 term 参数。
收到 PreVote 消息的节点同意重新选举的条件是:
参数中的任期更大,或者任期相同但 log index 更大;
至少一次 election timeout 时间内没有收到领导者心跳;
只有超过半数节点同意 PreVote 消息,该节点才能真正去自增任期并发起新的选举。
回到上述情况,网络链路有问题的那个节点在 PreVote 阶段会发现自己无法赢得超过半数节点同意自己发起选举(别的节点都能收到心跳),因此不会自增任期去干扰 Leader 工作。
问题解决了吗?
问题并没有解决,只有 PreVote 阶段还可能有一种极端情况会导致 Raft 失去活性。如图所示:
图中是一个 5 节点组成的 Raft 集群,故障发生之前 4 是 Leader。现在故障发生了,5 宕机了,同时 4 只和 2 保持连接,1、2、3互相保持连接。这种情况下 1、3 收不到 Leader 的心跳,会发起 PreVote请求,但由于 2 能收到 Leader 节点 4 的心跳,所以 2 不会同意 PreVote 请求,因此节点 1、3 无法获得多数派的 PreVote 同意。
该集群的问题是,无法选举出新的 Leader,但旧的 Leader 又只能 AppendEntries 到两个节点(2和自己),无法达成多数派,整个集群无法取得任何进展,不满足活性。
此处 Raft 协议明明可以容忍 2 个节点故障,但增加了 PreVote 阶段后反而无法容忍仅仅 1 个节点故障,其实没有 PreVote 阶段的话,1 和 3 是有机会当选 Leader 推进整个系统正常工作的。
因此 Raft 还需要增加一种机制来让 Leader 主动下台。
这个机制很简单:Leader 没有收到来自多数派节点的 AppendEntries 响应时就主动下台。这样,图中 1、2 和 3 都有机会当选新的 Leader,整个集群依旧可以正常工作。
etcd 把这叫做 CheckQuorum,etcd 的 issue 中有关于此问题的讨论:https://github.com/etcd-io/etcd/issues/3866
CheckQuorum 确保了如果当前 Leader 无法连接到多数派节点,它将会下台并选举出新的 Leader。PreVote 确保一旦 Leader 当选,整个系统将是稳定的,Leader 不会被迫下台。
那么 PreVote + CheckQuorum 可以解决活性问题了吗?
可以了!