Kubernetes 备份完全实战:用 Velero 搞定集群备份、恢复与跨集群迁移(附完整命令与踩坑记录)
本文为实战指南,所有命令与配置基于 Velero v1.18.x(写作时最新稳定版 v1.18.2)编写,可直接复制使用。目标读者:有 Kubernetes 使用经验的后端 / SRE / 运维工程师。
目录
- [Kubernetes 备份完全实战:用 Velero 搞定集群备份、恢复与跨集群迁移(附完整命令与踩坑记录)](#Kubernetes 备份完全实战:用 Velero 搞定集群备份、恢复与跨集群迁移(附完整命令与踩坑记录))
-
- [前言:为什么 Kubernetes 备份不能只靠 etcd snapshot](#前言:为什么 Kubernetes 备份不能只靠 etcd snapshot)
- [一、Velero 是什么:核心概念与架构解析](#一、Velero 是什么:核心概念与架构解析)
-
- [1.1 一句话定位](#1.1 一句话定位)
- [1.2 组件架构](#1.2 组件架构)
- [1.3 核心资源速查表](#1.3 核心资源速查表)
- [1.4 v1.18 有什么值得升级的](#1.4 v1.18 有什么值得升级的)
- 二、环境准备与安装部署
-
- [2.1 部署 MinIO 作为备份后端(自建集群场景)](#2.1 部署 MinIO 作为备份后端(自建集群场景))
- [2.2 安装 CLI](#2.2 安装 CLI)
- [2.3 准备凭证](#2.3 准备凭证)
- [2.4 执行安装](#2.4 执行安装)
- [2.5 验证安装](#2.5 验证安装)
- [三、备份实战:按需备份、定时备份与 Hook 应用一致性](#三、备份实战:按需备份、定时备份与 Hook 应用一致性)
-
- [3.1 按需备份:selector、namespace 过滤与 TTL](#3.1 按需备份:selector、namespace 过滤与 TTL)
- [3.2 定时备份:Schedule CR](#3.2 定时备份:Schedule CR)
- [3.3 Hook:解决备份一致性问题的正确姿势](#3.3 Hook:解决备份一致性问题的正确姿势)
- [四、三种卷数据保护路线:CSI 快照 vs fs-backup vs Data Mover](#四、三种卷数据保护路线:CSI 快照 vs fs-backup vs Data Mover)
-
- [4.1 三种路线对比](#4.1 三种路线对比)
- [4.2 三种路线的配置方式](#4.2 三种路线的配置方式)
- [4.3 我的选型建议](#4.3 我的选型建议)
- 五、恢复实战与跨集群迁移
-
- [5.1 基础恢复](#5.1 基础恢复)
- [5.2 跨集群迁移:完整流程](#5.2 跨集群迁移:完整流程)
- 六、生产环境最佳实践与踩坑记录
-
- [6.1 六个真实踩坑与排查路径](#6.1 六个真实踩坑与排查路径)
- [6.2 监控与验证体系](#6.2 监控与验证体系)
- [6.3 3-2-1 原则在 K8s 的落地](#6.3 3-2-1 原则在 K8s 的落地)
- [七、选型对比:Velero vs Kasten K10 vs Trilio vs Stash](#七、选型对比:Velero vs Kasten K10 vs Trilio vs Stash)
- 八、总结与展望
- 参考资料
前言:为什么 Kubernetes 备份不能只靠 etcd snapshot
很多团队的第一反应是:"K8s 集群备份嘛,我给 etcd 打个快照不就行了?"
这个想法在控制面灾备上是成立的------etcd snapshot 确实能把 API 对象原样恢复回来。但它解决不了三个致命问题:
第一,etcd 快照不包含持久卷数据。 你的 MySQL 数据、Prometheus 的 TSDB、Kafka 的日志段文件,全都在 PV 里,etcd 里只有 PVC 和 PV 这层"壳"。etcd 恢复之后,应用起来了,数据却是空的。
第二,etcd 快照是全量原子快照,无法选择性恢复。 误删一个 namespace 想只捞回这一个 ns 的资源?etcd 快照做不到,它只能整体回滚,代价是整个集群回到某个时间点,期间其他 namespace 的变更全部丢失。
第三,etcd 快照无法跨集群迁移。 迁移到另一个集群、甚至另一个云厂商时,etcd 数据里的证书、node 信息、ProviderID 等内容不但无用,还可能有害。
所以一个必须建立的认知是:etcd 备份 ≠ 应用备份,etcd 备份 ≠ Kubernetes 备份。etcd 快照属于"集群级灾难恢复(DR)"范畴,应该进 runbook;而"资源对象 + 持久卷数据"的可选择性、可移植性备份,需要另一个工具来承接------这就是 Velero 的位置。
Velero 前身是 Heptio Ark,2019 年随 Heptio 被 VMware 收购,之后长期在 VMware Tanzu 体系下演进,2026 年 3 月被 Broadcom 正式捐赠进入 CNCF Sandbox (2026-07-30 官宣),GitHub 仓库也迁移到了 velero-io/velero。这个治理变化对使用者的意义是:它不再绑定任何单一厂商的商业路线图,作为 CNCF 项目,它的中立性和长期存续能力更强了------这也是我把 Velero 作为 K8s 备份默认推荐项的重要理由之一。
当前稳定版本为 v1.18.2(2026-06-26 发布),官方支持线为当前 minor + n-1,即 v1.18 与 v1.17。本文所有内容以 v1.18 文档为准。

一、Velero 是什么:核心概念与架构解析
1.1 一句话定位
Velero 是一个开源的 Kubernetes 集群资源与持久卷备份、恢复、迁移工具,Apache-2.0 协议。它的核心机制可以用一句话概括:
每个操作都是 CRD。 备份、恢复、定时任务全部以自定义资源(Backup / Restore / Schedule......)的形式存在,由集群内的 controller 处理;对象存储是唯一的 source of truth。
这句话是理解 Velero 所有行为的钥匙。因为备份就是 CR,所以它可以被 kubectl 查看、被 GitOps 管理、被 Argo CD 校验;因为对象存储是 source of truth,所以跨集群迁移天然就是"目标集群指向同一个备份桶,然后 restore"这么简单。
1.2 组件架构
Velero 由三部分组成:
- 本地 CLI:用户入口,本质上只做一件事------向 API Server 提交 CR;
- Velero Server (Deployment,跑在
veleronamespace):核心 controller,处理 Backup、Restore、Schedule、GC 等逻辑; - node-agent(DaemonSet,每个节点一个):负责文件级备份(fs-backup,底层引擎是 Kopia)和数据移动(data mover)。
下面这张图展示了完整的备份流程全景:
#mermaid-svg-cj5Y59gT7S1eCvTg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cj5Y59gT7S1eCvTg .error-icon{fill:#552222;}#mermaid-svg-cj5Y59gT7S1eCvTg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cj5Y59gT7S1eCvTg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cj5Y59gT7S1eCvTg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cj5Y59gT7S1eCvTg .marker.cross{stroke:#333333;}#mermaid-svg-cj5Y59gT7S1eCvTg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cj5Y59gT7S1eCvTg p{margin:0;}#mermaid-svg-cj5Y59gT7S1eCvTg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg .cluster-label text{fill:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg .cluster-label span{color:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg .cluster-label span p{background-color:transparent;}#mermaid-svg-cj5Y59gT7S1eCvTg .label text,#mermaid-svg-cj5Y59gT7S1eCvTg span{fill:#333;color:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg .node rect,#mermaid-svg-cj5Y59gT7S1eCvTg .node circle,#mermaid-svg-cj5Y59gT7S1eCvTg .node ellipse,#mermaid-svg-cj5Y59gT7S1eCvTg .node polygon,#mermaid-svg-cj5Y59gT7S1eCvTg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cj5Y59gT7S1eCvTg .rough-node .label text,#mermaid-svg-cj5Y59gT7S1eCvTg .node .label text,#mermaid-svg-cj5Y59gT7S1eCvTg .image-shape .label,#mermaid-svg-cj5Y59gT7S1eCvTg .icon-shape .label{text-anchor:middle;}#mermaid-svg-cj5Y59gT7S1eCvTg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cj5Y59gT7S1eCvTg .rough-node .label,#mermaid-svg-cj5Y59gT7S1eCvTg .node .label,#mermaid-svg-cj5Y59gT7S1eCvTg .image-shape .label,#mermaid-svg-cj5Y59gT7S1eCvTg .icon-shape .label{text-align:center;}#mermaid-svg-cj5Y59gT7S1eCvTg .node.clickable{cursor:pointer;}#mermaid-svg-cj5Y59gT7S1eCvTg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cj5Y59gT7S1eCvTg .arrowheadPath{fill:#333333;}#mermaid-svg-cj5Y59gT7S1eCvTg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cj5Y59gT7S1eCvTg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cj5Y59gT7S1eCvTg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cj5Y59gT7S1eCvTg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cj5Y59gT7S1eCvTg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cj5Y59gT7S1eCvTg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cj5Y59gT7S1eCvTg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cj5Y59gT7S1eCvTg .cluster text{fill:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg .cluster span{color:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cj5Y59gT7S1eCvTg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cj5Y59gT7S1eCvTg rect.text{fill:none;stroke-width:0;}#mermaid-svg-cj5Y59gT7S1eCvTg .icon-shape,#mermaid-svg-cj5Y59gT7S1eCvTg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cj5Y59gT7S1eCvTg .icon-shape p,#mermaid-svg-cj5Y59gT7S1eCvTg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cj5Y59gT7S1eCvTg .icon-shape .label rect,#mermaid-svg-cj5Y59gT7S1eCvTg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cj5Y59gT7S1eCvTg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cj5Y59gT7S1eCvTg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cj5Y59gT7S1eCvTg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交 Backup CR
watch
1 查询并收集资源
2 上传资源 YAML 与元数据
3 卷数据处理
3 卷数据处理
备份数据流
快照元数据
velero CLI
API Server
Velero Server Controller
对象存储 BSL
S3 / GCS / Azure Blob / MinIO
node-agent 节点A
Kopia fs-backup 或 data mover
node-agent 节点B
CSI 快照
需要强调的一个关键细节:Velero 走 API Server 备份对象,不碰 etcd 本身。这带来了可移植性(YAML 走到哪里都能恢复),也带来了两个推论:
- 推论一:CRD 必须在备份范围内(或由 GitOps 重建),否则恢复出来的 CR 没有对应的 API 资源,直接变成孤儿------这是后文踩坑章节的真实案例;
- 推论二:备份不是严格原子的。备份过程中新创建/修改的对象可能不被包含,对一致性要求高的场景需要 Hook 配合(第三章)。
从 controller 视角再看一遍备份的生命周期,有助于理解后文的排障思路:CLI 提交 Backup CR 后,BackupController 先做验证(BSL 是否 Available、参数是否合法),然后通过 API Server 的 list/watch 收集目标资源,将资源序列化为 JSON 并打包上传到对象存储,同时根据配置决定卷数据的处理路线(CSI 快照或 node-agent 数据移动)。每一步的状态都会写回 Backup CR 的 status.phase(New → InProgress → Completed / Failed / PartiallyFailed),velero backup describe 读到的就是这个状态机。特别值得留意 PartiallyFailed 这个中间态:资源备份成功但部分卷备份失败时备份会停在这个状态,Phase: PartiallyFailed 同样要人工介入,不能只盯着 Failed 报警。
1.3 核心资源速查表
| 资源(CRD) | 作用 | 关键说明 |
|---|---|---|
| Backup | 描述一次备份:包含哪些 ns/资源、是否含卷数据、TTL | CLI velero backup create 就是提交这个 CR |
| Restore | 描述一次恢复:从哪个 Backup 恢复、namespace 映射 | 默认非破坏性,已存在的资源会被跳过 |
| Schedule | 定时备份,内嵌 Backup 模板 + cron 表达式 | 产生的 Backup 命名带时间戳 |
| BackupStorageLocation(BSL) | 备份元数据/资源文件存到哪个对象存储桶 | 支持多 BSL;对象存储是 source of truth |
| VolumeSnapshotLocation(VSL) | 云盘快照存到哪(区域/账户配置) | 仅 CSI/云快照路线需要 |
| DataUpload | 一次数据移动任务的执行记录 | 排查备份卡 InProgress 时先看它 |
| DownloadRequest | 申请下载备份内容(日志、报告) | velero backup logs 的底层实现 |
| ServerStatusRequest | 查询 Velero server 版本与状态 | velero version --client-only 之外的服务端校验 |
1.4 v1.18 有什么值得升级的
如果团队还在 v1.15 以下,v1.18 值得作为升级目标,几个特性直击生产痛点:
- 并发备份处理:v1.18 之前备份是全局串行的,多租户场景下 A 团队一个 2 小时的大备份会阻塞 B 团队 10 分钟的小备份。现在不同用户的备份可以同时进行;
- 数据移动器缓存卷:restore 时 CSI snapshot data movement 与 fs-backup 的 data mover pod 可以配置 cache volume,解决了两类老问题------临时盘受限的节点上备份失败、同节点多个 data mover 并发互相踩踏;
- 增量备份大小报告:增量备份到底传了多少数据,现在可观察了,容量规划不再是拍脑袋;
- 通配符 namespace 过滤 :支持 Glob 模式,如
includedNamespaces: ['team-*']、排除test-*,多租户场景的配置量大幅下降; - VolumePolicy 增强:可以按 PVC phase 过滤,跳过 Pending/Lost 状态的 PVC------以前这种 PVC 会导致备份报错或备出空数据;
- 防 OOM:大型备份仓库(repo)操作从 Velero server 主进程移出,几十 GB 级备份仓库维护时 server 不再被 OOM kill。
另外 v1.17 带来的 VolumeGroupSnapshot(多卷崩溃一致性快照)、现代化 fs-backup 架构和 Windows 节点支持也值得关注。技术栈上,当前版本基于 Go 1.25.7 与 Kopia 0.22.3。
二、环境准备与安装部署
本章以"自建集群 + MinIO 对象存储"为例(这是最容易本地复现的组合),同时给出云上(AWS S3)的等价命令。以下所有配置均为示例而非配方------原理和参数逻辑是通用的,具体 provider 与参数请按你的环境替换。
2.1 部署 MinIO 作为备份后端(自建集群场景)
先创建 namespace 并部署 MinIO,包含 PVC、Deployment、Service:
yaml
apiVersion: v1
kind: Namespace
metadata:
name: velero
---
apiVersion: v1
kind: Secret
metadata:
name: minio-credentials
namespace: velero
type: Opaque
data:
accesskey: bWluaW9hZG1pbg== # minioadmin(base64)
secretkey: bWluaW9hZG1pbg== # minioadmin(base64,生产环境务必更换!)
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minio-pvc
namespace: velero
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 100Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
namespace: velero
spec:
replicas: 1
selector:
matchLabels:
app: minio
template:
metadata:
labels:
app: minio
spec:
containers:
- name: minio
image: minio/minio:latest
args:
- server
- /data
- --console-address=:9001
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: minio-credentials
key: accesskey
- name: MINIO_ROOT_SECRET
valueFrom:
secretKeyRef:
name: minio-credentials
key: secretkey
ports:
- containerPort: 9000
name: api
- containerPort: 9001
name: console
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: minio-pvc
---
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: velero
spec:
type: NodePort
selector:
app: minio
ports:
- name: api
port: 9000
targetPort: 9000
nodePort: 30900
- name: console
port: 9001
targetPort: 9001
nodePort: 30901
---
apiVersion: batch/v1
kind: Job
metadata:
name: minio-init
namespace: velero
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: mc
image: minio/mc:latest
command: ["/bin/sh", "-c"]
args:
- mc alias set local http://minio:9000 minioadmin minioadmin && mc mb local/velero-backups
注意:上面用 PVC 跑 MinIO 只适合 POC 与演练环境。生产环境把备份存在同一个集群里是伪备份------集群挂了备份一起没,后文最佳实践章节会展开 3-2-1 原则。
2.2 安装 CLI
bash
# Linux (amd64)
wget https://github.com/velero-io/velero/releases/download/v1.18.2/velero-v1.18.2-linux-amd64.tar.gz
tar -xzf velero-v1.18.2-linux-amd64.tar.gz
sudo mv velero-v1.18.2-linux-amd64/velero /usr/local/bin/
velero version --client-only
# macOS
brew install velero
2.3 准备凭证
Velero 的 credentials 文件必须是 INI 格式 ,这是高频翻车点(写成裸的 key=value 会导致 BSL 一直 Unavailable):
ini
[default]
aws_access_key_id=minioadmin
aws_secret_access_key=minioadmin
云上 AWS IAM 用户同理:
ini
[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
2.4 执行安装
MinIO 场景(S3 兼容存储走 AWS 插件):
bash
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.0.0 \
--bucket velero-backups \
--secret-file ./credentials \
--backup-location-config region=minio,s3Url=http://minio.velero.svc:9000,s3ForcePathStyle=true \
--use-node-agent \
--default-volumes-to-fs-backup
AWS 云上场景:
bash
velero install --provider aws --plugins velero/velero-plugin-for-aws:v1.0.0 --bucket backups --secret-file ./aws-iam-creds --backup-location-config region=us-east-2 --snapshot-location-config region=us-east-2 --use-node-agent
velero install 关键参数详解:
| 参数 | 作用 | 建议 |
|---|---|---|
--provider |
存储与快照 provider | S3 兼容存储一律填 aws |
--plugins |
插件镜像 | 必须与 provider 匹配,版本对齐 Velero 主版本 |
--bucket |
BSL 桶名 | 建议按集群/环境分桶,至少分前缀 |
--secret-file |
credentials(INI) | 格式错误是 BSL Unavailable 第一大原因 |
--use-node-agent |
部署 node-agent DaemonSet | 需要文件级备份或 data mover 时必须开启 |
--default-volumes-to-fs-backup |
默认所有卷走 fs-backup | 云盘无快照能力或混合环境时建议开启 |
--backup-location-config |
BSL 附加配置 | S3 兼容存储需 s3Url + s3ForcePathStyle=true |
--snapshot-location-config |
VSL 配置(如 region) | CSI/云快照路线需要 |
--velero-pod-cpu-request 等 |
资源 request/limit | 大集群建议显式设置,默认值偏小 |
--use-volume-snapshots |
是否启用卷快照 | 纯 fs-backup 场景可设为 false |
2.5 验证安装
bash
kubectl get pods -n velero
kubectl get backupstoragelocation -n velero
# 期望状态:velero pod Running;BSL 的 PHASE 为 Available
# 如果 BSL 显示 Unavailable,用 describe 看具体报错:
kubectl describe backupstoragelocation default -n velero
验证 MinIO 桶中已生成目录结构(backups/、restores/ 等),再创建一个测试备份:
bash
kubectl create namespace backup-test
kubectl run nginx-test --image=nginx -n backup-test
velero backup create smoke-test --include-namespaces backup-test --wait
velero backup describe smoke-test --details
--wait 会阻塞到备份完成,适合脚本化验证;describe --details 输出中的 Phase: Completed 与 Backup Volumes 部分是判断备份是否真正成功的依据。
三、备份实战:按需备份、定时备份与 Hook 应用一致性
3.1 按需备份:selector、namespace 过滤与 TTL
bash
# 按 label selector 备份(只备份 app=nginx 的资源)
velero backup create nginx-backup --selector app=nginx
# 备份指定 namespace
velero backup create prod-backup --include-namespaces production,databases
# 通配符过滤(v1.18+):备份所有 tenant-* ns,排除 test-* 开头的
velero backup create tenant-backup --include-namespaces 'tenant-*' --exclude-namespaces 'test-*'
# 排除某些资源类型(例如不想备份 Event)
velero backup create app-backup --include-namespaces production --exclude-resources events.events.k8s.io
# 指定 TTL,超过后 GC controller 自动清理备份与快照
velero backup create release-backup --include-namespaces production --ttl 168h
三个 namespace 级选项的组合使用也值得展开:--include-namespaces 与 --exclude-namespaces 可以叠加,Velero 先取包含集再剔除排除集;--exclude-resources 则在资源类型维度再过滤一层。三者叠加的优先级是排除永远赢------即使某个资源同时命中包含规则,只要命中任一排除规则就不会进备份。这个语义在写复杂备份策略时要牢记,否则容易出现"以为备份了、其实被排除"的静默遗漏。此外,selector 只对有 label 的资源生效,没有 label 的裸资源不会被 selector 捕获,想全量保护就用 namespace 维度,selector 适合补充性精选。
TTL 默认 30 天,GC controller 每小时 跑一次。TTL 过期时 Velero 会删除:Backup CR、对象存储中的文件、卷快照以及关联的 Restore。这里有个容易被忽略的设计:删除失败不会无限重试到阻塞,而是给 Backup 打上 velero.io/gc-failure=<Reason> 标签 (如 BSLNotFound、BSLReadOnly),需要监控这个标签来发现"备份桶被误删后 GC 全面失败"这类次生问题。
关于对象存储的选择再多说两句。对象存储端不只是"找个桶放文件",它决定了备份体系的 RPO 与容灾半径:MinIO/Ceph 自建桶的恢复速度受限于机房带宽,公有云 S3/GCS 的存储成本则随保留期线性增长,TTL 分层策略必须和存储单价一起算账。另外,多 BSL(BackupStorageLocation)是 v1.18 之前的版本就已支持、但常被忽略的能力------可以把元数据与卷数据指向不同桶,或配置一个"只读第二 BSL"作为容灾副本,写进 Runbook 的 Disaster Recovery 段落。
另外提醒:备份不是严格原子的。备份运行期间新建的 Pod、写入的 ConfigMap 可能不在备份里。对有状态服务,这个问题要用 Hook 解决,见 3.3。
3.2 定时备份:Schedule CR
Schedule 支持纯 CLI 与 YAML 两种方式,生产环境推荐 YAML(可进 Git 管理):
bash
velero schedule create nginx-daily --schedule="0 1 * * *" --selector app=nginx
yaml
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: daily-production
namespace: velero
spec:
schedule: "0 2 * * *"
useOwnerReferencesInBackup: false
template:
includedNamespaces: [production, databases]
excludedNamespaces: [dev, test]
storageLocation: default
ttl: 720h
defaultVolumesToFsBackup: true
生产环境推荐分层备份策略,而不是单一全量:
| 层级 | 频率 | 范围 | TTL | 用途 |
|---|---|---|---|---|
| 高频层 | 每小时 | 核心数据库 ns | 24h | 快速回滚窗口 |
| 日常层 | 每日 | 全部生产 ns | 30d | 常规恢复 |
| 归档层 | 每周 | 全量 | 90d~180d | 合规与长期留档 |
3.3 Hook:解决备份一致性问题的正确姿势
Velero 拍的是卷的某个时点快照,如果备份瞬间数据库正在写 WAL 或刷脏页,恢复出来的数据就是"崩溃时点"状态------运气好能靠 redo 恢复,运气坏直接损坏。应用级一致性必须靠 Hook 主动冻结。
Pod annotation 方式(最常用):
yaml
apiVersion: v1
kind: Pod
metadata:
name: postgres
namespace: databases
annotations:
# 备份前执行逻辑备份,导出一致性的 dump 文件
pre.hook.backup.velero.io/command: '["/bin/sh", "-c", "pg_dump -U postgres mydb > /backup/dump.sql"]'
pre.hook.backup.velero.io/container: postgres
# 备份后清理 dump 文件,避免重复占空间
post.hook.backup.velero.io/command: '["/bin/sh", "-c", "rm /backup/dump.sql"]'
post.hook.backup.velero.io/container: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: backup-dump
mountPath: /backup
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: backup-dump
emptyDir: {}
另一种对应用零侵入的方案是文件系统冻结(FSFreeze),适合块存储快照路线:
yaml
annotations:
pre.hook.backup.velero.io/freeze: "true"
还可以直接在 Backup CR 中写 hook(推荐,随备份进 Git):
yaml
apiVersion: velero.io/v1
kind: Backup
metadata:
name: consistent-db-backup
namespace: velero
spec:
includedNamespaces: [databases]
hooks:
resources:
- name: freeze-postgres
includedNamespaces: [databases]
pre:
- exec:
container: postgres
command: ["/bin/sh", "-c", "pg_dump -U postgres mydb > /backup/dump.sql"]
onError: Fail
一个观点必须亮明:hook + fs-backup 的组合只保证"文件级可恢复",不等于"数据库级可恢复" 。fs-backup 是增量文件复制(Kopia),恢复的是目录树;对 PostgreSQL/MySQL 这类重状态数据库,dump 文件 + 卷快照的组合才稳妥。SQLite/嵌入式数据库则相反------它们没有网络协议入口,恰恰只能靠文件级方案(结合 backup.velero.io/backup-volumes 标注卷)。没有放之四海皆准的方案,一致性方案要按数据库类型选。
关于 hook 的两个实操细节也值得一提。其一,onError 的默认行为值得确认:hook 执行失败时备份会标记为 PartiallyFailed,如果你的备份体系里数据库一致性是硬要求,应该把 PartiallyFailed 纳入告警而不是忽略它。其二,hook 命令在应用容器内执行,这意味着镜像里必须包含 dump 工具(如 pg_dump)------不少精简版数据库镜像不带客户端工具,需要在构建镜像时补装,或者改用 sidecar 容器执行 dump(pre.hook.backup.velero.io/container 指向 sidecar)。这两点在方案评审时经常被漏掉,上线前务必验证。
四、三种卷数据保护路线:CSI 快照 vs fs-backup vs Data Mover
资源对象备份只有一条路(API Server → 对象存储),但持久卷数据有三条路线,这是 Velero 选型和使用中最容易混淆的部分。
#mermaid-svg-szqcrkBtEszHtQJk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-szqcrkBtEszHtQJk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-szqcrkBtEszHtQJk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-szqcrkBtEszHtQJk .error-icon{fill:#552222;}#mermaid-svg-szqcrkBtEszHtQJk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-szqcrkBtEszHtQJk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-szqcrkBtEszHtQJk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-szqcrkBtEszHtQJk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-szqcrkBtEszHtQJk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-szqcrkBtEszHtQJk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-szqcrkBtEszHtQJk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-szqcrkBtEszHtQJk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-szqcrkBtEszHtQJk .marker.cross{stroke:#333333;}#mermaid-svg-szqcrkBtEszHtQJk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-szqcrkBtEszHtQJk p{margin:0;}#mermaid-svg-szqcrkBtEszHtQJk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-szqcrkBtEszHtQJk .cluster-label text{fill:#333;}#mermaid-svg-szqcrkBtEszHtQJk .cluster-label span{color:#333;}#mermaid-svg-szqcrkBtEszHtQJk .cluster-label span p{background-color:transparent;}#mermaid-svg-szqcrkBtEszHtQJk .label text,#mermaid-svg-szqcrkBtEszHtQJk span{fill:#333;color:#333;}#mermaid-svg-szqcrkBtEszHtQJk .node rect,#mermaid-svg-szqcrkBtEszHtQJk .node circle,#mermaid-svg-szqcrkBtEszHtQJk .node ellipse,#mermaid-svg-szqcrkBtEszHtQJk .node polygon,#mermaid-svg-szqcrkBtEszHtQJk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-szqcrkBtEszHtQJk .rough-node .label text,#mermaid-svg-szqcrkBtEszHtQJk .node .label text,#mermaid-svg-szqcrkBtEszHtQJk .image-shape .label,#mermaid-svg-szqcrkBtEszHtQJk .icon-shape .label{text-anchor:middle;}#mermaid-svg-szqcrkBtEszHtQJk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-szqcrkBtEszHtQJk .rough-node .label,#mermaid-svg-szqcrkBtEszHtQJk .node .label,#mermaid-svg-szqcrkBtEszHtQJk .image-shape .label,#mermaid-svg-szqcrkBtEszHtQJk .icon-shape .label{text-align:center;}#mermaid-svg-szqcrkBtEszHtQJk .node.clickable{cursor:pointer;}#mermaid-svg-szqcrkBtEszHtQJk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-szqcrkBtEszHtQJk .arrowheadPath{fill:#333333;}#mermaid-svg-szqcrkBtEszHtQJk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-szqcrkBtEszHtQJk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-szqcrkBtEszHtQJk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-szqcrkBtEszHtQJk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-szqcrkBtEszHtQJk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-szqcrkBtEszHtQJk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-szqcrkBtEszHtQJk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-szqcrkBtEszHtQJk .cluster text{fill:#333;}#mermaid-svg-szqcrkBtEszHtQJk .cluster span{color:#333;}#mermaid-svg-szqcrkBtEszHtQJk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-szqcrkBtEszHtQJk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-szqcrkBtEszHtQJk rect.text{fill:none;stroke-width:0;}#mermaid-svg-szqcrkBtEszHtQJk .icon-shape,#mermaid-svg-szqcrkBtEszHtQJk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-szqcrkBtEszHtQJk .icon-shape p,#mermaid-svg-szqcrkBtEszHtQJk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-szqcrkBtEszHtQJk .icon-shape .label rect,#mermaid-svg-szqcrkBtEszHtQJk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-szqcrkBtEszHtQJk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-szqcrkBtEszHtQJk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-szqcrkBtEszHtQJk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否 或 需要增量/跨存储
否, 仅本地备份
是, 恢复到其他集群
PV 数据保护路线选择
云环境且有
CSI 快照驱动?
路线1 CSI 快照
优点: 块级快, 增量原生
缺点: 快照锁定在同集群/同区域
需要把数据
搬到对象存储?
路线2 fs-backup Kopia
优点: 无快照依赖, 增量压缩, 普适
缺点: 占节点 IO, 大卷慢
路线3 Data Mover
优点: 数据集中到对象存储, 可跨集群恢复
缺点: 全量拷贝耗时, 需 cache volume
4.1 三种路线对比
| 维度 | CSI 快照 | fs-backup(Kopia) | Data Mover(--snapshot-move-data) |
|---|---|---|---|
| 数据去向 | 云厂商快照服务 | 对象存储(Kopia repo) | 对象存储 |
| 恢复范围 | 同集群/同区域 | 任意可达 BSL 的集群 | 任意可达 BSL 的集群 |
| 依赖 | CSI driver + VolumeSnapshotClass | node-agent | node-agent + CSI 快照 |
| 性能 | 快(块级、增量) | 中(文件级、增量、压缩) | 慢(需完整搬运) |
| 增量能力 | 依赖云盘实现 | 原生增量 | 依赖底层快照 |
| 文件排除 | 不支持 | 支持(path 过滤) | 支持 |
| 跨集群迁移 | 需云厂商快照导入或 Data Mover | 直接可用 | 原生场景 |
4.2 三种路线的配置方式
路线 1:CSI 快照。确保集群安装了 CSI driver,并给 VolumeSnapshotClass 打标签(Velero 通过这个标签自动发现可用的 snapshot class):
bash
kubectl get volumesnapshotclass
kubectl label volumesnapshotclass csi-aws-vsc velero.io/csi-volumesnapshot-class=true
路线 2:fs-backup。两种粒度------全局默认或按卷标注:
bash
# 全局默认(安装时或修改 velero Deployment 参数)
velero install ... --use-node-agent --default-volumes-to-fs-backup
按卷标注(Pod annotation,只备份指定卷):
yaml
metadata:
annotations:
backup.velero.io/backup-volumes: data,config
路线 3:Data Mover 。在备份时加 --snapshot-move-data,Velero 会先取 CSI 快照,再把快照数据搬运到对象存储:
bash
# 典型场景:迁移前的大备份,--wait 阻塞等待完成
velero backup create pre-migration --include-namespaces 'tenant-*' --snapshot-move-data --wait
v1.18 起,data mover pod 可以配置 cache volume,缓解临时盘受限和同节点并发失败问题(VolumePolicy 中配置,也可按 PVC phase 跳过 Pending/Lost 的 PVC)。
4.3 我的选型建议
决策依据(按优先级排列):
- 恢复目标决定路线。如果备份的最大价值是"集群炸了快速重建",CSI 快照最快;如果最大价值是"跨集群/跨云迁移"或"对象存储归档",快照路线的锁定性是硬伤,应选 fs-backup 或 Data Mover。
- 有 CSI 快照能力就别硬扛文件级备份。40GB PVC 用 fs-backup 实测约 15 分钟(2 worker 环境),块级快照通常秒级完成且对应用透明。
- 混合环境直接
--default-volumes-to-fs-backup兜底,再对大卷、快照友好的云盘单独覆盖为 CSI 路线,这是省心且可控的组合。 - Data Mover 是迁移专用工具而非日常备份工具------全量搬运成本高,日常备份用它得不偿失。
一句话总结:日常备份看 CSI 能力,归档与迁移看 BSL 连通性,别让"哪条路线最先进"绑架决策。
五、恢复实战与跨集群迁移
5.1 基础恢复
bash
# 从指定备份恢复(默认恢复备份中的全部资源)
velero restore create --from-backup nginx-backup
# 恢复到测试 namespace 做验证(强烈推荐先做这一步再动生产)
velero restore create --from-backup daily-production-20260424 --namespace-mappings production:restore-test
# 已存在资源改为"更新"而非"跳过"
velero restore create --from-backup nginx-backup --existing-resource-policy=update
三条命令对应三个关键认知:
认知一:默认恢复是非破坏性的。 已存在的资源会被跳过(不改不删),这在"往现有集群补资源"场景是安全网,但在"彻底回滚"场景会造成新旧混杂------需要 --existing-resource-policy=update。
认知二:先恢复到影子 namespace 验证,是零成本的保命习惯。 --namespace-mappings production:restore-test 一条命令就能得到完整的生产副本,验证应用能起、数据在、连接串对,再决定是否真恢复。
认知三:API 版本必须匹配。 备份使用源集群 API server 的 preferred version 存储,恢复时目标集群必须存在相同的 group/version。K8s 版本差异过大(如 1.22 → 1.31)时,被移除的 API(如旧版 extensions/v1beta1 Ingress)会恢复失败。
5.2 跨集群迁移:完整流程
跨集群迁移是 Velero 最能体现"对象存储是 source of truth"设计的场景。目标集群只需要满足两个条件:指向同一个(或内容相同的)BSL 桶,且 Velero 版本兼容。
目标集群 Velero 对象存储 BSL 源集群 Velero 运维/CLI 目标集群 Velero 对象存储 BSL 源集群 Velero 运维/CLI #mermaid-svg-sqSAPVZcx7Md2RHY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sqSAPVZcx7Md2RHY .error-icon{fill:#552222;}#mermaid-svg-sqSAPVZcx7Md2RHY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sqSAPVZcx7Md2RHY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sqSAPVZcx7Md2RHY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sqSAPVZcx7Md2RHY .marker.cross{stroke:#333333;}#mermaid-svg-sqSAPVZcx7Md2RHY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sqSAPVZcx7Md2RHY p{margin:0;}#mermaid-svg-sqSAPVZcx7Md2RHY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sqSAPVZcx7Md2RHY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sqSAPVZcx7Md2RHY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sqSAPVZcx7Md2RHY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sqSAPVZcx7Md2RHY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sqSAPVZcx7Md2RHY .sequenceNumber{fill:white;}#mermaid-svg-sqSAPVZcx7Md2RHY #sequencenumber{fill:#333;}#mermaid-svg-sqSAPVZcx7Md2RHY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sqSAPVZcx7Md2RHY .messageText{fill:#333;stroke:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sqSAPVZcx7Md2RHY .labelText,#mermaid-svg-sqSAPVZcx7Md2RHY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .loopText,#mermaid-svg-sqSAPVZcx7Md2RHY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sqSAPVZcx7Md2RHY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sqSAPVZcx7Md2RHY .noteText,#mermaid-svg-sqSAPVZcx7Md2RHY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sqSAPVZcx7Md2RHY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sqSAPVZcx7Md2RHY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sqSAPVZcx7Md2RHY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sqSAPVZcx7Md2RHY .actorPopupMenu{position:absolute;}#mermaid-svg-sqSAPVZcx7Md2RHY .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sqSAPVZcx7Md2RHY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sqSAPVZcx7Md2RHY .actor-man circle,#mermaid-svg-sqSAPVZcx7Md2RHY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sqSAPVZcx7Md2RHY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} velero backup create pre-migration --snapshot-move-data --wait 上传资源元数据 + 卷数据 备份完成 配置相同 BSL (backup-location-config) 同步 backup 元数据 backup pre-migration 可见 velero restore create --from-backup pre-migration 拉取资源与卷数据 恢复完成 验证 Pod 状态 / PVC 绑定 / Service 连通性
实操步骤:
bash
# ---- 源集群 ----
# 1. 迁移前备份(含卷数据搬运)
velero backup create pre-migration --include-namespaces 'tenant-*' --snapshot-move-data --wait
# ---- 目标集群 ----
# 2. 安装 Velero(相同版本 + 指向同一个桶)
velero install --provider aws --plugins velero/velero-plugin-for-aws:v1.0.0 \
--bucket velero-backups --secret-file ./credentials \
--backup-location-config region=minio,s3Url=http://minio.backup-svc:9000,s3ForcePathStyle=true \
--use-node-agent
# 3. 确认备份元数据已同步(对象存储是 source of truth,无需手动导入)
velero backup-location get
velero backup get
# 4. 恢复(可按需做 namespace 映射)
velero restore create --from-backup pre-migration
# 5. 验证
kubectl get pods -A | grep tenant
velero restore describe pre-migration-20260929120000 --details
两个迁移场景的特有注意点:
- StorageClass 差异 :源集群的
storageClassName会原样恢复,目标集群没有同名 SC 时 PVC 会一直 Pending。迁移前先kubectl get sc对齐,或在恢复前修改备份中资源的 SC 字段(可借助 velero 的 ResourceModifier 或提前在目标集群创建同名 SC); - 节点亲和与 ProviderID :Pod 的
nodeSelector/nodeAffinity如果绑定了源集群的节点名或拓扑域(zone),目标集群会调度不上;云盘快照携带的可用区约束同理。这些"环境绑定字段"是 etcd 方案跨不了集群、而 Velero 也需要人工预处理的残留点。
六、生产环境最佳实践与踩坑记录
6.1 六个真实踩坑与排查路径
坑 1:备份卡在 InProgress 不动。
排查顺序:kubectl get dataupload -n velero 看数据移动任务状态 → 看 node-agent 日志 → 检查 credentials 是否过期 → 检查 bucket 写权限。data mover 失败或 Velero pod 崩溃是最常见根因。v1.18 的 cache volume 特性修复了其中"同节点并发失败"这一子类。
坑 2:恢复后 PVC 一直 Pending。
三个可能原因逐一排查:目标集群 StorageClass 不存在或名字不同;源集群 CSI driver 注释(volume.beta.kubernetes.io/storage-provisioner 等 storage-provisioner annotation)残留------v1.16 之前的版本有此 bug,需手动清理 annotation;目标集群缺少 VolumeSnapshotClass 或没打 velero.io/csi-volumesnapshot-class=true 标签。
坑 3:CRD 没备份,恢复后业务全断。
真实案例:某团队用 Traefik,IngressRoute 是 CRD。备份时只选了应用 namespace,CRD(cluster-scoped)不在范围内。恢复后 Deployment 和 Service 都在,但 IngressRoute 对象因 API 不存在而恢复失败------全站流量入口静默丢失。对策:备份范围显式包含 CRD,或由 GitOps/CI 负责重建 CRD,并明确写进恢复 runbook。
坑 4:备份"成功",恢复出来是空卷。
另一个真实案例:应用内嵌 SQLite(数据写在 PV 里的 .db 文件),备份时没有标注 backup.velero.io/backup-volumes,fs-backup 没有覆盖该卷,恢复后 Sonarr 数据全空。教训:备份describe里的 "Backup Volumes" 明细必须人工核对 ,Phase: Completed 只代表流程完成,不代表数据齐了。
坑 5:BSL 一直 Unavailable。
按命中率排序:credentials 格式错误(必须是 INI 的 [default] 段);bucket 不存在或拼写错误;MinIO/存储端点不可达(s3Url 填错或没加 s3ForcePathStyle=true);S3 签名版本不兼容(部分 S3 兼容实现需要显式降级签名版本)。
坑 6:恢复时报 Already Exists,资源没进去。
默认非破坏性恢复跳过了已存在资源。按场景选:确认覆盖 → --existing-resource-policy=update;只是补缺 → 保持默认并人工 diff。
6.2 监控与验证体系
bash
# 备份明细与日志(排障三板斧)
velero backup describe production-backup --details
velero backup logs production-backup
# 数据移动任务与存储位置健康
kubectl get dataupload -n velero
velero backup-location get
# 恢复结果检查
velero restore describe <restore-name> --details
velero restore logs <restore-name>
比命令更重要的是机制:每月一次恢复演练 。把 velero restore create --from-backup daily-production-YYYYMMDD --namespace-mappings production:dr-drill-<date> 做成例行任务,验证核心应用能启动、关键表有数据。不测试的备份等于没有备份------这不是修辞,是无数团队在真实故障中用数据空卷换来的教训。
资源占用参考(2 worker 实测数据):Velero server 空闲约 50MB 内存;node-agent 备份期间约 100MB;40GB PVC fs-backup 约 15 分钟;压缩后每日增量约 8GB。这个量级说明 Velero 的常驻开销很低,瓶颈在备份窗口期的网络与磁盘 IO。
6.3 3-2-1 原则在 K8s 的落地
经典 3-2-1:3 份数据副本、2 种存储介质、1 份异地。落到 K8s 备份体系:
| 维度 | 落地方式 |
|---|---|
| 3 份副本 | 集群内数据(源)+ BSL 对象存储 + 云快照或第二 BSL |
| 2 种介质 | 对象存储 + 块存储快照(或磁带/冷存储归档) |
| 1 份异地 | 备份桶与集群不同 region、不同账号,防止权限误删与区域级故障 |
| 应用一致性 | pre-hook 数据库一致性冻结(pg_dump/mongodump) |
| 成本控制 | 排除 dev/test namespace;TTL 分层(24h/30d/90d) |
| 元数据保障 | CRD 与集群配置进 Git(GitOps),与 Velero 备份互补 |
特别强调最后一条:GitOps 是 Velero 的元数据备份,Velero 是 GitOps 的数据备份,两者互补而非替代。CRD、RBAC、ConfigMap 这类声明式资源由 Git 管理,Velero 专注于 PV 数据和快速整体恢复,这是当代 K8s 备份体系的标准双保险结构。
七、选型对比:Velero vs Kasten K10 vs Trilio vs Stash
以下评分来自 faun.dev 2026 年的横向评测(11 维度、百分制),供参考而非定论:
| 工具 | 总分 | 易用性 | 成熟度 | 移植性 | 定价透明度 | 许可/定价模式 |
|---|---|---|---|---|---|---|
| Velero | 77 | 70 | 80 | 88 | 100 | 开源免费(Apache-2.0),CNCF Sandbox |
| Kasten K10 | 68 | 78 | 76 | 48 | 45 | 商业收费,Veeam 旗下 |
| Trilio | 58 | 62 | 62 | 52 | 22 | 按节点计费 |
| Stash | 56 | 60 | 52 | 58 | 40 | 开源 + 商业版 |
逐一点评:
- Velero:开源免费、CNCF 治理、移植性最强(S3/GCS/Azure/MinIO/Ceph 等皆可接)、社区最大。弱项也很明确:没有官方 UI(纯 CLI),应用一致性要自己写 hook。另外 VMware vSphere 也有官方插件;社区插件覆盖 AlibabaCloud OSS、HuaweiCloud OBS、DigitalOcean、OpenStack、OpenEBS、Portworx 等,S3 兼容存储(腾讯云 COS、Ceph、NooBaa 等)统一走 AWS 插件接入。
- Kasten K10(Veeam):企业级 UI、多集群统一管理、应用一致性内置(蓝绿验证 Kanister)。被 Veeam 收购后路线图聚焦企业市场,费用不菲。
- Trilio:增量备份快,对 OpenStack 混合环境支持好;按节点计费,规模化后成本敏感。
- Stash:轻量、CRD 驱动,适合小团队;社区规模小,长期维护有不确定性。
还有一个不能漏的选项:云厂商原生方案(GKE Backup、AWS Backup for EKS、Azure Backup for AKS)。它们省事、集成深,但把你锁死在单一云上------与本文反复强调的可移植性诉求直接冲突。
选型决策建议(按场景给依据,不按工具给结论):
- 自建集群 / 多云 / 需要跨集群迁移 → Velero(移植性是唯一硬通货);
- 企业多集群需要统一管控台 + 预算充足 → Kasten K10(为 UI 和应用一致性付费);
- OpenStack 混合环境 → Trilio;
- 轻量小团队、卷数据为主 → Stash 或云原生方案;
- 已锁定单一云且规模不大 → 云厂商原生方案(接受锁定换取省事)。
八、总结与展望
回顾全文,五个核心判断:
- etcd 备份 ≠ Kubernetes 备份:etcd 快照管控制面 DR,Velero 管应用与数据的可选择性、可移植性保护,两者都要进 runbook;
- 对象存储是 source of truth:这个设计让备份可 GitOps 化、迁移只是"换个集群指向同一个桶",是 Velero 架构上最值得品味的一笔;
- 卷数据三选一看恢复目标:日常备份优先 CSI 快照,归档与跨集群用 fs-backup/Data Mover,不要凭"先进性"选路线;
- 一致性靠 Hook,有效性靠演练:数据库 freeze/dump 解决备份时点一致性,每月恢复演练解决"备份是否真的能用"------不测试的备份等于没有备份;
- GitOps + Velero 双保险:声明式资源进 Git,PV 数据进 BSL,覆盖彼此盲区。
展望方面,Velero 进入 CNCF Sandbox 后治理独立性显著增强;v1.18 的并发备份、Glob namespace 过滤和数据移动器缓存卷已经明显改善多租户体验,v1.18.3-rc.1 也已在路上(暂无 v1.19)。可以预期并发处理、可观测性(增量大小报告这类)会继续深化。如果你的团队还在用 cron + shell 脚本给 etcd 打快照,或者备份恢复从未演练过,现在就是动手的好时机------从一个 velero install 和一次恢复演练开始。
参考资料
- Velero 官网:https://velero.io/
- Velero v1.18 发布公告:https://velero.io/blog/Velero-1.18
- Velero 工作原理(官方文档 v1.18):https://velero.io/docs/v1.18/how-velero-works/
- 支持的存储提供商(官方文档 v1.18):https://velero.io/docs/v1.18/supported-providers/
- Velero GitHub 仓库(velero-io/velero):https://github.com/velero-io/velero