Kubernetes 基础体系 · 第 65/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes etcd 备份恢复:Snapshot、证书、Revision 和灾难演练

在 Kubernetes 控制面中,etcd 是保存集群期望状态的核心数据层。API Server 的对象并不主要保存在自己的本地磁盘里,而是通过加密连接读写 etcd;Deployment、Pod、Service、Secret、ConfigMap、RBAC、Lease 以及大量控制器状态,最终都可能表现为 etcd 中的键值和版本记录。

因此,“备份了 etcd”并不自动等于“备份了整个 Kubernetes 集群”。一个可恢复的方案至少要同时回答以下问题:

  • Snapshot 是否来自一个可验证的 etcd 数据状态?
  • 备份命令使用了什么客户端证书、CA 和权限?
  • 恢复后 etcd 的 Revision 如何处理?
  • 旧的 Watch、ResourceVersion 和控制器缓存会发生什么?
  • 控制面证书、静态 Pod 配置、加密密钥和外部依赖是否仍然存在?
  • 恢复流程是否在故障前演练过,且能达到明确的 RPO 和 RTO?

本文以 etcd v3 和 Kubernetes 当前稳定 API 语义为基础。命令行工具的具体名称和参数会随 etcd 小版本变化,生产环境应优先使用与正在运行的 etcd 版本匹配的工具,并以该版本的 etcdutletcdctl 帮助输出为准。


一、先建立正确的边界:etcd 备份到底保存了什么

1.1 Kubernetes 数据路径

典型的 Kubernetes 请求路径如下:

kubectl / client
       |
       v
   kube-apiserver
       |
       |  mTLS + etcd client certificate
       v
      etcd
       |
       v
  MVCC key-value store

例如,创建一个 Deployment 时:

  1. 客户端向 API Server 发送 HTTP 请求。
  2. API Server 进行认证、授权、准入和对象校验。
  3. API Server 将对象写入 etcd。
  4. etcd 为该次提交分配新的全局 Revision。
  5. API Server 向 Watch 缓存、控制器和其他 Watch 客户端传播变化。

etcd Snapshot 保存的是 etcd 数据库在某个一致状态下的 MVCC 数据和相关元数据。它通常能够恢复:

  • Kubernetes API 对象;
  • 对象的 resourceVersion 所对应的 Revision 信息;
  • etcd 中的租约、键值和部分内部状态;
  • Kubernetes 使用的 Secret 数据,包括可能经过加密转换后的密文。

它不会自动保存:

  • API Server、Controller Manager、Scheduler 的进程参数;
  • kubelet 本地状态和节点上的容器运行时数据;
  • 控制面证书和私钥文件;
  • kube-apiserver 的静态 Pod manifest;
  • Kubernetes 加密提供程序配置及其密钥文件;
  • 负载均衡器、DNS、外部数据库、云盘、对象存储中的数据;
  • 应用程序在集群外部保存的状态。

因此,etcd Snapshot 是“集群 API 数据备份”,而不是完整的基础设施镜像。

1.2 Snapshot 与普通文件复制不同

etcd 是一个并发读写的事务型 MVCC 数据库。直接复制正在写入的 member/snap/db 文件,不等价于通过 etcd 提供的一致性备份接口导出数据库。

正确的在线备份方式是让 etcdctl 或兼容工具通过 etcd 客户端协议调用 Snapshot 接口:

etcdctl snapshot save
       |
       | 经过 TLS 认证
       v
etcd endpoint
       |
       v
线性一致的数据库快照

这样做的关键原因是:Snapshot 必须代表一个可恢复的逻辑数据库状态,而不是某个时刻磁盘文件中可能互相不匹配的若干页。


二、Snapshot 的工作机制和一致性边界

2.1 在线 Snapshot 的语义

以单个 endpoint 为例:

export ETCDCTL_API=3

etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/apiserver-etcd-client.crt \
  --key=/etc/kubernetes/pki/apiserver-etcd-client.key \
  snapshot save /backup/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db

这条命令的含义是:

  • ETCDCTL_API=3:明确使用 etcd v3 API;
  • --endpoints:指定 etcd 客户端地址,而不是 peer 地址;
  • --cacert:验证 etcd 服务端证书;
  • --cert--key:向 etcd 证明客户端身份;
  • snapshot save:请求 etcd 导出一致性 Snapshot;
  • 最后一个参数:本地保存路径。

生产环境中,127.0.0.1:2379、证书路径和证书名称都可能不同。kubeadm 集群常见路径是 /etc/kubernetes/pki/etcd/,但这不是所有发行版、托管 Kubernetes 或外部 etcd 部署的规范。

