Kubernetes 跨集群与跨区域灾难恢复
凌晨两点,主区域的 Kubernetes API、节点池和块存储控制面同时不可达。值班人员在备用区域创建了新集群,Velero 也能看到异地对象仓库里的 Backup;恢复启动后,CRD 因版本不兼容被拒绝,PVC 仍引用源区域的快照句柄,Service 恢复出旧的 LoadBalancer 注解,ExternalSecret 又因目标集群没有云身份而无法解密。更危险的是,旧区域短暂恢复网络后重新开始消费消息,两边同时写数据库,原本的基础设施故障升级成数据分叉。
跨集群灾备不是把 YAML 和 PVC 搬到另一个 API Server。它要同时重建运行控制面、数据副本、身份信任、外部依赖和流量入口,并保证任意时刻只有一个写入主站。恢复工具负责搬运其中一部分资产,架构负责让这些资产在新的故障域重新组成可接管的系统。
先把“另一个集群”拆成故障域
两个 kubeconfig context 不等于两个灾备站点。源、目标若共享云账号管理员、对象仓库、KMS key、DNS 控制面、镜像仓库、身份提供方或同一存储后端,一次账号失陷、区域中断或误删除仍可能同时摧毁两边。
设计时把依赖画成故障域矩阵:
| 能力 | 源站 | 目标站 | 需要隔离或预置的证据 |
|---|---|---|---|
| 集群与节点 | 主区域、主账号 | 备用区域或独立账号 | IaC revision、配额、节点镜像与创建耗时 |
| Kubernetes 对象 | 生产 API | 新 API | Git/IaC 与对象备份的所有权边界 |
| 持久数据 | 源卷、快照 | 目标卷 | 跨区副本、data mover 仓库、恢复吞吐 |
| 身份与密钥 | 源 workload identity | 目标 issuer/role/KMS grant | 短期恢复身份、信任策略和解密演练 |
| 制品与依赖 | Registry、DNS、数据库、MQ | 异地副本或降级入口 | digest、endpoint、容量和接管动作 |
| 仲裁与流量 | 主站写入 | 默认隔离 | fencing token、DNS/全局流量权和回切门禁 |
冷备只保留恢复点和 IaC,成本低,但 RTO 包含账号、网络、集群和节点池创建。温备预建控制面、基础插件和最小节点,缩短启动时间,却要持续处理版本漂移。热备让应用副本和数据复制长期运行,RTO 最短,但单写仲裁、复制延迟、双倍容量和误切流成本最高。模式不是产品开关,而是业务影响分析后的容量承诺。
目标站先通过能力基线
不要在恢复过程中才发现目标站缺少 API、CSI 或配额。先用只读命令采集两个 context 的差异,真实名称由受控配置注入:
export SRC_CTX='<source-context>'
export DR_CTX='<recovery-context>'
export APP_NS='orders'
for ctx in "$SRC_CTX" "$DR_CTX"; do
echo "=== $ctx ==="
kubectl --context "$ctx" version
kubectl --context "$ctx" get nodes -o wide
kubectl --context "$ctx" api-versions | sort
kubectl --context "$ctx" get crd -o name | sort
kubectl --context "$ctx" get storageclass,csidriver
kubectl --context "$ctx" get volumesnapshotclass 2>/dev/null || true
done目标站至少预建 CNI、CoreDNS、CSI、snapshot-controller、Ingress/Gateway、证书签发、镜像访问和备份控制器。CRD 只恢复 schema,不会自动恢复 controller;StorageClass 名称相同也不能证明 provisioner、拓扑、加密、扩容和 reclaim policy 相同。
再检查恢复身份。调度恢复的人不应永久持有 cluster-admin,仓库读取身份也不应拥有删除恢复点的权限:
kubectl --context "$DR_CTX" auth can-i create restores.velero.io -n velero
kubectl --context "$DR_CTX" auth can-i create persistentvolumeclaims -n "$APP_NS"
kubectl --context "$DR_CTX" auth can-i patch customresourcedefinitions.apiextensions.k8s.io
kubectl --context "$DR_CTX" auth can-i delete backups.velero.io -n velero前三项按职责分配给不同阶段的短期身份;最后一项对只读演练身份应为 no。目标云角色还要单独验证对象仓库读取、KMS 解密、快照复制或共享、镜像拉取和 DNS 变更权限。把 access key 写进 kubeconfig、Helm values 或恢复日志会让演练本身成为泄露路径,优先使用 workload identity、短期会话和外部 Secret 系统。
用映射合同消除环境偶然性
跨集群恢复前建立机器可读的映射表。它不是备份工具配置的副本,而是源语义到目标语义的转换合同:
recoveryId: dr-cross-region-01
source:
cluster: source-cluster
region: source-region
kubernetesMinor: "<observed-minor>"
target:
cluster: recovery-cluster
region: recovery-region
kubernetesMinor: "<observed-minor>"
mappings:
namespaces:
orders: recovery-orders
storageClasses:
source-block-retain: recovery-block-retain
ingressClasses:
source-ingress: recovery-ingress
serviceAccounts:
orders-api: orders-api-recovery
externalEndpoints:
payment: payment-sandbox
messageBus: recovery-message-bus
gates:
sourceWriteFenced: false
repositoryReadOnly: true
businessValidationPassed: false
trafficApproved: falsekubernetesMinor 决定 API 转换风险;storageClasses 不能只改名字,还要核对 driver、volumeBindingMode、allowedTopologies、访问模式、文件系统和 KMS;externalEndpoints 必须默认指向沙箱或只读副本。四个 gate 应由不同证据解锁,不能因为 Restore 结束自动把流量打开。
Velero 可以通过带特定 label 的 ConfigMap 修改恢复时的 StorageClass,并通过 namespace mapping 把对象放进隔离命名空间。下面只演示转换入口,实际 key 使用采集到的类名:
apiVersion: v1
kind: ConfigMap
metadata:
name: change-storage-class-config
namespace: velero
labels:
velero.io/plugin-config: ""
velero.io/change-storage-class: RestoreItemAction
data:
source-block-retain: recovery-block-retainVelero Restore Reference说明了 namespace mapping、已有资源策略以及恢复状态字段的行为。默认策略遇到目标同名对象时会跳过而不是覆盖;--existing-resource-policy=update 也只是尽力更新 Kubernetes spec,不会覆盖 PVC 中的数据。生产恢复不能把 update 当成通用冲突解决器。
数据路径决定能否真正跨区
原生 CSI 快照通常只在存储后端定义的账号、区域和驱动边界内有效。把 VolumeSnapshotContent 的 handle 写进对象仓库,不代表目标区域可以读取它。跨域数据保护常见三条路径:
云或存储后端把快照异步复制到目标故障域,目标站再用兼容 CSI/provider plugin 创建卷。Velero CSI snapshot data movement、FSB、K8up、VolSync 等把数据移动到独立对象仓库或目标卷,再由目标 StorageClass 动态供应。数据库原生全量、增量与日志归档进入异地仓库,目标站按事务水位恢复;Kubernetes 工具负责对象和卷外壳。
第一条恢复快但锁定存储能力;第二条可移植性更好,却受小文件扫描、缓存、网络和目标写入吞吐影响;第三条能提供事务语义,但需要应用团队维护恢复命令、版本兼容和日志连续性。关键数据库通常组合第二、第三条,而不是押注单个 PVC 快照。
目标站恢复前记录 VolumeSnapshotClass.driver、快照 handle、复制完成时间、KMS key、目标 StorageClass 和预期容量。若采用文件级 data mover,还要记录仓库 prefix、repository password/KMS 依赖、并发、缓存和恢复节点调度条件。
正向实验:在隔离 namespace 完成一次跨集群恢复
实验使用两个非生产 context 和专用 namespace。先在源站创建不含真实客户数据的 fixture,并写入可验证水位:
export LAB_SRC_NS='dr-source-lab'
export LAB_DST_NS='dr-recovery-lab'
export BACKUP_NAME="dr-lab-$(date +%Y%m%d%H%M%S)"
kubectl --context "$SRC_CTX" create namespace "$LAB_SRC_NS"
kubectl --context "$SRC_CTX" -n "$LAB_SRC_NS" create configmap recovery-marker \
--from-literal=sequence=1042 \
--from-literal=committedAt='<fixture-utc-timestamp>'
kubectl --context "$SRC_CTX" -n "$LAB_SRC_NS" label configmap recovery-marker app=dr-lab
velero --kubecontext "$SRC_CTX" backup create "$BACKUP_NAME" \
--include-namespaces "$LAB_SRC_NS" --wait
velero --kubecontext "$SRC_CTX" backup describe "$BACKUP_NAME" --details预期 Backup 没有 error,仓库中存在对应对象清单;若 fixture 还包含 PVC,必须同时查看 PodVolumeBackup、DataUpload 或快照对象,而不是只看顶层 Phase。接着在目标站确认 BackupStorageLocation 可读,再恢复到新 namespace:
kubectl --context "$DR_CTX" -n velero get backupstoragelocations.velero.io
velero --kubecontext "$DR_CTX" backup get
export RESTORE_NAME="restore-${BACKUP_NAME}"
velero --kubecontext "$DR_CTX" restore create "$RESTORE_NAME" \
--from-backup "$BACKUP_NAME" \
--namespace-mappings "$LAB_SRC_NS:$LAB_DST_NS" \
--existing-resource-policy=none --wait
velero --kubecontext "$DR_CTX" restore describe "$RESTORE_NAME" --details
kubectl --context "$DR_CTX" -n "$LAB_DST_NS" get configmap recovery-marker \
-o jsonpath='{.data.sequence}{" "}{.data.committedAt}{"\n"}'预期输出包含 1042 和 fixture 时间,目标对象带有恢复追踪 label,源 namespace 不会在目标站被意外创建。对真实应用继续执行 schema 查询、checksum、只读 API、隔离写入和错误身份拒绝;所有 consumer、CronJob、mailer、支付回调与外部 Ingress 保持关闭。
对于 PVC,从恢复卷挂载只读校验 Pod,核对容量、文件属主、checksum 和数据库水位。PVC Bound 只证明供应成功,不证明使用了正确恢复点。将对象清单、卷 handle、应用断言和每阶段时间写入同一演练 ID 后,才允许 businessValidationPassed 变为 true。
反向实验:故意让目标站缺少关键能力
在另一个实验 namespace 先不安装某个测试 CRD,或临时移除 StorageClass 映射,再发起恢复:
kubectl --context "$DR_CTX" delete configmap -n velero change-storage-class-config --ignore-not-found
velero --kubecontext "$DR_CTX" restore create "negative-${BACKUP_NAME}" \
--from-backup "$BACKUP_NAME" \
--namespace-mappings "$LAB_SRC_NS:dr-negative-lab" --wait || true
velero --kubecontext "$DR_CTX" restore describe "negative-${BACKUP_NAME}" --details
kubectl --context "$DR_CTX" -n dr-negative-lab get events --sort-by=.lastTimestamp
kubectl --context "$DR_CTX" -n velero logs deploy/velero --since=15m包含 PVC 时,预期可观察到 StorageClass 不存在、PVC Pending、拓扑不匹配或 KMS/CSI 授权失败;包含缺失 CRD 的对象时,Restore detail 应出现无法识别资源或 admission 错误。失败实验的通过条件不是“最终想办法让命令绿了”,而是系统阻止入口开放,并能从 Restore item、Event、controller/plugin 日志定位到映射合同中的缺项。
补齐 CRD/controller 或恢复映射后应创建新的 Restore,不要直接修改已经结束的恢复历史。保留失败记录,比较修复前后的对象计数和业务证据,证明门禁真的捕获了问题。
接管必须先建立单写者
跨区域复制通常是异步的。故障时无法同时证明源站已停止和目标站已取得最新水位,就必须在一致性与可用性之间做明确决策。常见 fencing 手段包括:数据库租约或 epoch、消息队列 consumer group 隔离、云存储写租约、外部仲裁服务、撤销源站 IAM,以及只允许目标站获得的短期写令牌。
切流顺序建议固定为:
冻结 GitOps、HPA、CronJob 和自动故障转移动作,记录当前 revision。通过独立控制面封禁源站写身份,确认数据库、队列和外部 API 拒绝旧 token。在目标站完成数据水位、业务不变量、容量和安全校验。
先开放内部测试入口,再按权重调整全局流量或 DNS。观察错误率、延迟、写入水位、队列重复消费和身份拒绝,再扩大流量。
DNS TTL 只影响缓存收敛,不提供写入仲裁。旧站恢复后不能因为 Pod Ready 就自动抢回流量;它的数据可能落后,也可能包含故障窗口内的孤立写入。回切前先把旧站降为只读,确定数据回并策略,再重新演练单写切换。
回退不是删除 Restore 对象
velero restore delete 只删除 Restore CR 及其日志/结果文件,不删除已经创建的业务对象;直接 kubectl delete restore 连对象存储中的日志和结果也不会清理。回退要按演练清单反向处理:入口、外部回调、应用、PVC、快照、临时身份、基础设施。
kubectl --context "$DR_CTX" -n "$LAB_DST_NS" scale deployment,statefulset --all --replicas=0
kubectl --context "$DR_CTX" -n "$LAB_DST_NS" get all,configmap,secret,pvc -o yaml \
> "${RESTORE_NAME}-final-inventory.yaml"
kubectl --context "$DR_CTX" delete namespace "$LAB_DST_NS" --wait=true
velero --kubecontext "$DR_CTX" restore delete "$RESTORE_NAME" --confirm
kubectl --context "$DR_CTX" get pv,volumesnapshotcontents清理前对导出文件做脱敏,不保存 Secret data、token、完整 kubeconfig 或云账号标识。Namespace 消失后继续核对 PV reclaim policy、snapshot deletionPolicy、对象仓库副本和云负载均衡器;控制面对象删除不等于后端资产已回收。不可变恢复点按保留策略保留,不能为降低演练成本绕过 WORM。
若目标站已经接收真实写入,回退流量前必须处理新写数据。最简单的安全策略是保持目标站为唯一主站,修复其故障;需要回切时使用数据库复制、事件回放或业务补偿把水位合并,绝不能用旧快照覆盖新写入。
容量与成本决定灾备模式能否兑现
跨区域恢复总时间至少包含:目标基础设施创建、镜像拉取、对象恢复、数据读取与写回、数据库回放、controller 收敛、缓存预热、业务校验和流量收敛。估算时分别测量,不用备份任务耗时替代:
data_restore_time = bytes_to_read / sustained_repository_read
+ bytes_to_write / sustained_target_write
+ replay_and_validation
total_rto = infrastructure + platform + objects + data_restore_time
+ application_reconcile + business_validation + traffic_shift成本包括备用集群、节点与配额预留、跨区副本、对象请求、数据传输、KMS、镜像复制、日志保留、演练资源和人工值班。云服务价格与区域能力会变化,应在目标账号的当前官方价格页和配额页计算,不把一次报价固化为架构事实。
吞吐也不是无限提高并发。对象仓库请求限额、KMS QPS、API Server、CSI provisioner、data mover 缓存、节点网络和目标卷 IOPS 任何一个都可能先饱和。演练要记录并发增加后吞吐、错误率和长尾是否改善;若只增加限流和重试,就应降低并发或分波次恢复。
把恢复能力当成持续维护的产品
平台团队维护 IaC、基础插件、恢复控制器和映射合同;存储团队维护快照复制、仓库、KMS 与容量;应用团队维护一致性 Hook、schema 和业务断言;安全团队审批隔离身份与仓库删除;业务负责人决定 RPO/RTO、降级和切流。任何一方都不能用“备份成功”替代其他环节的证据。
每次 Kubernetes、CSI、Operator、备份工具或应用 schema 升级,都要用旧恢复点在目标站重跑兼容测试。持续观察最近可恢复水位、跨区复制延迟、仓库读取失败、孤儿快照、目标配额、证书/KMS 到期、镜像缺失和完整演练耗时。恢复脚本、映射合同与业务断言进入版本库;凭证和真实环境标识留在受控系统。
退出某个工具或区域时,先让新旧链并行生成恢复点,用相同 fixture 分别恢复并比较数据水位、对象差异、RTO、删除语义和费用。确认历史恢复点仍有受支持的读取路径后,才撤销旧插件、仓库身份、KMS grant 与目标基础设施。
跨集群灾备真正完成的标志,不是目标站出现一批 Pod,而是源站已被可靠隔离,目标站从独立恢复点重建了正确数据和身份,业务断言与容量门禁通过,流量切换可观察,回切不会产生双写,并且所有临时权限和费用资产都有明确归宿。
实施时使用的官方入口
Velero Restore Reference。Velero Backup Storage Location 与 Volume Snapshot Location。Velero File System Backup
Kubernetes VolumeSnapshot。Kubernetes RBAC。Kubernetes Secrets 安全实践
