Kubernetes 灾难恢复:来自三个可复现故障场景的指导

来源: Cloud Native Computing Foundation(云原生计算基金会)
作者: Saiyam Pathak、Saloni Narang | CNCF 大使
发布时间: 2026/9/10 19:37:00
原文链接: https://www.cncf.io/blog/2026/09/10/kubernetes-disaster-recovery-guidance-from-three-reproducible-failure-scenarios/


发布于 2026年9月10日,作者:Saiyam Pathak、Saloni Narang | CNCF 大使

范围

本文描述三个失败场景,它们揭示了”拥有备份”与”能够恢复”之间的差距,以及每个场景所对应的指导建议。每一个场景都可以在上面提到的实验室代码仓库中、在一台笔记本电脑上复现,文中展示的每一段终端输出都是该实验室中的真实抓取记录。

本文涵盖运行在 Kubernetes 上的有状态应用(stateful application)的恢复:验证备份中确实包含数据、声明状态(declared state)与实际存储状态(stored state)之间的分离,以及多卷(multi-volume)应用的一致性。本文不涉及合规框架、产品对比,也不涉及底层云或数据中心基础设施的恢复,但会指出这些责任的起点在哪里。

特定工具只在某个场景需要时出现(Velero、CSI 快照 API)。它们是参考实现(reference implementation),用于让场景具体化。这些故障模式(failure mode)和相关指导适用于任何承担相同角色的工具。

定义

RPO(恢复点目标)和 RTO(恢复时间目标)在同一时间线上

四个恢复层

要使一次恢复真正算数,四个层都必须回归:

四个恢复层

每一层都有成熟的工具支持,而且每一层在单独运行时通常都能正常恢复。恢复失败往往发生在层与层之间的衔接处:集群恢复了但数据为空、数据恢复了但没有流量路径、应用定义配置出来的是一个空卷。下面三个场景各自破坏了其中一个衔接点。

实验环境

一图看懂整个实验环境

两个集群和两项服务,全部位于本地:

  • 生产环境:一个 Kubernetes(容器编排平台)集群,其中每个节点都是一个拥有独立 kernel(内核)的轻量级 VM(虚拟机),因此丢失生产环境只是关闭一台机器而已。
  • 恢复环境:在任何故障发生之前就已存在的第二个集群。
  • 备份存储:位于两个集群之外的 S3 兼容 object store(对象存储),因此丢失任一集群都不会连带丢失 recovery points(恢复点)。两个集群都将各自的备份工具指向同一个 bucket(存储桶)。
  • Git:一个本地 Git 服务,保存 application manifests(应用清单),由恢复集群中的 GitOps controller(GitOps 控制器)监视。

工作负载是一个内容已知(四行)的 PostgreSQL(关系型数据库)应用,因此每次恢复都可以对照预期结果进行验证,而不是仅凭一个 green dashboard(绿色仪表盘)。

场景 1:验证备份包含数据

备份工具将对象和卷字节移动到 S3(对象存储)存储

Kubernetes(容器编排平台)备份包含两个截然不同的部分:资源定义(YAML,一种数据序列化格式)和持久卷数据。备份工具通过提供商快照或 CSI(容器存储接口)快照、文件系统备份,或将快照数据移动到外部存储来保护卷数据。本实验采用最后一种方式,即使用 Velero(Kubernetes 备份工具)及其 data mover(数据移动器)。

大多数验证止步于备份的 Completed 状态。再进一步,确认卷字节确实发生了移动:

`
$ kubectl -n velero get datauploads -l velero.io/backup-name=$BACKUP \
    -o custom-columns=’NAME:.metadata.name,PHASE:.status.phase,BYTES:.status.progress.bytesDone’
NAME                                       PHASE       BYTES
guestbook-rehearsal-20260727001126-q2j9m   Completed   47989888
`

这是数据搬运器(data mover)确认 47,989,888 字节的卷数据离开集群并落入外部存储。如果一款备份工具无法针对某次备份报告出这个数字,那它就值得被审视。