成功时通常会看到类似输出:

{"level":"info","msg":"opened snapshot stream; downloading"}
{"level":"info","msg":"fetch completed","snapshot-size":"..."}
Snapshot saved at /backup/etcd-snapshot-20250101-120000.db

输出格式会随工具版本变化。真正重要的结果是:命令退出码为 0、目标文件存在、后续校验能够读取该文件。

2.2 从多个 endpoint 备份时应该怎样选

etcd 集群通常有多个成员,但一次 Snapshot 只需要从一个健康 endpoint 获取。可以先检查成员状态:

etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/apiserver-etcd-client.crt \
  --key=/etc/kubernetes/pki/apiserver-etcd-client.key \
  endpoint status --write-out=table

常见输出包含:

  • endpoint;
  • member ID;
  • 当前 Revision;
  • 是否为 leader;
  • 数据库大小;
  • Raft term 和 index。

如果一个 endpoint 访问失败,可以从健康的其他成员执行 Snapshot。Snapshot 的一致性由 etcd 协议保证,不要求备份端一定连接 leader;但必须使用有权限、TLS 配置正确且能够完成读取的客户端。

检查健康状态:

etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/apiserver-etcd-client.crt \
  --key=/etc/kubernetes/pki/apiserver-etcd-client.key \
  endpoint health

健康检查成功只说明请求在当前时刻可以完成,不代表:

  • Snapshot 已经复制到异地;
  • 备份文件没有损坏;
  • 恢复后的 Kubernetes 控制面一定能启动;
  • 应用外部数据已经备份。

2.3 Snapshot 文件必须被验证和异地保存

保存完成后,使用与 etcd 版本匹配的工具检查 Snapshot。较新的 etcd 发行版通常提供 etcdutl

etcdutl snapshot status /backup/etcd-snapshot-20250101-120000.db --write-out=table

部分版本仍使用:

etcdctl snapshot status /backup/etcd-snapshot-20250101-120000.db --write-out=table

可能看到类似信息:

+-----------+----------+------------+---------+
| HASH      | REVISION | TOTAL KEYS |    TOTAL SIZE |
+-----------+----------+------------+---------+
| ...       | 1842301  | 12543      | ...     |
+-----------+----------+------------+---------+

这里的 REVISION 是该 Snapshot 中数据库的全局 Revision。它不是 Kubernetes 中某个单独对象的版本,也不是 Git commit。

建议同时记录文件摘要:

sha256sum /backup/etcd-snapshot-20250101-120000.db \
  > /backup/etcd-snapshot-20250101-120000.db.sha256

sha256sum --check \
  /backup/etcd-snapshot-20250101-120000.db.sha256

文件应至少有以下保存路径:

  1. 控制面本地临时目录;
  2. 与控制面故障域不同的存储;
  3. 与生产凭据具有不同访问控制的备份系统。

只把 Snapshot 放在 etcd 所在节点的本地磁盘上,无法抵御节点磁盘损坏、误删或整机丢失。


三、证书:Snapshot 能备份数据,但不能替你建立 TLS 信任

3.1 etcd 中的客户端证书与 peer 证书不是一回事

etcd 常见两类 TLS 通信:

API Server / etcdctl  -- client TLS --> etcd client endpoint
etcd member A         -- peer TLS   --> etcd member B

客户端访问一般使用:

  • etcd 服务端证书;
  • 签发该证书的 CA;
  • 客户端证书;
  • 客户端私钥。

成员之间同步则使用 peer 证书和 peer CA。两者用途、SAN、授权身份和配置参数可能不同,不能因为“都是证书”就互换。

例如,API Server 访问 etcd 的参数通常类似:

--etcd-servers=https://127.0.0.1:2379
--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key

etcdctl 使用的 --cacert--cert--key 与这些参数表达的是同一类 TLS 关系,但路径和文件可能不同。

3.2 TLS 参数分别解决什么问题

假设命令失败:

transport: authentication handshake failed

可能原因包括:

  • CA 不对,客户端无法验证服务端;
  • 客户端证书已过期;
  • 客户端证书的签发 CA 不被 etcd 信任;
  • 私钥与证书不匹配;
  • 证书 SAN 不包含访问地址;
  • etcd 要求客户端证书认证,但没有提供证书;
  • 连接的是 peer 端口而不是 client 端口。

检查证书有效期:

