文档版本 v3.7-DRAFT 处于 草稿 状态。如需获取最新的稳定版文档,请参阅 v3.6。
etcd learner 设计
etcd 学习者(Learner)
Gyuho Lee(github.com/gyuho,Amazon Web Services, Inc.),Joe Betz(github.com/jpbetz,Google Inc.)
背景
成员重组一直是运维中最大的挑战之一。让我们回顾一些常见的问题。
1. 新加入的集群成员使领导者过载
一个新加入的 etcd 成员初始时没有数据,因此需要从领导者处获取大量更新,直到其日志追上领导者的进度。这会导致领导者的网络负载加重,可能阻塞或丢弃发往跟随者的领导者心跳消息。在这种情况下,某个跟随者可能会因超时而发起新的领导者选举。也就是说,包含新成员的集群更容易发生领导者选举。而领导者选举以及随后向新成员传播更新的过程,都可能导致集群出现一段时间的不可用(参见图 1)。

2. 网络分区场景
如果发生网络分区会怎样?这取决于领导者所在的分区。如果领导者所在分区仍能维持活跃的多数派(quorum),则集群可以继续运行(参见图 2)。

2.1 领导者隔离
如果领导者与集群其余部分失去连接怎么办?领导者会监控每个跟随者的进度。当领导者失去与多数派的连接时,它将退化为跟随者角色,从而影响集群的可用性(参见图 3)。

当一个新节点被添加到一个 3 节点集群时,集群规模变为 4,法定人数(quorum)也变为 3。如果新节点刚加入后就发生了网络分区,会发生什么?这取决于分区后新成员位于哪个分区。
2.2 集群分裂为 3+1
如果新节点恰好与领导者处于同一分区,则领导者仍然保有大小为 3 的活跃法定人数。此时不会触发领导者选举,集群可用性也不受影响(参见图 4)。

2.3 集群分裂为 2+2
如果集群被划分为两个各含两个节点的分区,则任一分区都无法满足大小为 3 的法定人数要求。此时将触发领导者选举(参见图 5)。

2.4 法定人数丢失
如果先发生网络分区,然后再添加新成员会怎样?一个已分区的 3 节点集群已经有一个断开连接的跟随者。当添加新成员时,法定人数从 2 增加到 3。现在,该集群在总共 4 个节点中仅有 2 个活跃节点,因此失去了法定人数,进而触发新的领导者选举(参见图 6)。

由于添加成员操作会改变法定人数的大小,因此建议在替换不健康的节点时,应先执行“member remove”操作。
向一个单节点集群添加新成员会将法定人数变为 2,这会立即导致之前的领导者在发现当前无法维持法定人数时触发一次领导者选举。这是因为“member add”操作是一个两步过程:用户需先执行“member add”命令,然后才启动新节点进程(参见图 7)。

3. 集群配置错误
更糟糕的情况是新增成员配置错误。成员重组是一个两步过程:“etcdctl member add”和使用指定对等 URL 启动 etcd 服务器进程。也就是说,“member add”命令无论 URL 是否有效都会被执行。如果第一步使用了无效的 URL,则第二步根本无法成功启动新的 etcd 实例。一旦集群失去法定人数,就无法回滚成员变更(参见图 8)。

同样的情况也适用于多节点集群。例如,集群中有两个成员已下线(一个发生故障,另一个配置错误),另外两个成员正常运行,但现在更改集群成员资格至少需要 3 票才能通过(参见图 9)。

如上所述,一个简单的配置错误就可能导致整个集群进入无法运行的状态。在这种情况下,操作员需要手动使用 etcd --force-new-cluster 参数重新创建集群。由于 etcd 已成为 Kubernetes 的关键服务,即使是最轻微的中断也可能对用户造成重大影响。我们能做些什么来让 etcd 的这类操作更简便呢?其中,领导者选举对集群可用性最为关键:我们能否在不改变法定人数规模的前提下进行成员重新配置,从而减少干扰?新节点是否可以先处于空闲状态,仅从领导者处请求最少的数据更新,直到其日志追平?成员配置错误是否应始终可逆,并以更安全的方式处理(错误地执行添加成员命令绝不应导致集群失效)?用户在添加新成员时是否还需要担心网络拓扑结构?成员添加 API 是否可以在不考虑节点位置和当前网络分区的情况下正常工作?
Raft 学习者(Learner)
为了缓解前一节中提到的可用性问题,Raft §4.2.1 引入了一种新的节点状态“学习者”(Learner),该状态的节点以非投票成员身份加入集群,直到其日志追平领导者的日志为止。
v3.4 版本中的特性
操作员应尽可能少地进行操作即可添加一个新的学习者节点。member add --learner 命令用于添加新的学习者,它作为非投票成员加入集群,但仍会接收来自领导者的所有数据(参见图 10)。

