jimyag's Blog

Kubernetes etcd 一致性与恢复:多数派、快照和状态回退

三成员 etcd 集群失去一个成员后仍能写入,失去两个成员后却不能靠“把请求发给活着的那个”恢复服务。这个限制来自提交规则,而不是客户端缺少重试。恢复服务之前,必须先区分仍有多数派的成员替换,以及已经无法组成多数派的灾难恢复。

快照恢复还会带来另一类问题:磁盘上的 Kubernetes 对象回到了过去,但节点上的进程、业务卷里的数据和客户端缓存不会一起倒退。恢复数据库只是恢复控制面的一个环节。

多数派决定谁能推进已提交状态

etcd 用 Raft 复制变更。对包含 N 个投票成员的集群,多数派是 floor(N/2)+1;3 个成员需要 2 个,5 个成员需要 3 个。增加到偶数成员并不一定增加可容忍故障数,反而可能增加通信成本。

当一个成员失效,另外两个可以继续形成多数派。若失效者恰好是 leader,还需要重新选主,期间请求可能超时;“容忍一个故障”不等于“每个请求都无缝成功”。失去多数派后,存活成员不能单独确认新写入,拒绝推进状态避免了两个隔离分区各自提交冲突历史。故障语义见 etcd failure modes。

  flowchart TB
    A[原集群三个投票成员] --> B{可通信投票成员数}
    B -->|至少两个| C[可以形成多数派]
    C --> D[必要时重新选主后继续提交]
    B -->|只有一个| E[无法确认新提交]
    E --> F{其他成员能否恢复}
    F -->|能够恢复| G[恢复连接与成员 再形成多数派]
    F -->|永久丢失| H[评估快照与数据损失 执行灾难恢复]

网络分区时不能只数“进程活着几个”,要检查投票成员之间能否通信。磁盘慢、网络延迟和频繁选主也会让一个看似完整的集群无法稳定满足请求。

客户端请求超时还需要单独解释。v3.6.6 的 processInternalRaftRequestOnce 先通过 Raft 提出请求,再等待应用结果;超时分支移除等待者并返回错误,不会撤回已经提出的日志,也不等于事务回滚。因此超时请求的提交状态对客户端可能未知,多数派恢复后需要读回或使用适合业务的幂等协议确认,不能把每一次超时都标记成确定未写入。etcd API guarantees 对这种不确定性有明确说明。

正在加载 GitHub 文件…

读请求也有一致性条件

默认的线性一致读需要确认读取对应的已提交状态,不能把任意存活成员的本地数据直接当成最新状态。etcd v3.6.6 的 EtcdServer.Range在请求不是 Serializable 时,会先调用 linearizableReadNotify,成功后才执行读取。

正在加载 GitHub 文件…

Serializable 读允许从成员本地读取已知状态,可能过期,因此在失去多数派时仍可能成功。它适合能接受陈旧数据的读取场景,不能作为“集群仍具备一致读写能力”的健康证明。Watch 则从指定 revision 观察变更,它的历史可用范围还受到 compaction 限制;具体保证见 etcd API guarantees。

Kubernetes API Server 本身还有缓存和不同请求语义,不能把直接调用 etcd 的一次读成功,推广成所有 Kubernetes API 请求都仍可用。

成员替换与快照恢复解决不同故障

多数派仍然正常时,优先按成员管理流程移除永久故障成员并加入替代成员。这个过程延续原集群的逻辑状态。成员数量、learner、数据同步和提升为投票成员,需要按对应版本的运维说明执行,不能复制一个旧数据目录就当作新成员。

无法恢复原多数派时,快照恢复是从选定备份重新建立集群状态。必须接受快照之后已成功写入的变更可能丢失,并防止旧集群成员重新接入形成混杂拓扑。恢复工具会重建成员和集群身份,而不是继续冒充原来的故障成员。

这两种操作不能混用。为了让一个孤立成员“马上可写”就随意重建集群,可能把原本还能恢复的多数派历史丢掉。恢复之前需要先明确幸存数据、原成员是否真的不可恢复、所选快照的时间与完整性。

快照来源也影响完整性检查。etcdctl snapshot save 保存的快照带有校验信息,恢复工具的 copyAndVerifyDB 会核验;直接复制成员的数据文件可能不带这份校验信息,还可能缺少仅在 WAL 中的最新数据,不能把跳过 hash 检查当成两种备份等价的证明。重建多成员集群时,各成员应使用同一份选定快照和一致的初始集群配置,不能把不同时刻的快照拼成一个恢复点。恢复条件见官方灾难恢复说明。

正在加载 GitHub 文件…

快照恢复不只要关注键值,还要关注 revision