openssl x509 \
  -in /etc/kubernetes/pki/apiserver-etcd-client.crt \
  -noout -subject -issuer -dates -ext extendedKeyUsage

检查私钥与证书是否匹配,可以比较公钥摘要:

openssl x509 -in apiserver-etcd-client.crt -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | sha256sum

openssl pkey -in apiserver-etcd-client.key -pubout \
  | openssl pkey -pubin -outform DER \
  | sha256sum

两个摘要应一致。

3.3 证书本身可能包含高权限身份

etcd 使用基于证书身份的认证时,客户端证书可能对应管理员或特定用户。备份脚本若能读取这个证书和私钥,就可能拥有读取 Secret、修改集群状态甚至删除全部键值的能力。

所以应同时保护:

  • Snapshot 文件;
  • etcd CA;
  • 客户端证书;
  • 客户端私钥;
  • Kubernetes 加密密钥;
  • 备份系统中的访问令牌。

--insecure-skip-tls-verify 会跳过服务端证书验证,不应作为生产修复手段。它可能让命令“成功”,但掩盖了错误 endpoint、DNS 劫持或错误 CA 的问题。


四、Revision:为什么恢复不是简单地“把文件放回去”

4.1 Revision 的定义

etcd v3 使用 MVCC,即多版本并发控制。每次成功提交事务,etcd 都会推进一个全局 Revision:

Rn+1>RnR_{n+1} > R_n

其中:

  • RnR_n 表示第 nn 次提交后的全局数据库 Revision;
  • 一个事务即使包含多个键的修改,也只推进一次全局 Revision;
  • 同一个事务内写入的多个键通常具有相同的 mod_revision
  • create_revision 表示键首次创建时的 Revision;
  • version 表示该键被修改的次数。

例如,依次执行:

事务 T1:创建 /demo/a
事务 T2:创建 /demo/b,并修改 /demo/a
事务 T3:删除 /demo/b

可能得到:

当前状态 create_revision mod_revision version
/demo/a 存在 101 102 2
/demo/b 已删除 102 103 1

数据库当前 Revision 为 103。即使 /demo/b 当前不存在,删除事件仍然属于 Revision 103 的历史变化。

4.2 Kubernetes 的 resourceVersion 与 etcd Revision

Kubernetes API 对象中的 metadata.resourceVersion 通常承载与存储层版本相关的标识。对于使用 etcd 的 Kubernetes 集群,它通常与 etcd 的 MVCC Revision 有直接关系,但客户端不应把它当作可进行算术运算的业务版本号。

以下操作依赖 ResourceVersion 语义:

  • Watch 从某个版本之后继续接收事件;
  • 控制器根据版本判断缓存是否过期;
  • 通过条件更新避免覆盖并发修改;
  • API Server 的缓存与存储层对齐。

因此,Revision 不是可有可无的诊断字段。它连接了“数据库状态”“Watch 事件”和“控制器缓存”。

4.3 恢复原始 Revision 的危险

假设:

  1. Snapshot 在 Revision 1000 时创建;
  2. 集群后来运行到 Revision 1200;
  3. 灾难发生;
  4. 从 Revision 1000 的 Snapshot 恢复。

如果恢复后的数据库仍从 1000 继续递增,就可能出现 Revision 重用:

灾难前:
  1000 -- 1001 -- ... -- 1200

恢复后:
  1000 -- 1001 -- ...

客户端可能已经见过 1100 或 1200。恢复后再次出现 1001、1002 等旧编号,会破坏“版本持续向前”的假设。

一个具体的 Watch 场景:

控制器缓存已经处理到 resourceVersion=1200
恢复后的 etcd 当前 revision=1000
控制器重新建立 watch,使用起始版本 1200

此时可能发生:

  • 从 1200 开始的 Watch 暂时收不到恢复后新增的事件;
  • 控制器认为 Watch 起点非法;
  • API Server 的缓存与底层存储出现不一致;
  • 某些资源状态长时间不被重新协调;
  • 控制器重启后通过 LIST 能恢复一部分状态,但不能把“依赖旧 Watch 的所有假设”视为自动成立。

实际表现取决于 Kubernetes、API Server 存储缓存、控制器重启时机和使用的 etcd 版本,因此不能用“恢复成功且 API Server 能启动”证明 Revision 处理正确。


五、--bump-revision--mark-compacted 的逻辑

5.1 Revision bump 的目的

恢复时可以让 etcd 在 Snapshot 的原始 Revision 上增加一个人工偏移量:

Rrestored=Rsnapshot+BR_{\text{restored}} = R_{\text{snapshot}} + B

