课程目录(第 24 章 / 共 33 章)
课程/进阶:生产实践

备份与恢复:把「删错了」变成小事

24 章 / 共 33·18 分钟·进阶备份Velero快照灾难恢复

分清集群状态、资源清单、卷数据与镜像四层备份,用 etcd 快照、Velero 和 VolumeSnapshot 做一次真的能恢复的备份。

学完这一章,你将能够

  • 说得出备份要分哪四层、各层用什么手段
  • 会用 etcdctl 做快照并知道恢复为什么要先停 apiserver
  • 用 Velero 完成一次「备份 → 删除 → 恢复」的完整演练
  • 用 RPO 与 RTO 和业务方对齐备份策略

备份是分层的,不是「备份一下」

「我们有备份」这句话在生产环境里经常不成立。数据库每天 dump 到本机另一块盘、YAML 存在 Git 里、PVC 从来没动过——看起来像有备份,实际上机房的存储一坏就全没了。要回答「备份够不够」,先把备份对象拆成四层:每一层能恢复的东西、用的手段、恢复粒度都不一样。

text
① etcd 快照 ──── 所有 Kubernetes 对象(Namespace / PVC / CRD / Secret)
② 资源清单 ───── Git 里的 YAML(但 Secret 与部分 CRD 未必在里面)
③ 卷数据 ─────── PVC 里的真实数据(快照或逻辑导出)
④ 镜像仓库 ───── 应用能不能重新跑起来的前提
备份什么常用手段恢复粒度
etcd整个集群的对象状态etcdctl snapshot save、托管集群备份整个集群
清单Deployment、Service、CRD、配置Git 仓库、Velero命名空间或单个资源
卷数据PVC 里的文件与数据库文件VolumeSnapshot、mysqldump、RDB单个卷或单张表
镜像应用镜像与 chart仓库间复制、多机房同步单个镜像

一个务实的判断标准是:任何一层单独存在都不算备份。 只有 etcd 快照,恢复回来的是「指向已消失 PVC 的对象」;只有卷快照,恢复回来的是「没人挂载的数据」;只有 Git 里的 YAML,恢复回来的是「空数据库的新集群」。

etcd 快照:集群状态的地基

etcd 存着集群里所有对象的最终状态,它挂了,Deployment、Service、PVC、Secret 就都没了。etcd 快照是唯一能整体恢复集群对象的手段,其它工具(包括 Velero)备份的都是「对象的内容」,恢复时仍要往一个活着的集群里写。

在控制面节点上执行,证书路径是 kubeadm 集群的默认位置:

bash
ETCDCTL="etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key"

$ETCDCTL endpoint status --write-out=table
$ETCDCTL snapshot save /var/backups/etcd-$(date +%F-%H%M).db
$ETCDCTL snapshot status /var/backups/etcd-2024-05-21-0200.db --write-out=table

endpoint status 打出每个成员的角色、版本和数据库大小;snapshot status 打出 totalKeytotalSizerevisionhash,用来确认快照文件完整可读。快照做完后必须复制到集群之外的介质,留在 /var/backups 只是「同一块盘上的第二个副本」。

恢复是「用旧状态覆盖现有集群」,比备份危险得多。核心约束是:恢复必须在 etcd 与 apiserver 都停止之后做,否则客户端会继续写入,恢复完立刻被覆盖。

text
停 apiserver → restore 到新 data-dir → 切 data-dir 启动 etcd → 恢复 apiserver 并验证
bash
ETCDCTL_API=3 etcdctl snapshot restore /var/backups/etcd-2024-05-21-0200.db \
  --name=<etcd-节点名> --initial-cluster=<etcd-节点名>=https://<节点IP>:2380 \
  --initial-advertise-peer-urls=https://<节点IP>:2380 \
  --initial-cluster-token=etcd-cluster-restore --data-dir=/var/lib/etcd-restore

然后编辑 /etc/kubernetes/manifests/etcd.yaml,把 hostPathpath/var/lib/etcd 改成 /var/lib/etcd-restore,etcd 会自动重启。多控制面集群不能只恢复一个成员:要用同一个 --initial-cluster-token、按各自的名字与地址逐节点恢复,步骤以 etcd 官方灾难恢复文档为准。单节点实验集群最适合练这一步,生产上的恢复演练要有变更窗口和回滚预案。

Velero:把资源与卷一起备份

etcd 快照适合「整个集群回滚」,但日常更常见的是「某个命名空间被删了」「某张 PVC 要回到昨天」。这时候用 Velero:它把 Kubernetes 对象导出成备份文件存到对象存储,配合 CSI 快照把卷一起带上。