假设一个 Watch 客户端已经观察到较新的 revision,随后服务端从更旧快照恢复。客户端内存里仍可能保留快照之后的对象状态,只恢复键值数据无法自动撤销这些缓存。

etcd 的恢复工具提供 --bump-revision 与 --mark-compacted。前者把恢复后的 revision 提高到足够大的值,后者让旧历史读取和 Watch 明确失败,迫使具备正确恢复逻辑的客户端重新同步。增量需要根据快照年龄与写入速率评估,不能把实验里的固定数字当作所有环境的通用值。参数语义见 etcd 灾难恢复说明。

在 v3.6.6 的 Restore 中,工具先恢复数据库,再在同时设置相关参数时调整 revision。modifyLatestRevision 不只是改显示值,还写入 scheduled compaction revision,使恢复后的服务器把对应历史视为已压缩。这个实现解释了旧 Watch 为什么必须重新建立观察,而不是继续等一个已经不属于当前历史的事件。

正在加载 GitHub 文件…

实验一:单成员失效与失去多数派

实验通过 SSH 在 zj 的专用 kind 集群 article-control 中执行,Kubernetes v1.35.1,镜像内 etcd、etcdctl 和 etcdutl 为 3.6.6。三个控制面各有一个 stacked etcd 成员,实验只暂停自己的节点容器。

先写入实验键并保存快照,再写入另一个快照之后的键。暂停第二个控制面成员后,直接向第一个成员写入仍成功;再暂停第三个成员,写入返回:

1
2
3
OK
Error: context deadline exceeded
two members unavailable: write timed out; commit status unknown

这段输出仅表示写入超时,未获得提交确认。随后对仍活着的第一个成员执行 Serializable 读,读回之前写入的 /article/before 和 snapshot-value。这说明本地旧状态仍可读取,没有证明少数派可以提交新状态。恢复两个暂停成员后,脚本检查 endpoint health,并单独读回超时请求的键,避免从超时错误推断最终数据状态。

修订后在相同版本的新建隔离集群重跑,读回结果是:

1
after quorum recovery: timed-out key committed

这一次超时的写请求确实在恢复后被观察为已提交,直接说明“超时即未写入”的判断不成立。这也不意味着所有超时请求最终都会提交,具体结果仍需确认。

这组故障使用容器暂停,模拟成员无法响应;没有模拟磁盘损坏、数据不一致或跨机房网络分区。

实验二:在独立端口恢复旧快照

恢复实验没有替换 Kubernetes 正在使用的 etcd 数据目录。它把快照恢复到另一个目录,以单成员新集群运行,使用独立的 client 和 peer 端口 3379、3380。原 Kubernetes API Server 仍连接原集群。

恢复时提高 revision 并标记 compaction,读回结果为:

1
2
3
4
5
restored before=snapshot-value; after-snapshot key absent
revision: 1000001272
old revision rejected after mark-compacted
snapshot contains Kubernetes Deployment storage key
old watch rejected after mark-compacted

自动断言确认:快照之前的实验键存在,快照之后的实验键不存在;快照中有 /registry/deployments/control-ha/baseline 这个 Kubernetes Deployment 的存储键。校验脚本要求旧 revision 的 Range 和 Watch 错误文本包含 required revision has been compacted,连接失败或其他非零退出不能算通过。

上述结果验证了快照时间边界、独立恢复、revision 调整及旧 Watch 拒绝。实验没有把恢复后的 etcd 接回 API Server,也没有重启控制器验证完整集群恢复。因此不能据此声称 Kubernetes 控制器、Informer 缓存或工作负载已经完成灾难恢复演练。

配置、运行、证据与清理说明见 k8sdev/article-control-etcd。先按该目录 README 的固定提交准备步骤创建独立工作目录,再运行实验。

旧历史验证明确检查 compaction 错误;仅有非零退出不能证明历史已经失效。

正在加载 GitHub 文件…

控制面元数据与业务数据要分别恢复

etcd 快照保存 Kubernetes 的控制面状态,包括对象、配置和 Secret,并不包含挂载到业务 Pod 的卷内容,也不包含容器内存。快照需要按敏感备份保护,读取权限不能比生产集群本身更宽松。

恢复旧快照后,控制器重新观察到的期望状态可能与节点上的现状不同:快照后创建的对象可能从 API 中消失,旧对象可能重新出现,卷与外部资源的实际状态却已经前进。完整恢复验收必须进一步核对 API 对象、节点运行时、存储数据和外部系统,不能只检查 etcd endpoint healthy。

真正要制定恢复方案时,备份频率决定可接受的数据丢失窗口,演练结果决定恢复时间能否达到目标。多数派保证正常运行中的一致性,快照提供历史恢复点,它们各自承担不同的可靠性责任。

#Kubernetes #源码分析 #Kind