其中:

  • RsnapshotR_{\text{snapshot}} 是 Snapshot 的 Revision;
  • BB 是恢复时指定的 revision bump;
  • RrestoredR_{\text{restored}} 是恢复数据库启动时对外呈现的 Revision。

例如:

Snapshot revision = 1,000,000
bump              = 1,000,000,000
restored revision = 1,001,000,000

这不是伪造丢失期间真实发生过的每一个事件,也不会恢复 Snapshot 之后实际发生的对象变更。它只是在数值上避免旧 Revision 被再次使用。

5.2 bump 值应如何理解

如果能估计灾难期间可能产生的 Revision 数量,可以令:

B>ΔRlostB > \Delta R_{\text{lost}}

其中 ΔRlost\Delta R_{\text{lost}} 是从 Snapshot 创建到故障点之间可能发生的 Revision 增量。

但在灾难恢复中,通常无法准确知道:

  • Snapshot 创建后 API Server 又提交了多少事务;
  • 恢复期间是否有其他控制面仍在写入;
  • 客户端缓存已经观察到多大的 Revision;
  • 是否存在多个控制面或外部 etcd 客户端。

因此,生产系统常使用一个足够大的安全偏移量,而不是试图精确重建丢失的 Revision。这个数值不是 Kubernetes API 的固定常量,也不存在适用于所有集群的“官方万能值”。选择时要考虑 Revision 的数值范围、工具版本和长期运行时间,不能无条件复制某个示例数字。

5.3 mark-compacted 的目的

仅仅提高当前 Revision 还不够。假设 Snapshot 的历史 Revision 最高为 1000,恢复后经过 bump 当前 Revision 为 1,000,001,但 etcd 仍可能认为 500 是一个可用的历史 Watch 起点。

--mark-compacted 用于把恢复后的历史边界标记为已压缩,使从过旧 Revision 开始的 Watch 得到 ErrCompacted,客户端必须重新 LIST 当前状态,再从新的 Revision 建立 Watch。

恢复逻辑可以抽象为:

恢复前的 Snapshot:
  当前 revision = 1000

恢复并 bump:
  当前 revision = 1000 + B

标记 compacted:
  旧 revision 范围被视为不可继续 watch

这会迫使客户端走“重新同步”路径。对 Kubernetes 控制器而言,重新 LIST 当前对象并重建缓存,通常比悄悄沿用与恢复前不兼容的旧 Watch 更安全。

5.4 使用恢复工具

在支持这些参数的 etcd 版本中,命令形态通常类似:

etcdutl snapshot restore /backup/etcd-snapshot.db \
  --data-dir=/var/lib/etcd-restored \
  --name=etcd-restore-1 \
  --initial-cluster=etcd-restore-1=https://192.0.2.10:2380 \
  --initial-cluster-token=etcd-restore-token \
  --initial-advertise-peer-urls=https://192.0.2.10:2380 \
  --bump-revision=1000000000 \
  --mark-compacted

参数作用:

  • --data-dir:生成全新的 etcd 数据目录;
  • --name:新成员名称;
  • --initial-cluster:新集群初始成员映射;
  • --initial-cluster-token:避免误加入旧集群;
  • --initial-advertise-peer-urls:新成员对外宣布的 peer 地址;
  • --bump-revision:提高恢复后的 Revision;
  • --mark-compacted:将旧历史视为不可继续 Watch。

旧版 etcd 常见命令是:

etcdctl snapshot restore /backup/etcd-snapshot.db \
  --data-dir=/var/lib/etcd-restored

但参数支持并不完全一致。应先执行:

etcdutl snapshot restore --help
etcdctl snapshot restore --help

不要直接把某个版本的恢复命令复制到另一个版本。


六、恢复拓扑:不要把恢复目录直接塞回原集群

6.1 恢复是“建立新 etcd 集群”,不是复制一个成员

etcd 数据目录包含成员身份、集群元数据和 Raft 状态。将 Snapshot 恢复到一个新目录后,应把它作为新集群的初始数据来源,而不是把恢复目录挂到仍在运行的旧 etcd 集群上。

错误做法的典型形式:

旧 etcd 集群仍在运行
        |
        +-- 直接覆盖某个成员的数据目录
        |
        +-- 启动恢复后的成员

这可能导致:

  • member ID 或 cluster ID 不匹配;
  • Raft 日志与其他成员不一致;
  • 新成员误加入旧集群;
  • 反复启动、拒绝连接或数据分叉;
  • API Server 在多个不一致的 etcd 状态之间读写。

