Kubernetes 控制面 etcd 快照与恢复
API Server 恢复响应后,值班人员看到 Node、Namespace 和 Deployment 都回来了,以为抢修已经结束。几分钟后,控制器开始反复创建又删除对象,部分工作负载保留了故障后的缓存状态,另一些回到了快照时点。复盘发现,团队把旧 snapshot 直接恢复成了一个较低 revision 的 etcd,却没有让 informer 的旧 watch 失效;恢复期间还有一台旧成员没有完全隔离,短暂地向错误集群提供服务。
另一支团队在 kubeadm 节点上找到了 /etc/kubernetes/tmp/kubeadm-backup-etcd-*,便把它当成长期恢复点。真正需要恢复时,目录早已被升级清理策略删除;即使文件仍在,它也只对应一次 kubeadm 升级事务,external etcd 场景甚至可能为空。高可用成员仍然在线不等于存在可回退的逻辑状态,升级临时目录也不等于有经过验证的备份。
第一步识别部署架构与 etcd 所有者
在 kubeadm 默认 stacked 布局中,每个控制平面节点运行一个 local etcd 静态 Pod,manifest 通常位于 /etc/kubernetes/manifests/etcd.yaml,默认数据目录通常是 /var/lib/etcd。路径只是常见值,真实配置以静态 Pod 的 --data-dir、URL、name、volume mount 和镜像为准。
sudo grep -E -- '--(data-dir|listen-client-urls|advertise-client-urls|name|initial-cluster)' \
/etc/kubernetes/manifests/etcd.yaml
sudo grep -E 'image:|mountPath:|hostPath:' /etc/kubernetes/manifests/etcd.yaml
sudo crictl ps --name etcd如果 ClusterConfiguration.etcd.external 指向独立 endpoint,etcd 由外部集群和另一套成员、证书、备份责任承载。kubeadm 节点上的 local etcd 路径不再是权威数据。先查看 kubeadm 配置和 API Server 的 --etcd-servers,再去外部 etcd 的受控管理主机执行快照;不要在控制平面节点猜测数据目录。
kubectl -n kube-system get configmap kubeadm-config -o yaml
sudo grep -E -- '--etcd-(servers|cafile|certfile|keyfile)' \
/etc/kubernetes/manifests/kube-apiserver.yamlEKS、GKE、AKS 等托管集群通常不向租户暴露内部 etcd endpoint、成员或 client certificate。云厂商维护内部状态库的可靠性,不代表用户能够下载原始快照或执行任意时点回退。此时应使用厂商公开的集群状态与工作负载备份产品,按支持资源、PVC 类型、KMS、目标集群和地域限制验证;不能在 worker node 安装 etcdctl 寻找不存在的入口。责任判断可分别查阅 AWS Backup for EKS、Backup for GKE 和 AKS 备份恢复指导。
让工具版本跟随正在运行的成员
etcdctl 连接运行中的 etcd,用于 endpoint 检查和在线 snapshot save;etcdutl 操作本地 snapshot 与 data directory,用于 snapshot status 和 snapshot restore。etcd 3.6 起,旧教程中的 etcdctl snapshot status/restore 已不可用,工具职责变化可在 etcd 3.6 发布说明中核对。
先从静态 Pod 镜像或 external etcd 进程获取运行版本,再选择相同 major.minor 的 etcdctl 与 etcdutl。Kubernetes 发行版会校验自己的 etcd 兼容组合;不要因为 etcd 上游出现更新版本就绕过 Kubernetes 版本矩阵单独替换 backing store。
sudo grep 'image:' /etc/kubernetes/manifests/etcd.yaml
etcdctl version
etcdutl version二进制从 etcd Releases选择与目标成员匹配的正式 release,校验发布方提供的 checksum 后安装到受控备份主机。自动化脚本把版本作为显式输入并在不匹配时失败,不要使用浮动 latest。若使用容器运行工具,同样固定镜像 digest,并只读挂载证书、只写挂载 staging 目录。
证书不是连接装饰,而是集群根级凭证
kubeadm 默认 local etcd 常使用 /etc/kubernetes/pki/etcd/ca.crt、healthcheck-client.crt 和 healthcheck-client.key 进行 client TLS。/etc/kubernetes/admin.conf 是访问 Kubernetes API 的 kubeconfig,不能替代 etcd client certificate。实际路径仍应从 manifest 的参数和挂载读取。
sudo grep -E -- '--(trusted-ca-file|cert-file|key-file|peer-trusted-ca-file|peer-cert-file|peer-key-file)' \
/etc/kubernetes/manifests/etcd.yaml
sudo openssl x509 -in /etc/kubernetes/pki/etcd/healthcheck-client.crt \
-noout -subject -issuer -dates能够读取 etcd 的客户端通常能读取 Secret、ServiceAccount、RBAC 和工作负载配置。备份账号只获得连接指定 endpoint 和写入指定加密仓库所需权限;证书与私钥不写进镜像、Git、普通 CI artifact 或日志。备份主机使用最小 OS 权限,staging 目录采用受限权限和短保留,上传后立即验证加密对象并销毁明文副本。
peer certificate 则证明成员之间的身份。隔离恢复时,成员 name、peer URL、证书 SAN 和监听地址必须一致;restore 重写 member ID 与 cluster ID,不会替你签发新证书。恢复到新主机名或新地址时,先按集群 PKI 流程签发匹配 SAN 的 client/peer/server 证书,不能关闭 TLS 校验来绕过身份错误。
在线阶段只生成和验证快照
从一个健康成员执行 snapshot save 即可获得逻辑快照,不需要从所有成员各保存一份并混用。先检查 endpoint health/status,确认 leader、版本、db size 和 revision 合理,再写入权限受控的 staging 路径。
export ETCD_ENDPOINT='https://127.0.0.1:2379'
export ETCD_CACERT='/etc/kubernetes/pki/etcd/ca.crt'
export ETCD_CERT='/etc/kubernetes/pki/etcd/healthcheck-client.crt'
export ETCD_KEY='/etc/kubernetes/pki/etcd/healthcheck-client.key'
export SNAPSHOT='/secure-staging/etcd-snapshot.db'
sudo ETCDCTL_API=3 etcdctl \
--endpoints="$ETCD_ENDPOINT" \
--cacert="$ETCD_CACERT" --cert="$ETCD_CERT" --key="$ETCD_KEY" \
endpoint health
sudo ETCDCTL_API=3 etcdctl \
--endpoints="$ETCD_ENDPOINT" \
--cacert="$ETCD_CACERT" --cert="$ETCD_CERT" --key="$ETCD_KEY" \
endpoint status --write-out=table
sudo ETCDCTL_API=3 etcdctl \
--endpoints="$ETCD_ENDPOINT" \
--cacert="$ETCD_CACERT" --cert="$ETCD_CERT" --key="$ETCD_KEY" \
snapshot save "$SNAPSHOT"
sudo etcdutl --write-out=table snapshot status "$SNAPSHOT"
sudo sha256sum "$SNAPSHOT"预期 endpoint health 返回健康,snapshot save 报告保存成功,etcdutl snapshot status 能输出 hash、revision、total keys 和 size。记录 Kubernetes 版本、etcd 镜像 digest、工具版本、member list、endpoint status、snapshot SHA-256、revision、总键数、大小、证书有效期和上传对象版本。单有“文件存在”不能证明文件完整,也不能证明恢复工具和证书仍可用。
直接复制运行中数据目录里的 member/snap/db 风险更高:它可能遗漏还在 WAL 中的数据,也缺少 etcdctl snapshot save 写入的完整性 hash。只有来自已停止且来源明确的数据目录时,才把 backend db 作为最后恢复材料,并记录为何需要 --skip-hash-check。正常备份链不要依赖该例外。
用可验证 fixture 证明快照时点
在一次性 kubeadm 实验集群中创建 namespaced 与 cluster-scoped fixture,并保存 UID、resourceVersion 与内容摘要。快照后修改一个对象、删除一个对象、再创建一个对象。这样恢复结果能够区分快照前后,而不是只看 API Server 是否启动。
kubectl create namespace etcd-restore-lab
kubectl -n etcd-restore-lab create configmap before-snapshot \
--from-literal=state=before
kubectl create clusterrole etcd-restore-fixture \
--verb=get --resource=configmaps
kubectl -n etcd-restore-lab get configmap before-snapshot -o yaml > fixture-before.yaml
kubectl get clusterrole etcd-restore-fixture -o yaml > cluster-fixture-before.yaml
# 创建并验证 snapshot 后,再产生只属于 snapshot 之后的变化。
kubectl -n etcd-restore-lab patch configmap before-snapshot \
--type=merge -p '{"data":{"state":"after"}}'
kubectl -n etcd-restore-lab create configmap after-snapshot \
--from-literal=state=after快照前还应记录至少一个 Secret 的存在性和摘要,但不要导出明文;记录 CRD/CR、Webhook、Node 与核心组件状态,才能覆盖 cluster-scoped 对象和自定义 API。fixture 使用专用名称与 label,避免恢复后清理误伤正常资源。
恢复发生在隔离环境,而不是在线数据目录
etcd restore 的本质不是把 snapshot 文件覆盖回原目录,而是用同一 snapshot 为每个成员创建新的 data directory,并生成新的 member ID 与 cluster ID,组成新的 logical cluster。任何恢复命令都应在与原 API Server、原 etcd 成员和生产网络隔离的恢复主机上执行;输出目录必须是全新空目录。下面使用文档保留地址和 /srv/etcd-restore,刻意不指向 kubeadm 默认在线目录。
进入恢复阶段前应有可验证的阻断条件:所有旧 API Server 已停止或与恢复网络断开,所有旧 etcd member 无法加入,新旧 peer 网络不可达,原数据目录有只读保全副本,恢复主机没有生产负载均衡或 DNS 路由。任何一项无法证明,就停止执行 restore。
三成员恢复分别在对应隔离主机执行,三个节点使用同一个快照、同一 initial-cluster 和 token,每个节点只改变 name、data-dir 与本机 peer URL:
sudo install -d -m 0700 /srv/etcd-restore/cp-1
sudo etcdutl snapshot restore /restore-input/etcd-snapshot.db \
--name=cp-1 \
--data-dir=/srv/etcd-restore/cp-1/data \
--initial-cluster='cp-1=https://192.0.2.11:2380,cp-2=https://192.0.2.12:2380,cp-3=https://192.0.2.13:2380' \
--initial-cluster-token='isolated-recovery-drill' \
--initial-advertise-peer-urls=https://192.0.2.11:2380 \
--bump-revision=<capacity-derived-value> \
--mark-compactedcp-2 与 cp-3 使用自己的新目录、name 和文档保留地址。不得把 --data-dir 改成仍被 kubelet 或 etcd 进程使用的目录,也不得让这些地址路由到现网。--force-new-cluster 不是跳过隔离和成员重建的快捷按钮;仍有旧成员存活时使用它可能形成严重冲突,etcd 灾难恢复文档对此给出明确警告。
恢复产物先由一套隔离的 etcd 启动配置消费。为每个成员准备与新 name、peer/client URL、data-dir 和证书 SAN 一致的配置,启动后只从恢复网络检查:
etcdctl --endpoints=https://192.0.2.11:2379,https://192.0.2.12:2379,https://192.0.2.13:2379 \
--cacert=/restore-pki/etcd/ca.crt \
--cert=/restore-pki/etcd/healthcheck-client.crt \
--key=/restore-pki/etcd/healthcheck-client.key \
member list --write-out=table
etcdctl --endpoints=https://192.0.2.11:2379,https://192.0.2.12:2379,https://192.0.2.13:2379 \
--cacert=/restore-pki/etcd/ca.crt \
--cert=/restore-pki/etcd/healthcheck-client.crt \
--key=/restore-pki/etcd/healthcheck-client.key \
endpoint status --cluster --write-out=table预期看到三个新的 member ID、一个 leader、相同 cluster ID、合理且已提升的 revision。只有这些证据稳定后,才由正式恢复变更把 API Server 指向恢复后的 endpoint;该变更需要逐节点备份 manifest、语法检查、双人复核和明确回滚点。禁止在提供服务的节点上边写原目录边重启静态 Pod。
revision bump 为什么必须和 compact 标记一起出现
Kubernetes controller 与 informer 会缓存对象,并用 resourceVersion 建立 watch。快照恢复把 etcd 回到较低 revision;若客户端仍持有故障前较高 revision,它可能等待永远不会自然到达的版本,或继续相信已经回退的缓存。--bump-revision 把恢复后的 revision 向前推进,--mark-compacted 把旧范围标记为已 compact,使旧 watch 收到失效信号并重新 list。
bump 值不是固定魔法数字。先从正常时段的 etcd_debugging_mvcc_current_revision 增长率或连续 endpoint status 样本估计每小时 revision 增量,再乘以最大恢复点年龄和恢复窗口,并留出业务峰值余量:
bump >= peak_revision_rate × (snapshot_age + recovery_duration) × safety_factor演练要验证控制器发生 relist,而不是只看 revision 数值变大。保存 API Server 与关键 controller 的 watch/compaction 日志、恢复前后 resourceVersion 样本和对象对账。bump 太小仍可能与旧缓存区间重叠;无限放大虽不会增加数据量,却会让审计与排障失去可解释性,因此每次恢复都记录计算输入。
两个安全反例验证 hash 和成员身份
第一个反例在副本上截断 snapshot,不碰唯一恢复文件。etcdutl snapshot status 或 restore 应因 hash/格式错误失败,损坏文件不得进入成员重建。随后从对象仓库重新下载,比较上传时与下载后的 SHA-256,再执行 status。
cp /restore-input/etcd-snapshot.db /restore-input/corrupt.db
truncate -s -4096 /restore-input/corrupt.db
etcdutl --write-out=table snapshot status /restore-input/corrupt.db预期命令非零退出并报告校验或数据库读取错误。这个失败不能通过对正常 snapshot save 产物添加 --skip-hash-check 绕过;正确修复是重新取得完整副本。
第二个反例只在隔离网络中把 cp-1 的 peer URL 改成证书 SAN 不包含的地址,或给它挂载 cp-2 的 peer certificate。预期成员日志出现 TLS hostname/identity 错误,集群无法形成健康 quorum。恢复原配置和正确证书后,member list 与 endpoint health 应重新收敛。这个实验说明 member name、peer URL、member ID 和证书身份是四个相关但不同的对象,复制静态 Pod 文件不能自动建立正确身份。
API 恢复后还要与外部世界对账
API Server 重新连接后,先读取 fixture。before-snapshot 应恢复为 state=before,after-snapshot 不应存在,cluster role 与 CRD/CR 应回到对应时点。随后检查 Node、Namespace、核心 controller、Webhook、证书、ServiceAccount、工作负载与事件。恢复后的 API 可读不代表业务能够接管。
etcd 快照不会恢复 PV 后端字节、节点本地文件、镜像、/etc/kubernetes/pki、静态 Pod manifest、CNI、云负载均衡器、DNS、KMS、外部数据库或队列。快照之后发生的磁盘删除、密钥轮换、DNS 切换和消息消费不会自动回退。对账时把 Kubernetes 对象中的 volume handle、Secret 版本和外部资源 ID 与云库存逐项比较;冲突先 fencing,再决定重建、补偿或放弃旧对象。
验收至少包含 etcd member/endpoint、API 对象差异、controller relist、关键业务读写、PV 数据、外部依赖、实际 RPO/RTO,以及恢复后重新生成一次快照。最后一项能证明新逻辑集群已经重新进入保护链。
故障先按第一证据分型
| 现象 | 第一证据 | 判断与动作 |
|---|---|---|
etcdctl snapshot restore 不存在 | etcdctl version、etcdutl version | 新版本已拆分职责,使用匹配版本的 etcdutl,不要退回旧 binary 操作新数据。 |
| snapshot status 失败 | 文件 SHA-256、对象版本、etcdutl 错误 | 停止恢复并重新取得完整快照;正常在线快照不跳过 hash。 |
context deadline exceeded | endpoint health、2379 路由、TLS 日志 | 区分网络不可达、成员无 leader 和证书失败,再修复对应层。 |
x509 或 peer handshake 失败 | 证书 SAN、subject、URL、时钟 | 修正成员地址或重新签发匹配证书,不关闭 TLS 验证。 |
| 三成员无法选主 | member list、cluster ID、initial-cluster/token | 检查是否使用同一 snapshot、完整成员表和唯一恢复 token,确认旧成员已隔离。 |
| API 可读但 controller 异常 | restore flags、revision、watch/relist 日志 | 检查 bump 与 compact 标记,确认客户端重新 list。 |
| 对象恢复但 Pod/PVC 失败 | PV handle、CSI Event、外部库存 | etcd 只恢复 API 状态,继续恢复实际卷或修正失效引用。 |
| kubeadm 临时备份目录膨胀 | /etc/kubernetes/tmp/kubeadm-backup-* | 它服务升级回滚,升级验证结束后按变更规则清理,不能代替周期备份。 |
自动化要把成功定义成可恢复证据
生产快照任务可运行在受控管理主机或专用备份节点,通过健康 endpoint 创建 snapshot,写入加密 staging,执行 etcdutl snapshot status 与 SHA-256,再上传独立故障域。任务记录 run ID、源 cluster/member、revision、工具与镜像版本、对象版本、KMS key 和保留策略。上传失败或 status 失败时,不能发布“最新恢复点”索引。
监控至少观察最后一次成功快照年龄、snapshot revision 增长、文件大小突变、上传失败、仓库对象缺失、证书到期、恢复演练年龄和恢复后快照失败。文件大小稳定不等于健康;键数、revision 和业务对象数量需要一起解释。告警由平台 owner 接收,并关联可执行的隔离演练入口。
高可用 etcd 能容忍有限成员故障,却不能抵御误删、逻辑损坏、勒索删除、证书丢失或整站故障。备份仓库与集群账号、KMS 和网络应隔离,删除权限与写入权限分开,重要恢复点采用对象版本或不可变保留。仓库管理员也不应默认拥有 etcd client private key。
容量、频率和成本围绕 RPO/RTO计算
快照大小主要受 etcd backend、历史对象和碎片影响;频率受允许的数据丢失窗口、写入速率和上传能力影响。保存每次 size、total keys、revision、snapshot duration、上传字节、对象存储时间和 restore duration,观察趋势而不是设置脱离负载的固定阈值。异常膨胀要检查大量短生命周期对象、Event、CRD 数据和 compaction/defrag 状态。
恢复容量需要同时容纳原数据目录保全副本、新 data directory、snapshot、下载副本、日志和回滚空间。三成员并行恢复会增加磁盘与网络峰值;对象仓库跨区域读取、KMS 调用、不可变保留、日志索引和演练计算资源都会产生费用。云与商业产品的价格在目标账号官方计费入口核算,不写死未知单价。
RPO 较小时不要只机械提高全量快照频率。评估 etcd 写入压力、快照持续时间、上传重叠、仓库保留和演练能力;无法在目标 RTO 内下载、恢复、启动三成员并完成 API/业务对账的恢复点,即使生成得很频繁,也没有满足接管目标。
升级、回滚和退出围绕兼容组合执行
Kubernetes 或 etcd 升级前保存当前静态 Pod manifest、镜像 digest、工具版本、成员拓扑、证书状态和经过恢复验证的快照。按目标 Kubernetes 发行版支持矩阵升级,逐成员检查 health、leader、alarms 和 revision;不跨过官方要求的中间版本。kubeadm upgrade 产生的临时 etcd/manifest 备份只支撑该升级事务,验证稳定后按规则清理。
升级后的第一轮任务必须用新工具创建快照,并用匹配的 etcdutl 在隔离环境完成 status 与恢复演练。回滚不仅要还原二进制,还要确认数据格式、manifest、证书和成员状态兼容。只回滚 kubeadm 包或镜像 tag,不能证明 etcd 数据可以安全降级。
迁移到 external etcd 或托管 Kubernetes 时,先在新平台建立独立备份与恢复证据,再停止旧快照计划。旧快照仍需要对应版本的 etcdutl、PKI、成员拓扑、KMS key 和解密权限;保留期结束后按审批销毁快照、临时证书副本、恢复 data directory、旧主机账号、对象仓库授权和监控规则。
清理隔离演练而不破坏证据
先导出 member list、endpoint status、restore 日志、API fixture diff、业务校验与 RPO/RTO。确认新恢复点已经生成后,停止隔离 API Server 和 etcd,卸载恢复目录,撤销临时网络、DNS、ServiceAccount、client/peer certificate 和对象仓库访问。fixture 在确认不再用于回归后删除:
kubectl -n etcd-restore-lab delete configmap before-snapshot after-snapshot --ignore-not-found
kubectl delete clusterrole etcd-restore-fixture --ignore-not-found
kubectl delete namespace etcd-restore-lab --ignore-not-found原数据目录保全副本和输入 snapshot 按事故证据与保留策略处理,不能跟普通临时目录一起立即删除。到期销毁时记录对象版本、KMS key、审批人与结果;同时确认恢复网络、旧成员端口和临时证书已撤销。恢复演练真正结束的标志,是线上没有被覆盖,隔离集群不再运行,敏感材料按策略保留或销毁,下一份恢复点已经能够独立验证。