删除命名空间(namespace,包含 PVC),再从此备份恢复,大约两分钟内就返回了同样的四行数据。这是理想路径(happy path),而它掩盖了三件没有任何备份工具会自动完成的事:

  1. 保护卷数据并不能让数据库备份达到应用一致性(application consistent)。当应用需要时,必须配置 flush 或 quiesce 钩子(hook)。
  2. 恢复到不同的基础设施上可能需要存储类(storage class)映射和其他转换。工具提供的是机制;每个团队必须自行设计和测试这些机制。
  3. 备份阶段(phase)为 Completed 意味着备份操作已完成。它并不证明应用能够启动、包含预期的数据,或正常提供流量服务。只有端到端(end to end)恢复测试才能提供这一证据。

边界。 备份工具只是把资源恢复到已经存在的集群中。它们不会创建集群、节点、网络、负载均衡器(load balancer)或 DNS。必须有其他东西来恢复 Kubernetes 本身,而这个东西就是以代码定义的基础设施(infrastructure as code)或 Cluster API。一份以“恢复备份”开头的灾难恢复(DR)计划,必须说明备份会被恢复到什么之上。

场景 2:声明的状态并非存储的状态

GitOps 陷阱:控制器重建的是声明,而存储承载的是数据

生产集群已经断电。灾前就存在的恢复集群上,一个 GitOps 控制器指向 Git,一个备份工具指向共享存储。它从未运行过这个应用。

从 Git 同步应用成功了:同步状态显示为 Synced,StatefulSet 完成滚动部署,数据库 Pod 处于 Running 和 Ready,所有仪表盘都是绿色的。然后查询数据库却返回:

ERROR: relation "attendees" does not exist

数据库在运行,但它是空的。没有任何东西发生故障。Git 里始终只有声明,所以 Kubernetes 完全按照 YAML 所说的执行:创建一个 StatefulSet,创建一个 Service,并为 PVC 配置一个全新的空卷。GitOps 完美地重建了声明的状态,却没有恢复任何已存储的状态。

两个工具都是必需的,因为需要找回的是两样不同的东西,而每个工具恰好只承载其中一样:Git 存储的是意图,备份存储的是状态。在实验环境中,能够产出经过验证的数据的恢复流程是:

  1. 移除同步创建出来的空应用。
  2. 从备份存储恢复应用,包括卷。
  3. 对照预期内容验证数据。

这次恢复还跨越了基础设施:备份是在一个节点运行时上取得的,却恢复到了另一个节点运行时上。灾难可能迫使恢复工作落到不同的基础设施上,因此恢复的可移植性是需要测试的,而不是想当然的。

度量。 在实验环境中,从关闭生产到在恢复集群中获得经过验证的数据,实况操作用了四分钟,彩排重跑时略少于两分钟。这两个数字都只度量了脚本化的那一小段;生产环境的 RTO 还要把检测、决策、流量切换和回切都包进来。普遍的教训是:仪表盘变绿的那一刻并不是恢复完成。数据回来并被检查过的那一刻才是。

场景 3:多卷一致性

来自不同时刻的两个快照撕裂了数据

真实的有状态应用会横跨多个卷:数据库数据加上 WAL、消息代理分区、副本集。实验室里的替代方案会写入配对数据,每秒五次,将订单 n 写入一个 PVC,将支付 n 写入另一个 PVC,并遵循一条不变量:每一笔支付都必须有其对应的订单。

对这两个卷分别进行快照,相隔五秒,生成了两个各自都处于 ReadyToUse 状态、单独看来都完美无缺的快照。将两者都恢复后,比较最后提交的序列号:

last order committed : 108352 last payment committed : 108377
[FAIL] 25 payments have NO matching order. [FAIL] Each snapshot succeeded. The restore is still wrong.