用 CLI 安装(<插件版本> 按 velero-plugin-for-aws 的最新 release 替换):

bash
velero install --provider aws --plugins velero/velero-plugin-for-aws:<插件版本> \
  --bucket kube101-backup --backup-location-config region=cn-north-1,s3Url=https://s3.example.com \
  --snapshot-location-config region=cn-north-1 --secret-file ./credentials-velero
kubectl -n velero get pods && velero backup-location get

如果 CLI 提示 --provider 已废弃,直接去掉即可,新版靠插件自动识别对象存储类型。credentials-velero 是 AWS 凭证文件,内容是 [default] 段加上 aws_access_key_idaws_secret_access_key 两行。

生产上更推荐把备份策略写成清单交给 Git 管(snapshotVolumes 默认为 true,会连卷一起备份):

yaml
apiVersion: velero.io/v1
kind: Backup
metadata:
  name: demo-backup
  namespace: velero
spec:
  includedNamespaces:
    - demo
  ttl: 720h
yaml
apiVersion: velero.io/v1
kind: Restore
metadata:
  name: demo-restore
  namespace: velero
spec:
  backupName: demo-backup
  namespaceMapping:
    demo: demo-restored
yaml
apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: demo-daily
  namespace: velero
spec:
  schedule: "0 2 * * *"
  template:
    includedNamespaces:
      - demo

Scheduletemplate 就是 Backupspec,所以「每天凌晨 2 点备份 demo」就是这几行;ttl 控制保留时长,上面 Backup 里的 720h 约等于 30 天。对应的 CLI 命令是:

bash
velero backup create demo-backup --include-namespaces demo --wait
velero backup describe demo-backup --details
velero restore create --from-backup demo-backup --namespace-mappings demo:demo-restored
velero backup logs demo-backup

velero backup describe --details 会列出备份了哪些资源、卷快照的状态;velero backup logs 能看出哪些资源因为权限或 CRD 缺失没被备份到——这一步是「备份到底成不成」的关键证据

PVC 快照:VolumeSnapshot 与从快照恢复

Velero 的卷备份底层依赖 CSI 快照,所以集群里得先有快照能力:快照 CRD、snapshot-controller,以及 CSI 驱动自带的 csi-snapshotter sidecar。多数托管集群已经内置,自建集群需要自己装;如果下面的路径在你的版本里 404,就改用同一 release 下的 deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yamlsetup-snapshot-controller.yaml 直接 kubectl apply -f

bash
SNAP_VER=v8.2.0   # 按 external-snapshotter 的最新 release 替换
kubectl apply -k "https://github.com/kubernetes-csi/external-snapshotter//client/config/crd?ref=${SNAP_VER}"
kubectl apply -k "https://github.com/kubernetes-csi/external-snapshotter//deploy/kubernetes/snapshot-controller?ref=${SNAP_VER}"
kubectl -n kube-system rollout status deploy/snapshot-controller

装好后先定义 VolumeSnapshotClassdriver 必须填集群里实际使用的 CSI 驱动名(用 kubectl get csidrivers 查看):

yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: csi-snapclass
  annotations:
    snapshot.storage.k8s.io/is-default-class: "true"
driver: <你的 CSI 驱动名>
deletionPolicy: Retain
yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: data-snap-20240521
  namespace: demo
spec:
  volumeSnapshotClassName: csi-snapclass
  source: {persistentVolumeClaimName: data}

从快照恢复,是新建一个 PVC 并把 dataSource 指向快照:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-restored
  namespace: demo
spec:
  storageClassName: <原 PVC 的 StorageClass>
  dataSource:
    name: data-snap-20240521
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes: ["ReadWriteOnce"]
  resources: {requests: {storage: 10Gi}}
bash
kubectl -n demo get pvc data-restored

VolumeSnapshotREADYTOUSE 变成 true、新 PVC 变成 Bound,就说明恢复完成。注意恢复出的 PVC 不能小于快照的容量,而且必须用兼容的 StorageClass,快照的前提条件在 生产存储 里讲过。

有状态应用:逻辑备份还是卷快照

卷快照快、对应用透明,恢复的是「某个时刻的整个磁盘」;逻辑备份慢,却能跨版本、跨云、按表恢复。两者不是二选一,而是分工。卷快照适合整库回滚到某个时间点,但依赖 CSI 支持、只能整体恢复、跨存储迁移难;逻辑备份适合定期 dump、迁移与精确恢复,但慢、占 CPU/IO,需要应用侧工具。