正确思路是:

停止旧控制面写入
        |
        v
隔离或销毁旧 etcd 拓扑
        |
        v
从 Snapshot 创建新的初始 etcd 集群
        |
        v
让 API Server 只连接新集群
        |
        v
验证 Kubernetes 对象和控制器行为

6.2 单成员恢复的示例流程

以下流程假设:

  • 使用单成员 etcd 进行演练;
  • 新节点地址为 192.0.2.10
  • Snapshot 已被校验;
  • kube-apiserver 是静态 Pod;
  • 具体证书路径和 manifest 参数已根据环境确认。

第一步:阻止旧 API Server 继续写入

在恢复旧集群时,必须先停止或隔离 API Server、Controller Manager、Scheduler 和可能访问 etcd 的其他客户端。否则恢复过程可能出现两套控制面同时写入不同数据源。

对静态 Pod 集群,不应盲目删除 Pod 后认为服务已停止。应检查对应 manifest 目录和容器状态,例如:

crictl ps | grep -E 'kube-apiserver|etcd'

实际操作方式由 kubeadm、系统服务、托管平台或发行版决定。

第二步:保存旧目录,而不是立即删除

mv /var/lib/etcd /var/lib/etcd.before-restore-$(date +%Y%m%d-%H%M%S)
mkdir -p /var/lib/etcd

保留旧目录有两个作用:

  • 如果发现恢复流程或 Snapshot 选错,可以回到原现场;
  • 可以对比故障前后的日志和数据目录。

不要在未经确认前使用:

rm -rf /var/lib/etcd

第三步:恢复 Snapshot

etcdutl snapshot restore /backup/etcd-snapshot.db \
  --data-dir=/var/lib/etcd \
  --name=etcd-restore-1 \
  --initial-cluster=etcd-restore-1=https://192.0.2.10:2380 \
  --initial-cluster-token=etcd-restore-20250101 \
  --initial-advertise-peer-urls=https://192.0.2.10:2380 \
  --bump-revision=1000000000 \
  --mark-compacted

这一步只是在磁盘上生成恢复后的数据目录,并不意味着 etcd 已经对外提供服务。

第四步:配置 etcd 进程

启动参数必须与恢复拓扑匹配。典型配置还包括:

--listen-client-urls=https://0.0.0.0:2379
--advertise-client-urls=https://192.0.2.10:2379
--listen-peer-urls=https://0.0.0.0:2380
--initial-advertise-peer-urls=https://192.0.2.10:2380
--cert-file=...
--key-file=...
--trusted-ca-file=...
--client-cert-auth=true
--peer-cert-file=...
--peer-key-file=...
--peer-trusted-ca-file=...
--peer-client-cert-auth=true

如果是单成员恢复,--initial-cluster 中只能包含恢复后的新拓扑成员。多成员恢复时,应从同一个 Snapshot 为每个成员分别生成数据目录,并使用一致的初始集群配置;不能把一个已经启动过的成员目录复制成多个成员目录而不修改成员身份。

第五步:先验证 etcd,再启动 API Server

etcdctl \
  --endpoints=https://192.0.2.10:2379 \
  --cacert=/path/to/etcd-ca.crt \
  --cert=/path/to/client.crt \
  --key=/path/to/client.key \
  endpoint health

etcdctl \
  --endpoints=https://192.0.2.10:2379 \
  --cacert=/path/to/etcd-ca.crt \
  --cert=/path/to/client.crt \
  --key=/path/to/client.key \
  endpoint status --write-out=table

验证重点包括:

  • endpoint 可以完成健康检查;
  • 当前 Revision 已高于 Snapshot 原始 Revision;
  • 成员名称、成员数量和 leader 状态符合新拓扑;
  • 数据库大小和 key 数量没有明显异常;
  • etcd 日志没有 TLS、cluster ID、peer URL 或 WAL 错误。

确认 etcd 可用后,再让 API Server 指向恢复后的 client endpoint。API Server 的 etcd 地址、CA、客户端证书和私钥必须与恢复环境匹配。


七、恢复后的 Kubernetes 验证不能只看 kubectl get nodes

7.1 API Server 可访问不等于数据恢复正确

基础检查:

kubectl get --raw='/readyz?verbose'
kubectl get nodes -o wide
kubectl get namespaces
kubectl get pods -A

这些命令分别验证:

  • API Server 的就绪检查;
  • Node 对象是否存在;
  • Namespace 等基础对象是否存在;
  • 工作负载对象是否能够被列出。

检查 API 资源和事件:

