通过集群成员变更来看 etcd 的分布式一致性
集群成员变更一直是 etcd 最棘手的问题之一,在变更过程中会遇到各种各样的挑战,我们稍后一一来看。为了把问题描述清楚,首先需要了解 etcd 内部的 raft
实现。
1. etcd 内部的 raft 实现
leader 会存储所有 follower 对自身 log 数据的 progress
(复制进度),leader 根据每个 follower 的 progress
向其发送 replication message
。
replication message
是 msgApp
外加上 log 数据。
progress 有两个比较重要的属性:match
和 next
。match
是 leader 知道的 follower 对自身数据的最新复制进度【或者说就是 follower 最新的 log entry sent index】,如果 leader 对 follower 的复制进度一无所知则这个值为 0。next
则是将要发送给 follower 的下一个 log entry sent 的序号。
progress 有三个状态:probe
,replicate
和 snapshot
。
+--------------------------------------------------------+
| send snapshot |
| |
+---------+----------+ +----------v---------+
+---> probe | | snapshot |
| | max inflight = 1 <----------------------------------+ max inflight = 0 |
| +---------+----------+ +--------------------+
| | 1. snapshot success
| | (next=snapshot.index + 1)
| | 2. snapshot failure
| | (no change)
| | 3. receives msgAppResp(rej=false&&index>lastsnap.index)
| | (match=m.index,next=match+1)
receives msgAppResp(rej=true)
(next=match+1)| |
| |
| |
| | receives msgAppResp(rej=false&&index>match)
| | (match=m.index,next=match+1)
| |
| |
| |
| +---------v----------+
| | replicate |
+---+ max inflight = n |
+--------------------+
如果 follower 处于 probe
状态,则 leader 每个心跳包最多只发送一个 replication message
。leader 会缓慢发送 replication message
并探测 follower 的处理速度。leader 收到 msgHeartbeatResp
或者收到 msgAppResp
(其中 reject 值为 true)时,leader 会发送下 一个 replication message
。
当 follower 给 leader 的 msgAppResp
的 reject 为 false 的时候,它会被置为 replicate
状态,reject 为 false 就意味着 follower 能够跟上 leader 的发送速度。leader 会启动 stream 方式向以求最快的方式向 follower 发送 replication message
。当 follower 与 leader 之间的连接断连或者 follower 给 leader 回复的 msgAppResp
的 reject 为 true 时,就会被重新置为 probe
状态,leader 当然也会把 next
置为 match+1
。
当 follower 处于 replicate
状态时,leader 会一次尽量多地把批量 replication message
发送给 follower,并把 next
取值为当前 log entry sent 的最大值,以让 follower 尽可能快地跟上 leader 的最新数据。
当 follower 的 log entry set 与 leader 的 log entry sent 相差甚巨的时候,leader 会把 follower 的状态置为 snapshot
,然后以 msgSnap
请求方式向其发送 snapshot
数据,发送完后 leader 就等待 follower 直到超时或者成功或者失败或者连接中断。当 follower 接收完毕 snapshot 数据后,就会回到 probe
状态。
当 follower 处于 snapshot
状态时候,leader 不再发送 replication message
给 follower。
新当选的 leader 会把所有 follower 的 state 置为 probe
,把 match
置为0,把 next
置为自身 log entry set 的最大值。
leader 与 follower 之间进行数据同步的时候,可以通过下面两个步骤进行流量控制:
限制 message 的 max size。这个值是可以通过相关参数进行限定的,限定后可以降低探测 follower 接收速度的成本;
当 follower 处于
replicate
状态时候,限定每次批量发送消息的数目。leader 在网络层之上有一个发送 buffer,通过类似于 tcp 的发送窗口的算法动态调整 buffer 的大小,以防止 leader 由于发包过快导致 follower 大量地丢包,提高发送成功率。
snapshot
,故名思议,是某个时间节点上系统状态的一个快照,保存的是此刻系统状态数据,以便于让用户可以恢复到系统任意时刻的状态。 etcd-raft 中的 snapshot 代表了应用的状态数据,而执行 snapshot 的动作也就是将应用状态数据持久化存储,这样,在该 snapshot 之前的所有日志便成为无效数据,可以删除。
2. 集群成员变更
当集群加入新节点时,新加入的节点是没有任何数据的,因此新节点的 log entry sent 与 leader 的 log entry sent 相差很大,所以 leader 会向该节点发送 snapshot
数据。这时 leader 的网络有可能会过载、阻塞甚至丢弃 leader 发送给 follower 的 heartbeat
,一段时间以后某个 follower 会因为选举超时将自己的状态切换为 candidate 并发起选举。所以新加入的节点很容易对集群造成影响,无论是 leader 选举还是将后续的更新传播给新成员,都很容易导致集群不可用。
网络隔离
如果发生了网络隔离,集群还会正常工作吗?主要还是取决于 leader 被隔离到哪个区域。
当集群的 Leader 在多数节点这一侧时,集群仍可以正常工作。例如一个 3 节点集群,它的 quorum
为 2,其中一个 follower 被网络隔离,因为 leader 所在的这一侧的 majority
为 2,所以不会发生重新选举。
Quorum 机制,是一种分布式系统中常用的,用来保证数据冗余和最终一致性的投票算法,具体参考 分布式系统之 Quorum 机制。应用在 etcd 的场景中,quorum 表示能保证集群正常工作的最少节点数。而
majority
表示集群当前能参加投票的节点数量。
如果 leader 被整个集群都隔离了,这时 leader 的 majority
为 1,无法发起选举,leader 就会将自己的状态切换为 follower,影响到了集群的可用性。
拥有 3 个节点的集群加入 1 个新节点之后集群节点数量变为 4
,quorum 大小变为 3
。
新加入节点后隔离网络
如果加入新节点之后发生了网络隔离,集群还会正常工作吗?主要还是取决于新加入的节点被隔离到了哪个区域。
如果新加入的节点与 leader 被隔离在同一个区域内,leader 的 majority
数量仍然为 3
,不会导致重新选举,也不会影响集群的可用性。
如果新节点与 leader 不在同一区域内,并且集群被对半隔离,这时任何一侧的 majority 都不是 3
,从而会发生重新选举,leader 将状态切换为 follower。
隔离网络后再加入新节点
如果先发生网络隔离,后加入新节点,集群还会正常工作吗?
假设一个拥有 3 个节点的集群已经有一个 foloower 被隔离了,这时再加入新节点,quorum 就会从 2 变为 3。但此时新加入的节点还没有启动,集群的 majority
为 2,从而会发生重新选举。
因为 member add
命令会改变集群的 quorum 大小,所以建议先通过 member remove
命令移除处于崩溃状态的 follower。
加入新节点带来的问题
向一个单节点集群中加入新节点后,集群的 quorum 大小变为 2,但这时还会发生重新选举,为什么呢?因为加入节点的操作是分成两步进行的:
执行
member add
命令启动新节点
当你执行完 member add
命令后,集群的 quorum 大小变为 2,但此时新节点还没有启动,从 leader 的视角来看,majority
仍然是 1,不满足 quorum,所以会重新选举。
来看一种更糟糕的场景,如果新加入的节点配置错误(比如 --peer-urls
是非法的),当执行 member add
命令之后,单节点集群的 quorum 大小变为 2,发生重新选举,但此时新节点不会启动成功的,所以无法满足 quorum。一旦集群无法满足 quorum,就再也无法完成集群成员变更。
多节点集群类似。例如一个拥有 3 个节点的集群,新加入一个配置错误的节点后,quorum 大小从 2 变为 3
。此时只要有 1 个 follower 发生故障,整个集群就会变为不可用状态,因为集群的 majority 为 2,不满足 quorum(其中 1 个 follower 发生故障,另一个配置错误)。
这就带来了一个很严峻的问题:只要新加入的节点配置上出了点什么差错,整个集群的容错能力就会减 1。这时你只能通过 etcd --force-new-cluster
命令来重新创建集群。
但 etcd 可是 Kubernetes 集群至关重要的组件啊,即使是最轻微的中断也可能会对用户的生产环境产生重大影响。怎样才能使成员变更的操作更安全呢?相对于其他方面来说,leader 选举对 etcd 集群的可用性有着至关重要的影响:有没有办法在集群成员变更的时候不改变集群的 quorum 大小?能否让新加入的节点处于备用的空闲状态,缓慢接收 leader 的 replication message
,直到与 leader 保持同步?新加入的节点如果配置错误,有没有办法能让其回退?或者有没有更安全的办法来完成集群成员变更的操作(新加入节点配置错误不会导致集群的容错能力下降)?集群管理员新加入节点时需要关心网络协议吗?无论节点的位置在哪,无论是否发生网络隔离,有没有办法让用来加入新节点的 API 都可以正常工作?
3. 引入 Raft Learner 角色
为了解决上一节提到的加入新节点带来的容错能力下降的问题,rfat 4.2.1 论文 中介绍了一种新的节点角色:Learner
。以该角色加入集群的节点不参与投票选举,近接收 leader 的 replication message
,直到与 leader 保持同步为止。
v3.4 中的新特性
集群管理员向集群中添加新节点时要尽可能减少不必要的操作项。通过 member add --learner
命令可以向 etcd 集群中添加 learner 节点,不参加投票,只接收 replication message
。
当 Learner
节点与 leader 保持同步之后,可以通过 member promote
来将该节点的状态提升为 follower,然后将其计入 quorum 的大小之中。
leader 会验证 promote
请求来确保其操作的安全性。只有当 learner 的 log 数据与 leader 保持一致后,learner 才能被提升为 follower 节点。
Learner 被提升为 follower 之前会一直被当成备用节点,且 leader 节点不能被转换为 learner 节点。learner 节点也不会接受客户端的读写操作,这就意味着 learner 不需要向 leader 发送 Read Index
请求。这种限制简化了 etcd v3.4 中 learner 的实现方式。
除此之外,etcd 还限制了集群中 Learner
节点数量的上限,以避免大量的 replication message
使 leader 过载。Learner 节点自身不能改变自己的状态,etcd 提供了 learner 状态检测和安全性检测,集群管理员必须自己决定要不要改变 learner 的状态。
v3.5 中的新特性
新加入的节点默认就是 Learner 角色
当 learner 的 log 数据与 leader 保持一致后,集群会自动将 learner 转换为 follower。从用户的角度来看,你仍然可以使用
member add
命令来加入新节点,但集群会自动帮你把新加入的节点设置为 learner 状态。新加入的节点被视为备用节点,一旦集群的可用性受到影响,就会被提升为 follower 状态。
learner 节点可以被设置为只读状态,被设置成只读状态后就永远不能被提升为 follower 状态。在弱一致性模式中,learner 只接收 leader 发送的数据,并且永远不会响应写操作。在没有共识开销的情况下从本地读取数据会大大减少 leader 的工作量,但向客户端提供的数据可能会过时。在强一致性模式中,learner 会向 leader 发送
read index
以获取最新的数据,但仍然拒绝写请求。
4. 参考资料
https://github.com/etcd-io/etcd/blob/master/raft/design.md
https://etcd.readthedocs.io/en/latest/server-learner.html