具体到应用:Redis 用 redis-cli BGSAVE 生成 RDB 后复制到对象存储,开了 AOF 就一起备份;MySQL 用 mysqldump --single-transaction --databases demo > demo.sql 做逻辑备份,再靠 binlog 做时间点恢复。卷快照要配合 FLUSH TABLES WITH READ LOCK 或数据库自带的 backup 模式才是一致的。

bash
# Redis:先落盘再取文件,顺手记下 key 数量作为校验基线(Pod 名以你的集群为准)
kubectl -n demo exec deploy/redis -- redis-cli -a "$REDIS_PASSWORD" BGSAVE
kubectl -n demo exec deploy/redis -- redis-cli -a "$REDIS_PASSWORD" DBSIZE
kubectl -n demo cp redis-0:/data/dump.rdb ./dump-$(date +%F).rdb
# MySQL / PostgreSQL:导出成可读文件,能跨版本、跨云、按表恢复
kubectl -n demo exec deploy/mysql -- sh -c 'mysqldump --single-transaction --databases demo > /tmp/demo.sql'
kubectl -n demo exec deploy/postgres -- sh -c 'pg_dump -Fc -f /tmp/demo.dump demo'

--single-transaction 让 InnoDB 在同一个事务快照里读,不锁表;pg_dump -Fc 是自定义格式,能配合 pg_restore 并行恢复、按表恢复。逻辑备份的代价是慢和占 CPU/IO,频率要按 RPO 定,别无脑天天全量。

怎么选,先看你要恢复什么:

要恢复什么优先手段原因
整库回到 1 小时前卷快照快,且能保住整库一致性
只捞回一张误删的表逻辑备份快照无法单表恢复
跨云 / 跨存储迁移逻辑备份快照格式依赖 CSI 驱动
升级到新版本引擎逻辑备份快照只有原始字节,版本不兼容
机房级灾难恢复两者都要快照省时间,逻辑备份保底

卷快照的一致性还要靠引擎配合:MySQL 用 FLUSH TABLES WITH READ LOCK(或 XtraBackup 的备份锁)、PostgreSQL 用 pg_backup_start/pg_backup_stop,否则快照可能抓到「写了一半」的页,恢复后起不来。

参考:reference/k8s-in-action/db/redis/README.mdreference/k8s-in-action/db/tikv/backup-restore.md

快照不是备份

卷快照通常和源卷在同一个存储集群里,存储集群整体故障时两者一起消失。快照要定期转存到另一套介质,或者用 Velero 同步到对象存储——「不同介质」是备份的定义之一

动手练习:备份、删掉、再恢复

动手练习:完整走一遍「备份 → 删除 → 恢复」

准备一个有状态的应用,demo 命名空间里放一个 PVC 加一个会写文件的 Pod:

bash
kubectl create namespace demo
kubectl -n demo apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes: ["ReadWriteOnce"]
  resources: {requests: {storage: 1Gi}}
---
apiVersion: v1
kind: Pod
metadata:
  name: writer
spec:
  containers:
    - name: writer
      image: busybox:1.36
      command: ["sh", "-c", "echo hello-from-before-backup > /data/proof.txt && sleep 3600"]
      volumeMounts: [{name: data, mountPath: /data}]
  volumes:
    - name: data
      persistentVolumeClaim: {claimName: data}
EOF

kubectl -n demo exec writer -- cat /data/proof.txt

确认文件内容之后,创建一次备份:

bash
velero backup create demo-backup --include-namespaces demo --wait
velero backup describe demo-backup --details

现在模拟事故,把整个命名空间删掉:

bash
kubectl delete namespace demo

恢复到一个新命名空间,避免覆盖可能存在的残留:

bash
velero restore create --from-backup demo-backup --namespace-mappings demo:demo-restored
velero restore get
kubectl -n demo-restored exec writer -- cat /data/proof.txt

看到 hello-from-before-backup 打印出来,这次备份才算真的可用。如果 Velero 部署时没有配置卷快照,/data 会是空的——那正好说明「只备份了对象,没备份数据」。

演练、验证与 RPO/RTO

备份最大的谎言是「备份任务显示成功」。真正能证明它可用的只有一件事:恢复出来并且对得上

  • 定期恢复演练:至少每季度一次,把生产备份恢复到隔离命名空间,跑一遍业务自检。演练记录要写清楚从发起到验证通过用了多久——这就是实测 RTO。
  • 数据校验:数据库比 SELECT COUNT(*) 或关键表主键范围,Redis 比 DBSIZE,文件比 sha256sum。数字对得上才算数据完整。
  • 自动化:把演练写成 Job 或 CronJob,结果和耗时发到 可观测性 里的告警通道。人工演练一定会被别的事挤掉。