kubectl api-resources
kubectl get events -A --sort-by=.lastTimestamp

但还需要验证对象内容,而不仅是对象数量:

kubectl -n production get deployment web -o yaml
kubectl -n production get secret registry-credentials -o yaml
kubectl get clusterrolebinding -o wide

读取 Secret 时要注意:恢复的是 etcd 中保存的密文或明文存储形式,并不代表应用一定能使用它。若启用了静态加密,API Server 必须使用正确的 encryption configuration 和对应密钥才能解密。

7.2 检查控制器是否重新建立 Watch

恢复后,Controller Manager、Scheduler、Ingress Controller、Operator 等组件可能持有恢复前的缓存或 ResourceVersion。应观察其日志中的以下信号:

  • too old resource version
  • watch closed
  • compacted
  • resource version ... is too old
  • 反复 LIST、WATCH;
  • 控制器队列是否持续堆积。

典型的安全路径是:

旧 Watch 起点失效
       |
       v
客户端收到 ErrCompacted / 旧版本错误
       |
       v
重新 LIST 当前对象
       |
       v
从新的 resourceVersion 建立 Watch

如果某个控制器没有正确处理重新 LIST,可能出现“对象存在但控制器不再协调”的故障。此时可以通过滚动重启相关控制器,迫使其丢弃进程内缓存并重新同步,但这不是修复控制器 bug 的替代方案。

7.3 验证 Lease、Service、DNS 和工作负载

Kubernetes 的部分组件使用 Lease:

kubectl get lease -A

恢复旧 Snapshot 后,Lease 可能包含灾难前的 leader 选举状态。控制器通常会根据租约时间和重新选举机制恢复,但应观察是否出现多个 leader、持续抢锁或控制器不再工作。

Service 和 DNS 也应单独验证:

kubectl -n kube-system get pods
kubectl -n kube-system get svc
kubectl -n kube-system get endpointslices

若使用 CoreDNS,验证:

kubectl -n kube-system logs deploy/coredns
kubectl run dns-test --image=busybox:1.36 --restart=Never \
  --rm -it -- nslookup kubernetes.default.svc

镜像版本需要根据实际仓库和网络条件调整。DNS 查询成功只能证明集群内解析链路基本可用,不能证明外部 DNS、Ingress、负载均衡器或云厂商控制器已经恢复。


八、一个完整的灾难恢复状态机

可以把恢复过程建模为以下状态转换:

stateDiagram-v2
    [*] --> Normal: etcd 集群正常
    Normal --> BackupVerified: Snapshot 已保存并校验
    Normal --> Incident: 控制面或存储故障
    Incident --> Quiesced: 停止旧控制面写入
    Quiesced --> SnapshotSelected: 选择目标 Snapshot
    SnapshotSelected --> NewEtcdRestored: 恢复新 etcd 数据目录
    NewEtcdRestored --> EtcdVerified: TLS、健康、Revision 验证
    EtcdVerified --> ApiServerConnected: API Server 指向新 etcd
    ApiServerConnected --> ControllersResynced: 控制器重新 LIST/WATCH
    ControllersResynced --> WorkloadsValidated: API、DNS、节点、应用验证
    WorkloadsValidated --> Recovered: 达到恢复验收条件
    SnapshotSelected --> Rollback: Snapshot 不可读或版本不兼容
    EtcdVerified --> Rollback: 拓扑或证书配置错误
    Rollback --> Incident

这里最容易被跳过的是 ControllersResynced。API Server 能返回 HTTP 200,只说明请求路径存在;只有控制器重新建立缓存、节点重新上报、Service 和应用依赖恢复后,集群才接近业务可用。


九、常见误解、失败表现与诊断路径

9.1 误解:有 Snapshot 就有完整集群备份

失败表现:

  • API Server 启动失败;
  • etcd 能启动,但 API Server 报 TLS 错误;
  • Secret 读取时报解密错误;
  • 静态 Pod manifest 丢失;
  • 节点存在于 API 对象中,但 kubelet、容器运行时和本地卷数据不存在。

诊断路径:

  1. 检查控制面 manifest 是否存在;
  2. 检查 API Server 的 etcd 参数;
  3. 检查 CA、客户端证书和私钥;
  4. 检查 encryption configuration 和密钥;
  5. 检查节点本地磁盘、PV、云盘和外部依赖。

9.2 误解:直接复制 member/snap/db 就可以备份

失败表现:

  • 恢复后数据库无法打开;
  • WAL、snapshot 和成员元数据不一致;
  • etcd 启动但出现数据损坏;
  • 备份文件在测试环境中偶尔能用,在线上压力下失败。