当学习者节点的日志进度已赶上领导者后,可通过 member promote API 将其提升为投票成员,此后该节点将计入法定人数(参见图 11)。

etcd 服务器会对提升请求进行验证,以确保操作的安全性。只有当日志完全追平领导者后,学习者才能被提升为投票成员(参见图 12)。

学习者在被提升前仅作为备用节点:不能将领导权转移给学习者。学习者拒绝客户端的读写请求(客户端负载均衡器不应将请求路由到学习者)。这意味着学习者无需向领导者发送 Read Index 请求。这一限制简化了 v3.4 版本中学习者的初始实现(参见图 13)。

此外,etcd 限制了集群中允许存在的学习者总数,以避免因日志复制而使领导者过载。学习者永远不会自行提升自己。虽然 etcd 提供了学习者状态信息和安全检查机制,但是否提升学习者仍需由集群操作员最终决定。
未来版本提议的功能
将学习者状态设为默认且唯一初始状态:将新成员的默认状态设为学习者将极大提升成员重新配置的安全性,因为学习者不会改变法定人数的大小。即使发生配置错误,也能始终逆转操作,而不会失去法定人数。
实现投票成员提升的全自动机制:一旦学习者追平了领导者的日志,集群可自动将其提升。etcd 要求用户定义某些阈值,一旦满足条件,学习者便自动升级为投票成员。对用户而言,“member add”命令的使用方式与今天相同,但得益于学习者特性,安全性更高。
将学习者作为备用故障转移节点:学习者以备用节点身份加入集群,当集群可用性受到影响时,可自动被提升。
使 learner 只读:learner 可以作为只读节点,永远不会被提升。在弱一致性模式下,learner 仅从 leader 接收数据,且不处理写入请求。本地提供读服务而无需共识开销,可显著减轻 leader 的工作负载,但可能返回过期数据。在强一致性模式下,learner 向 leader 请求 read index 以提供最新数据,但仍拒绝写入。
Learner 与 Mirror Maker 的对比
etcd 使用 watch API 实现“镜像制造者(mirror maker)”,持续将键的创建和更新转发到另一个独立集群。一旦完成初始同步,镜像通常具有较低的延迟开销。Learner 和镜像功能上有重叠,两者都可用于复制现有数据以支持只读访问。然而,镜像不能保证线性一致性。在网络断开期间,先前的键值可能已被丢弃,客户端需要自行验证 watch 响应的正确顺序。因此,镜像不提供任何顺序保证。若追求最低延迟(例如跨数据中心场景)而牺牲一致性,应使用镜像;若需保留所有历史数据及其顺序,则应使用 learner。
附录:v3.4 版本中的 Learner 实现
向“MemberAdd”API 暴露“Learner”节点类型。
etcd 客户端为“MemberAdd”API 添加一个标志位,用于指定 learner 节点。etcd 服务端处理器使用 pb.ConfChangeAddLearnerNode 类型应用成员变更条目。一旦该命令被应用,服务器即可通过 etcd --initial-cluster-state=existing 参数加入集群。此 learner 节点既不能参与投票,也不计入法定多数(quorum)。
etcd 服务端不得将领导权转移给 learner,因为它可能仍然落后,且不计入法定多数。etcd 服务端限制集群中最多只能有一个 learner:learner 越多,leader 需要传播的数据量就越大。客户端可以连接 learner 节点,但 learner 会拒绝除可序列化读取和成员状态 API 之外的所有请求。这是为了简化初始实现。未来,learner 可扩展为持续镜像集群数据的只读服务器。客户端负载均衡器必须提供辅助函数以排除 learner 节点的端点,否则发送到 learner 的请求可能会失败。客户端的 sync member 调用应考虑 learner 节点类型,更新 endpoints 的调用也应如此。
MemberList 和 MemberStatus 的响应应标明哪个节点是 learner。
添加“MemberPromote”API。
在 Raft 内部,对 learner 节点的第二次 MemberAdd 调用会将其提升为投票成员。Leader 会维护每个 follower 和 learner 的进度。如果 learner 尚未完成其快照消息的接收,则拒绝提升请求。仅当满足以下条件时才接受提升请求:learner 节点处于健康状态,且与 leader 同步或差异在阈值范围内(例如,需复制给 learner 的日志条目数少于快照数量的十分之一,这意味着即使提升后,leader 也不太可能需要再次向 learner 发送快照)。所有这些逻辑均硬编码在 etcdserver 包中,不可配置。
参考
- 原始 GitHub issue:etcd#9161
- 使用场景:etcd#3715
- 使用场景:etcd#8888
- 使用场景:etcd#10114