有二十五笔支付引用了并不存在的订单。没有任何组件发生故障,每个操作都报告成功,而合并后的恢复点所描述的,是一个从未存在过的时刻。在生产环境中,这五秒的间隔意味着备份工具正在逐个遍历一百个 PVC 的列表。

API 给出的答案。 VolumeGroupSnapshot 在 Kubernetes 1.36 中达到了 GA。一个对象通过标签选择 PVC,CSI 驱动会收到一个请求,为所有这些 PVC 创建一个协调一致、崩溃一致(crash consistent)的恢复点:

`
apiVersion: groupsnapshot.storage.k8s.io/v1
kind: VolumeGroupSnapshot
metadata:
  name: ledger-group-snap
spec:
  volumeGroupSnapshotClassName: csi-hostpath-groupsnapclass
  source:
    selector:
      matchLabels:
        group: ledger
`

一组快照在同一时刻截取两个卷

恢复该组的成员快照并运行相同的验证器:

last order committed : 109169 last payment committed : 109169
[OK] Every payment has a matching order. Restore is consistent.

这些注意事项不仅适用于实验环境:

  • 支持情况因驱动而异。一个支持普通 VolumeSnapshots 的驱动并不能证明它支持组快照;CSI 组 RPC 是独立的实现。截至 2026 年中,为实验环境检查过的大多数主流云驱动都未实现这些功能。
  • 配置是显式的:快照控制器和 CSI sidecar 上的 CRD 与特性门控必须由运维人员启用。
  • 崩溃一致性不等于应用一致性。该 API 消除了跨卷的时间偏差;它不会刷新或静默数据库。
  • 实验环境使用 CSI hostpath 测试驱动,该驱动实现了组 RPC,但会按顺序归档成员卷,因此在组快照期间会暂停写入端,以保证演示的确定性。时间点保证本身属于生产驱动的存储后端。

恢复测试指南

恢复测试不是删除一个 Pod 然后看它是否恢复;那测试的是工作负载协调。恢复测试应当:

  1. 将一个完整的有状态应用恢复到一个从未运行过它的干净目标环境中。
  2. 根据预期内容而非资源状态来验证数据和用户路径。
  3. 用计时器衡量整个过程。

以下原则可以直接从实验环境迁移到生产环境:

两个独立的故障域

生态系统中尚未填补的空白

这些场景暴露出当今没有任何单一工具能够填补的空白:

  1. 没有通用的跨集群故障转移契约。 数据、工作负载、集群、流量和身份各自都有工具,而每一行都缺失同样的东西:与下一行之间的共享契约。各家产品在自己的 API 内部回答了这个问题;核心 Kubernetes 并未定义这一顺序。
  2. 没有针对应用程序的标准恢复单元。 核心 Kubernetes 没有一个持续维护的 Application 资源,用以声明哪些对象、Operator、数据服务和外部依赖必须一起恢复。备份工具使用命名空间和标签,GitOps 控制器有自己的应用对象,包管理器有 release,而每一种方式划定的边界都不一样。
  3. 备份成功被当作恢复的证明。 备份完成的指标被广泛监控;而恢复演练的结果却很少被监控。

如何参与贡献

CNCF TAG Operational Resilience 下的云原生业务连续性(Cloud Native Business Continuity)计划是一项开放提案,正在寻求贡献者,目标是进行生态全景差距分析、更新备份与灾难恢复指南,以及构建参考架构:https://github.com/cncf/toc/issues/1779

参考资料

  • 可复现的实验环境:https://github.com/saiyam1814/kubecon-japan-dr-demo
  • Velero,CNCF Sandbox:https://www.cncf.io/projects/velero/
  • Velero 备份钩子:https://velero.io/docs/v1.18/backup-hooks/
  • VolumeGroupSnapshot 正式发布公告:https://kubernetes.io/blog/2026/05/08/kubernetes-v1-36-volume-group-snapshot-ga/
  • 本文档的博客版本:https://blog.kubesimplify.com/a-backup-is-not-disaster-recovery