正确做法:

使用 etcd 的 Snapshot API。只有在明确停止 etcd、理解文件一致性并使用匹配工具的离线场景下,才讨论直接操作数据目录,而且这不应替代标准 Snapshot。

9.3 误解:恢复 Snapshot 后 Revision 自然会正确

失败表现:

  • 控制器持续报告旧 ResourceVersion;
  • Watch 反复断开;
  • 对象存在,但 Operator 不再处理变更;
  • API Server 或客户端出现 too old resource version

正确做法:

根据 etcd 版本使用 --bump-revision--mark-compacted,并重启或验证所有依赖 Watch 的组件。恢复后应确保客户端能够在旧 Watch 失效时重新 LIST。

9.4 误解:恢复一个成员就等于恢复三成员集群

失败表现:

  • 新成员无法加入旧集群;
  • peer 连接反复失败;
  • cluster ID mismatch
  • etcd 没有 leader;
  • 成员之间数据库 Revision 不一致。

正确做法:

先决定恢复目标是:

  • 临时单成员 etcd,用于快速恢复控制面;
  • 重新建立三成员或更多成员的 etcd 集群;
  • 迁移到新的控制面拓扑。

然后从同一 Snapshot 按新拓扑恢复,避免混用旧成员目录和新 Snapshot。

9.5 误解:加 --insecure-skip-tls-verify 可以修复证书问题

这只会跳过服务端身份验证,不能修复:

  • 客户端证书过期;
  • 私钥不匹配;
  • etcd 不信任客户端 CA;
  • 使用错误端口;
  • 证书 SAN 不匹配;
  • etcd 未启用对应认证模式。

诊断时应先检查 endpoint、证书链、有效期、SAN、Extended Key Usage 和 etcd 启动参数,而不是直接降低 TLS 安全性。


十、备份策略中的 RPO、RTO 与版本兼容性

10.1 RPO 决定备份频率

RPO 是允许丢失的最大数据时间窗口。若 Snapshot 每小时生成一次,理论上最多可能丢失接近一小时内写入 etcd 的对象变更。

但“对象变更丢失”不等于“应用数据丢失”:

  • Deployment 的副本数可能在 Snapshot 后发生变化;
  • Secret 轮换可能丢失;
  • Job、Lease、Event 等短生命周期对象可能回到旧状态;
  • PV 中的数据库文件可能已经比 etcd 中的 PVC 状态更新,或者反过来。

对于有状态应用,必须把 etcd Snapshot 与应用数据备份放进同一个恢复设计中。

10.2 RTO 不只取决于 Snapshot 大小

RTO 是从故障发生到服务恢复所允许的最长时间。影响因素包括:

  • 能否快速获取 Snapshot;
  • 是否有可用控制面节点;
  • 证书是否仍有效;
  • 是否需要重新创建负载均衡器;
  • 是否要重建 DNS;
  • 是否要恢复外部数据库和云盘;
  • 控制器重新同步需要多长时间;
  • 恢复后是否必须重启节点或工作负载。

因此,只测量 snapshot restore 命令耗时,不能代表 Kubernetes 的真实恢复时间。

10.3 工具版本必须与 etcd 版本匹配

需要明确区分:

  • etcdctl:主要用于访问正在运行的 etcd,也包含部分 Snapshot 操作;
  • etcdutl:用于部分离线维护和 Snapshot 恢复操作;
  • etcd server:真正读取数据目录并参与 Raft。

同名参数在不同版本中可能不存在、弃用或改变默认行为。特别是恢复时的 revision bump、compact 标记和输出格式,应以当前 etcd 版本的帮助信息和发行说明为准。

Kubernetes 版本还会影响:

  • API Server 使用的 etcd 主版本;
  • 存储版本和资源转换;
  • 控制器对 Watch 错误的处理;
  • 加密配置格式;
  • 静态 Pod manifest 和证书管理方式。

不要用“当前工具”恢复一个完全不同主版本生成的数据库而不做兼容性验证。恢复前应保留:

etcd --version
etcdctl version
etcdutl version
kube-apiserver --version
kubectl version --client

十一、灾难演练应当验证什么

一次有效演练不是“命令执行成功”,而是从备份文件开始,验证到业务读写路径结束。

11.1 演练前固定输入