指标含义怎么压低
RPO(恢复点目标)最多能接受丢多少数据,用时间衡量提高备份频率、开 binlog 或增量、用同步复制
RTO(恢复时间目标)最多能接受中断多久自动化恢复流程、定期演练、预置环境

对齐的方式是反着问业务方三个问题:丢多少数据算事故?停多久算事故?多久前的数据必须能拿回来? 如果业务说「丢 15 分钟可以,停 1 小时可以」,那备份频率就得高于 15 分钟一次,恢复流程要能在 1 小时内跑完并验证。注意 RTO 包含「发现故障 + 决策 + 执行恢复 + 验证」的全过程,只算 kubectl apply 的时间会严重低估。

恢复演练:从备份到可用数据的五步

演练的目的不是「跑通流程」,而是量出真实 RTO 并发现备份里缺了什么。固定五步,每一步都有产物:

  1. 建隔离环境:新建命名空间或临时集群,绝不在生产命名空间里做恢复,避免恢复动作污染线上。
  2. 按依赖顺序恢复:先恢复 CRD 与 StorageClass,再恢复对象,最后灌数据。顺序反了就会卡在「对象找不到 CRD」或「PVC 找不到存储类」。
  3. 校验数据:数据库比行数与主键范围,Redis 比 DBSIZE,文件比 sha256sum。数字对不上就停下来查,别继续往上放流量。
  4. 跑业务自检:用应用自己的健康检查或一条只读请求验证,而不是只看 Pod 是 Running
  5. 记录并清理:记下「发起时间 → 校验通过时间」(这就是实测 RTO)、备份文件大小与缺失项,然后删掉临时环境。
bash
start=$(date +%s)
velero restore create --from-backup demo-backup --namespace-mappings demo:drill
kubectl -n drill exec deploy/redis -- redis-cli -a "$REDIS_PASSWORD" DBSIZE
echo "RTO=$(($(date +%s) - start))s"
kubectl delete namespace drill

演练要定期做,而且要换人做。 写脚本的人自己执行,永远发现不了「路径只有作者知道」这类问题。数据库的恢复优先用 Operator 的接口(KubeBlocks 用 kbcli cluster restore,TiDB 用 BR 的 Restore 对象),细节见数据库 Operator

参考:reference/k8s-in-action/db/kubeblocks/kubeblocks-0.9.3/README.md

备份前后的一致性校验

卷快照默认是崩溃一致性的(等于拔电源那一刻的字节):文件类负载打快照前用 fsfreeze -f /data 冻结写入、快照 READYTOUSE 后再 fsfreeze -u /data 解冻,数据库则用引擎自己的静默接口,不要指望卷快照一定一致。校验也要留证据:备份前造一份已知数据并记下校验值,恢复后重算,再用 elbencho 量一次吞吐——数据对得上、速率没塌,这次备份才算可用。

bash
fio --name=prep --filename=/data/verifyfile --size=2G --bs=1M --rw=write --direct=1
sha256sum /data/verifyfile | tee /data/verifyfile.sha256   # 备份前记下
sha256sum -c /data/verifyfile.sha256                       # 恢复后,输出 OK 才算成功
elbencho -w -b 4m -s 16g --direct /data/verify             # 恢复后的卷再量一次吞吐

参考:reference/k8s-in-action/storage/elbencho/README.mdreference/k8s-in-action/storage/volumesnapshots/README.md

常见坑与速查表

备份恢复里最致命的四个坑

  • 备份和源数据在同一块盘:本机 /var/backups、同一个存储池的快照,都不算备份。介质要分开,最好跨机房。
  • 只备份了 PVC,没备份 CRD 与 ConfigMap:恢复时对象起不来,或者自定义资源找不到对应 CRD。Velero 的 backup logs 会提示跳过了哪些资源。
  • 恢复时命名空间或 StorageClass 不一致:目标命名空间不存在、原 StorageClass 在新集群没有同名实现,恢复就会卡在 Pending。
  • 快照驱动不支持VolumeSnapshot 一直 READYTOUSE=false,或 Velero 报 no snapshotter found。用 kubectl get csidrivers 与 Class 的 driver 字段核对。
