Kubernetes集群备份实战:用Velero构建可靠的namespace级容灾体系 刚接手那套Kubernetes集群时我最怕的不是集群扩容失败也不是某个Pod网络异常而是半夜有人在群里发一句“线上namespace好像被人删掉了”。那种紧张感经历过一次就再也不想经历第二次先翻一遍有没有人留着原始的YAML再看etcd快照是不是最新的最后还得评估如果直接回滚会不会把其他业务一起带崩。真正把“备份、恢复、灾难恢复”这件事重视起来是我把一套基于Velero的方案完整落地之后。这篇文章就把这套方案从头讲透为什么K8s备份不能只靠etcd快照、Velero的工作原理、部署步骤、一次真实的误删恢复演练以及我在实际环境中踩过的坑和最终沉淀下来的容灾策略。如果你是刚开始接触Kubernetes的运维或开发读完应该能直接照着把自己的集群备份体系搭起来。就算你已经用了Velero后面那部分“真实环境中的坑”也建议认真看一遍有些问题文档上真的不会写。1. 只备份etcd远远不够K8s备份到底在备份什么1.1 一次误删namespace后我经历了什么先说那次让我印象深刻的事故。当时某套测试环境里有人执行了一条kubectl delete namespace demo --now等确认命令的时候整个namespace下的Deployment、Service、ConfigMap、PVC全没了。最初的设想是“拉个etcd快照整体回滚”但是一评估就发现行不通etcd快照恢复是整库还原会把这期间所有其他namespace的变更也一起回滚掉。我们是测试环境还能勉强接受如果是生产环境这种“连带伤害”几乎不可控。后来发现更尴尬的是很多人觉得K8s里的资源都定义在Git仓库里了丢了直接用kubectl apply回来就行。但实际现场的ConfigMap、Secret、PVC里的文件、被临时创建出来的资源大概率没有完整沉淀在仓库里。真要从零重建一个namespace少则几小时多则半天。1.2 集群里的数据要分成三类来看待为了搞清楚备份策略我把一个K8s集群里需要保护的东西拆成了三类数据类型包含内容典型恢复方式恢复场景集群元数据ETCD内的Namespace、ClusterRole、CRD、节点注册信息等etcd快照整体还原控制面坏了、整个集群需要时间倒流应用资源对象Deployment、Service、ConfigMap、Secret、Ingress、PVC等资源清单重新apply单个namespace被误删、跨集群搬迁持久卷业务数据数据库文件、上传目录、中间件数据目录卷文件拷贝 / 存储快照业务数据丢失、卷意外删除很多教程只讲etcd备份结果遇到“误删namespace”这种最常见的故障时反而束手束脚因为etcd恢复的代价和影响范围都太大。Velero这类工具正好补上的是第二类和第三类把应用资源连同持久卷数据按namespace级别或者标签级别备份下来恢复的时候可以精确到具体业务范围。1.3 Velero的边界它能做什么不能做什么Velero能做的是把Kubernetes API里的资源对象打包上传到对象存储同时能把Pod挂载的持久卷里的文件一起备份恢复时可以选择整个备份、指定namespace甚至可以映射到另一个namespace。这套机制对“误删namespace”“跨集群迁移”“按业务恢复”这些场景非常管用。但有两件事它做不了我建议一开始就有清晰预期它不备份etcd原始数据。apiserver内部状态、etcd自身存储的底层元数据不归它管所以集群控制面彻底坏了还得靠etcd快照兜底。它不保证应用级数据一致性。用文件级备份去备份MySQL的数据目录恢复出来的文件可能处于“写到一半”的状态。数据库这类业务还是得配合mysqldump或其他应用自己的备份机制。2. Velero是怎么工作的从Backup到Restore的完整链路2.1 三个核心概念先分清使用Velero之前先把三个词弄明白BackupStorageLocation、VolumeSnapshotLocation、Backup/Restore任务。BackupStorageLocation备份存储位置是Velero存放备份产物的地方通常是一个S3兼容的对象存储桶。资源对象的JSON清单、卷文件的压缩包都会传到这里。它的配置是全局性的决定所有备份最终落在哪里。VolumeSnapshotLocation卷快照位置是快照类备份专用的配置记录云存储或者存储系统的快照区域。如果只用文件级备份这个可以完全不配置。Backup和Restore则是两个自定义资源对象。你创建一条Backup记录集群里的Velero控制器就会按照它的配置去执行归档创建一条Restore记录控制器就读取对应的备份内容并重建资源。这整套流程是用Kubernetes自己的CRD机制实现的所以备份任务本身也可以被监控、被管理。2.2 卷数据备份的两种方式快照和文件级Velero处理持久卷数据时主要有两条路线这也是很多新手最容易迷糊的地方。第一条是CSI快照。它调用存储后端的快照接口把整个卷快速生成一个快照。优点是一致性好、速度快尤其是云环境里几乎不影响业务。缺点是对存储有要求必须安装了CSI驱动而且需要提前定义VolumeSnapshotClass。如果你用的是自建的NFS、本地盘这类存储这条路线基本走不通。第二条是文件级备份。Velero会在集群每个节点上跑一个Agent组件恢复时它会直接读取Pod挂载的卷目录里的文件打包压缩后上传到对象存储。老版本用的是Restic新版本默认转向Kopia。它的好处是不挑存储类型本地盘、NFS、托管云盘都能备份坏处是性能一般备份大目录时比较耗时而且备份过程中如果应用还在写文件可能抓到“写到一半”的内容。我做了个对比选择时可以对着看对比项CSI快照文件级备份实现方式调用存储后端快照API遍历卷目录读取文件并打包数据一致性好由存储编排一般文件可能处于中间状态存储依赖CSI驱动和VolumeSnapshotClass基本不依赖通用性强备份性能快不占用节点带宽慢占用节点磁盘和网络IO跨集群迁移快照与存储绑定换环境要映射更通用适合跨平台迁移推荐场景云环境生产库、高写入量应用自建存储、通用备份、混和集群我在生产环境里的习惯是云环境核心应用尽量走CSI快照自建机房或者存储类型复杂的环境直接用文件级备份兜底。2.3 恢复资源时背后发生了什么恢复操作不是简单地把YAML重新apply一遍。Velero会先恢复PVC和PV处理存储类映射和命名空间映射再恢复Deployment、StatefulSet这类工作负载。恢复的PVC会和原备份里的PV重新绑定关联这个过程如果存储类名字在两个集群里不一致就会直接卡在Pending状态。所以跨集群恢复时有两个参数非常关键--namespace-mappings把备份里的namespace映射到目标集群的新namespace和--storage-class-mappings把备份里的StorageClass映射为目标集群已有的StorageClass。很多恢复失败案例最后排查下来都是这两个映射没配。3. 从零部署Velero并跑通第一次备份3.1 部署前需要准备什么有点需要先说清楚Velero由两部分组成一个客户端命令行velero CLI一个部署在集群里的服务端。客户端版本和服务端版本最好保持一致否则可能出现API兼容问题。部署前要准备四样东西一个能访问目标Kubernetes集群的kubeconfig权限最好有集群管理员级别。一个S3兼容的对象存储桶。公有云可以直接用云厂商的对象存储自建环境可以部署一个S3兼容服务。无论哪种都需要准备好访问密钥。集群能够拉取Velero相关的容器镜像实在不行就准备好镜像仓库代理。如果准备使用文件级备份还需要确认集群节点允许运行以hostPath方式挂载卷的DaemonSet这是Agent读取卷数据的必要前提。3.2 安装命令与参数解读我个人最常用的是命令行安装方式一条命令就能把服务端组件、插件、备份位置配置都搞定。以最常见的S3兼容对象存储为例velero install \ --provider aws \ --plugins velero/velero-plugin-for-aws:v1.2.0 \ --bucket my-cluster-backup \ --secret-file ./credentials-velero \ --backup-location-config regionus-east-1,s3ForcePathStyletrue,s3Urlhttp://storage.example.local:9000 \ --snapshot-location-config regionus-east-1 \ --use-restic \ --wait这里逐条说明一下我的考虑--provider aws不是说你必须用公有云而是因为S3协议的对象存储都通过AWS插件访问。自建存储服务同样适用。--secret-file指向一个包含对象存储访问密钥的文件内容格式通常是两行aws_access_key_id...和aws_secret_access_key...。--backup-location-config里s3ForcePathStyletrue是自建S3兼容服务的关键项表示使用路径风格访问桶s3Url则指向存储服务的实际地址。--use-restic启用文件级卷备份能力如果你确定环境支持CSI快照可以去掉这个参数并加上--use-volume-snapshotsfalse。安装完成后先检查一下服务端是否正常运行kubectl -n velero get pods velero backup-location get看到Phase为Available说明备份存储位置已经连通。这一步是最容易出问题的对象存储地址、密钥、region配置只要有一个不对后面所有备份都会卡住。3.3 跑通第一次备份部署完成后先用一个小范围备份验证整条链路。我习惯先建一个测试namespace部署一个带PVC的简单应用然后再做备份。假设测试namespace叫demo-backup第一次备份命令如下velero backup create first-backup \ --include-namespaces demo-backup \ --default-volumes-to-restic解释两个参数--include-namespaces指定只备份这个namespace避免把集群里其他业务也带进去--default-volumes-to-restic表示namespace下所有没有配置快照的卷都走文件级备份。执行之后用velero backup describe first-backup和velero backup logs first-backup查看状态。正常情况下Phase会从New变成InProgress最终变成Completed。如果变成Failed或者PartiallyFailed优先去看logs里的报错信息百分之八十都是对象存储访问问题。4. 完整演练模拟误删namespace并成功恢复4.1 准备一个带持久卷的示例应用纸上谈兵没意思我带你完整走一遍恢复过程。先创建一个demo namespace部署一个带PVC的Nginx应用往卷里写一个文件用来验证。apiVersion: v1 kind: Namespace metadata: name: demo-app --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: demo-data namespace: demo-app spec: accessModes: - ReadWriteOnce resources: requests: storage: 500Mi --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo namespace: demo-app spec: replicas: 1 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: securityContext: fsGroup: 1000 runAsUser: 1000 containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80 volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: demo-data这里特别提一下securityContext里的fsGroup。如果用文件级备份Agent去读取卷数据时如果没有合适的组权限会碰到Permission denied。提前在Pod里定义fsGroup是避免这类问题的常规做法。应用起来之后往卷里写个验证文件kubectl exec -n demo-app deploy/nginx-demo -- sh -c echo backup-ok /usr/share/nginx/html/test.txt kubectl exec -n demo-app deploy/nginx-demo -- cat /usr/share/nginx/html/test.txt4.2 执行一次完整备份现在对demo-app做一次完整备份包含资源对象和卷文件velero backup create demo-backup \ --include-namespaces demo-app \ --default-volumes-to-restic等待备份完成velero backup describe demo-backup看到Phase为CompletedItemsBackedUp里有资源数量Volumes里有卷的备份记录就可以放心进行下一步了。备份产物已经传到了对象存储桶里你甚至可以去桶里看目录结构里面包含每个资源的JSON清单和卷数据压缩包。4.3 模拟事故整个namespace被删除这一步就是模拟开头那场事故kubectl delete namespace demo-app --waitfalse执行完之后检查一下集群状态Deployment、PVC、Service全部消失卷里的文件自然也没了。下面开始恢复。4.4 用Velero执行恢复恢复命令很短velero restore create --from-backup demo-backup然后查看恢复进度velero restore describe demo-restore-时间戳恢复过程会比较快Phase变成Completed后检查一下资源是否回来了kubectl -n demo-app get deploy,pvc,pod kubectl exec -n demo-app deploy/nginx-demo -- cat /usr/share/nginx/html/test.txt如果一切正常你会看到PVC重新创建Pod重新调度卷里的test.txt内容依然是backup-ok。整个恢复大概只需要一两分钟。4.5 恢复后必须做的验收检查恢复成功不等于业务恢复这是我反复强调的一点。恢复完成后至少检查四件事PVC的实际容量、状态是否和备份前一致。应用的配置项ConfigMap、Secret是否完整。业务接口是否能够正常响应而不只是Pod处于Running。如果涉及数据库单独验证数据完整性不能只看文件存在。5. 真实环境中最容易踩的坑和容灾规划建议5.1 五个让我印象深刻的坑这套方案用了一年多踩过的坑不少挑五个有代表性的分享给你。第一个是备份存储位置不可达。对象存储的endpoint在集群内外网不通或者访问密钥失效备份任务会长时间卡在InProgress最后Failed。排查时先看Velero日志再看velero backup-location get的Phase。建议部署前先手工用对象存储客户端工具验证一下密钥和连通性。第二个是文件级备份导致Pod mount失败。有些环境里Agent组件要挂载宿主机的目录才能读取卷内容。如果Pod安全策略限制了hostPath或者节点上目录不存在就会导致备份执行失败。遇到这种情况要检查Agent组件的DaemonSet状态而不是只盯着Backup日志。第三个是恢复后PV一直Pending。最常见原因是目标集群里没有备份时的StorageClass名字或者StorageClass的默认值不一致。恢复命令加上--storage-class-mappings把旧的存储类名映射到目标集群已有的存储类PV就能正常创建。第四个是备份成功但恢复内容不全。有时候是备份命令里--include-resources写得不完整遗漏了某些CRD。尤其是自定义资源比如一些扩展组件创建的CRD对象如果不在备份范围内恢复出来业务就是缺胳膊少腿的。建议备份核心业务namespace时把expires和crd相关资源都纳入范围或者用--include-cluster-resources谨慎处理集群级资源。第五个是定时备份的cron表达式问题。写Schedule的时候很容易忽略时区。如果用默认时区配置凌晨两点的备份实际执行时间可能和预期差了好几个小时。建议用6字段的cron并在最后一个字段明确时区比如0 2 * * * Asia/Shanghai。5.2 如何让备份真正可靠定期演练和监控部署完Velero只是开始真正保证容灾能力的是定期的恢复演练。我现在的习惯是每季度做一次全流程演练把最近一次备份恢复到一套临时的K8s集群里模拟目标业务在新环境启动验证数据可用性。演练完直接清理临时集群成本不高但能提前暴露大量配置问题。备份的监控也必不可少。定时备份任务如果连续失败两天没人发现的话等真出事时查备份才发现上次可用备份是一周前这个场景太常见了。建议把velero backup describe的Phase检查做成定时任务Failed时直接告警。同时定期检查对象存储桶里备份目录的大小变化防止存储满导致备份写入失败。5.3 Velero和etcd快照怎么配合最后说一个容易被忽略的分工问题。Velero负责应用资源和卷数据的备份etcd快照负责K8s集群自身状态的整体恢复。两者不是替代关系而是互补关系。我自己的容灾体系是etcd快照每天一次保留最近七天用于集群控制面严重故障时的整体回滚Velero对核心业务namespace每天一次备份保留七天用于误删namespace、按业务恢复、跨集群迁移。数据库这类高一致性要求的业务再加一层业务侧备份。三层各自守住各自的责任边界出故障时才能快速判断该用哪层备份。我对备份这件事最大的体会是备份方案不是给“万一出事”准备的安心丸而是每次真实故障来临时最靠得住的那条退路。别再迷信Git仓库里的YAML能救命也别把etcd快照当成万能药。把Velero这类工具用熟让恢复演练变成和日常发布一样的固定节奏真到了误删命令已经敲下去的那一刻你会感谢当初认真配置备份的自己。