演练记录至少应包含:

  • Snapshot 文件及 SHA-256;
  • Snapshot 创建时间和数据库 Revision;
  • etcd、Kubernetes、kubectl 工具版本;
  • etcd client/peer 地址;
  • CA、客户端证书和私钥来源;
  • API Server manifest;
  • 加密配置和密钥;
  • 控制面节点、负载均衡器和 DNS 配置;
  • PV、外部数据库和对象存储备份位置。

11.2 演练步骤

1. 验证 Snapshot 可读

etcdutl snapshot status snapshot.db --write-out=table

如果工具无法读取 Snapshot,应在真正故障前解决版本或文件损坏问题。

2. 在隔离环境恢复

恢复环境不能直接连接生产 etcd,也不能让生产 API Server 误连接演练 etcd。初始 cluster token、网络、DNS 和证书 SAN 应明确隔离。

3. 验证 Revision

记录:

Snapshot revision
restore bump
恢复后的 revision
compaction 边界

并确认恢复后的 Revision 不会回退到控制面已观察过的旧区间。

4. 验证 Kubernetes API

kubectl get --raw='/readyz?verbose'
kubectl get ns
kubectl get nodes
kubectl get pods -A
kubectl auth can-i get secrets --all-namespaces

最后一条命令用于验证当前 kubeconfig 身份是否有权限,不代表所有组件证书都正确。

5. 验证控制器和 Watch

观察:

kubectl -n kube-system logs deploy/coredns
kubectl -n kube-system get lease
kubectl get events -A --sort-by=.lastTimestamp

同时查看 Controller Manager、Operator 和关键控制器日志,确认它们完成重新 LIST/WATCH,而不是停留在旧缓存。

6. 验证应用读写和外部依赖

至少测试:

  • 创建并删除临时 Namespace;
  • 创建一个临时 Deployment;
  • Service 到 Pod 的访问;
  • 集群内 DNS;
  • Ingress 或负载均衡入口;
  • Secret 解码和应用启动;
  • PVC 绑定与实际卷可读写;
  • 外部数据库、对象存储和消息系统连接。

演练完成后,应清理临时资源,防止测试对象进入正式环境。

11.3 用验收条件结束演练

可以把恢复验收写成可测量条件:

etcd:
  endpoint health 成功
  成员拓扑正确
  Revision 已提升
  无持续 WAL、TLS、peer 错误

Kubernetes:
  /readyz 返回成功
  API 对象数量和关键对象内容符合预期
  控制器完成重新同步
  Node Ready 状态恢复
  CoreDNS 可解析
  关键工作负载可启动并访问外部依赖

运维:
  记录实际恢复时间
  记录丢失的对象变更范围
  记录需要人工介入的步骤
  备份文件和证书权限符合要求

只有这些条件全部满足,才能说明恢复流程具备可操作性。


十二、生产取舍:单成员、三成员与托管控制面

单成员 etcd 恢复路径简单,适合灾难后的临时控制面,但任何一次 etcd 节点故障都可能让 API Server 不可用。三成员 etcd 可以容忍一个成员故障,前提是剩余成员仍能形成 quorum;五成员可以容忍两个成员故障,但会增加资源、网络和运维复杂度。

Snapshot 不能替代高可用:

高可用:
  通过 quorum 抵御部分实时故障

Snapshot:
  在数据损坏、误删、整集群丢失时提供时间点恢复

两者解决的是不同故障类别。

托管 Kubernetes 中,云厂商可能管理 etcd,并提供控制面备份或自动恢复能力。此时通常不能直接访问 etcd 端口、证书或数据目录,平台的恢复保证、备份保留策略和恢复粒度应以厂商文档为准。即使控制面由云厂商恢复,应用级资源、PV、外部数据库、DNS 和密钥管理系统仍可能需要用户单独备份。


结语

etcd Snapshot 的核心价值,是把 Kubernetes API 数据保存为一个可验证、可迁移、可恢复的 MVCC 状态。真正困难的部分不在于执行一次 snapshot save,而在于理解恢复后的系统仍然面对四个连续问题:

  1. 证书和 TLS 信任是否能让新控制面访问新 etcd;
  2. 恢复拓扑是否是一个独立且一致的新 etcd 集群;
  3. Revision 是否向前推进,并让旧 Watch 正确失效;
  4. 控制面、节点、DNS、存储和外部依赖是否完成重新收敛。

一个合格的方案应同时保存 Snapshot、版本信息、证书和密钥、控制面配置以及外部依赖说明,并定期在隔离环境中执行完整恢复。备份文件只有在能够被读取、恢复、连接、重新同步并完成业务验证时,才真正具备灾难恢复价值。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。