现象原因怎么确认
Velero 备份显示 PartiallyFailed部分资源无权限或 CRD 缺失velero backup logs <备份名> 看具体跳过项
恢复后 PVC 一直 PendingStorageClass 不存在或不兼容kubectl get pvc -o yamlstorageClassName,对比目标集群
恢复后数据是空的只恢复了对象,没有卷快照velero backup describe <备份名> --details 看卷快照状态
VolumeSnapshotREADYTOUSECSI 驱动不支持快照或 Class 的 driver 写错kubectl describe volumesnapshot 看 Events
etcd 恢复后集群行为异常恢复时 apiserver 仍在运行,或只恢复了部分成员检查静态 Pod 清单与 etcd 日志,确认恢复期间没有写入

自测题

自测:既然 etcd 快照能恢复一切,为什么还要 Velero?(点击展开答案)

因为两者粒度不同。etcd 快照恢复的是整个集群在某一个时间点的对象状态,是全有或全无:你没法只把 demo 命名空间退回昨天,恢复就意味着整个集群回到那个时刻,之后所有变更都丢失。

Velero 的粒度是命名空间、资源类型甚至单个对象,还能带上卷数据、支持恢复到另一个命名空间或另一个集群,适合「误删了一个命名空间」这类日常事故。生产上的组合是:etcd 快照应对集群级灾难,Velero 应对业务级误删。

自测:为什么 etcd 恢复必须先停掉 apiserver?(点击展开答案)

apiserver 是 etcd 唯一的写入方。如果恢复过程中 apiserver 还在跑,控制器和客户端会继续写新对象,snapshot restore 写回旧状态之后,这些新写入要么被覆盖、要么和新数据混在一起,恢复出来的集群状态无法预测。

停 apiserver 也意味着控制器停止工作,整个集群进入静默状态。所以恢复流程的第一步不是恢复数据,而是先让集群停止写入,这也是 etcd 恢复通常安排在变更窗口里的原因。

自测:卷快照和逻辑备份,为什么说后者不能省?(点击展开答案)

卷快照恢复的是「某个时刻磁盘上的原始字节」,它有三个绕不过去的限制:跨存储后端或跨云时不一定能恢复、只能整体恢复不能只捞一张表、依赖 CSI 驱动支持。

逻辑备份是应用自己导出的可读数据(SQL、RDB),能跨版本、跨云、按表恢复,还能直接校验行数。对核心数据来说,卷快照负责快速整体回滚,逻辑备份负责精确恢复与迁移,两者互为补充。

自测:为什么数据库的卷快照不能直接当备份用?(点击展开答案)

卷快照抓到的是崩溃一致性(crash-consistent)的镜像,相当于拔电源那一刻的字节。数据库恢复时会走自己的崩溃恢复流程(重放 redo / WAL),多数情况能起来,但页可能处于中间状态,跨卷的时序也无法保证(数据卷与日志卷来自不同快照点)。 要拿到应用一致性,必须在快照前让引擎进入备份模式或加锁,或者干脆用引擎自带的备份工具(BGSAVE、XtraBackup、pg_basebackup)。所以生产上不是「快照还是逻辑备份」,而是快照保速度、逻辑备份保正确与可迁移

小结

  • 备份分四层:etcd 对象状态、Git 里的清单、PVC 卷数据、镜像仓库,任何一层单独存在都不算备份。
  • etcdctl snapshot save/status 是集群级灾难的唯一整体恢复手段,恢复必须在 etcd 与 apiserver 停止之后进行。
  • Velero 把对象与卷一起备份到对象存储,用 BackupRestoreSchedule 三种资源声明策略,backup logs 是判断备份完整性的关键证据。
  • VolumeSnapshot 依赖 CSI 驱动与 snapshot-controller,恢复时用 dataSource 新建 PVC。
  • 有状态应用用卷快照做整卷回滚、用逻辑备份做精确恢复,快照必须转存到不同介质。
  • 备份可用性的唯一证明是恢复演练加数据校验,RPO 与 RTO 是和业务方对齐的共同语言。

练习

  1. 给你手上的一个业务定一份备份方案:四层里每层用什么手段、多久一次、存在哪、谁负责验证。
  2. demo-dailyschedule 改成每 5 分钟一次,观察备份的产生与过期,说说为什么生产上不能对所有数据都这么干。
  3. 设计一次恢复演练:列出步骤、预计耗时、验证方法,并说明如果实测 RTO 超出业务要求,你会先优化哪一步。

数据能回来了,但拉镜像还得靠外网。下一章 镜像仓库与加速 讲怎么让拉取又快又稳。