etcd 备份与恢复:别等集群挂了才想起来没备份

发布时间:2026/7/8 14:34:47
etcd 备份与恢复:别等集群挂了才想起来没备份 etcd 备份与恢复别等集群挂了才想起来没备份一、那个凌晨三点没人想接的电话见过凌晨三点的监控告警吗etcd 集群的 Leader 选举超时三个节点中两个磁盘损坏watch 事件全部中断。整个 Kubernetes 集群的调度器、控制器、API Server 集体停摆。第一反应是重启节点。没用磁盘坏了。第二反应是看看有没有备份。运维同事翻了一通后发现——最近的 etcd snapshot 是三个月前初始化集群时顺手做的。之后的 Helm Release 记录、Service 定义、ConfigMap、Secret 全都没了。重建一个中等规模的集群需要六小时。这还不包括验证每个微服务的配置是否完整。那次事故后团队立了一条铁律etcd 备份没配好的集群不准上线。基础设施不需要漂亮话。它需要你在凌晨三点不用接电话。二、etcd 备份策略的三种方案etcd 本质上是一个分布式的键值存储所有 Kubernetes 集群状态Pod、Service、ConfigMap、Secret都存在里面。备份方案有三种flowchart TD A[etcd 备份策略] -- B[方案一: 定时 Snapshot] A -- C[方案二: etcd Velero] A -- D[方案三: 外部 etcd 集群] B -- B1[etcdctl snapshot save] B -- B2[CronJob 定时执行] B -- B3[适合中小集群] C -- C1[备份到对象存储] C -- C2[支持增量备份] C -- C3[适合生产大规模集群] D -- D1[独立于 K8s 部署] D -- D2[专用备份方案] D -- D3[适合托管 etcd 场景]方案一定时 Snapshot。用etcdctl snapshot save命令创建全量快照通过 CronJob 定时执行。适合节点数少于 50 的中小集群。优势是简单直接缺点是不支持增量备份RPO数据恢复点目标取决于快照间隔。方案二Velero etcd。Velero 是 Kubernetes 原生的灾备工具支持将集群状态含 etcd 数据备份到 S3 兼容的对象存储。它支持增量备份和定时调度RPO 可以做到小时级别。方案三外部 etcd 集群。如果 etcd 是独立于 Kubernetes 部署的如使用云厂商托管服务备份策略由外部集群的运维方案决定。云厂商通常提供自动备份和跨区域复制。选择哪个方案取决于两个关键指标RPO能容忍丢多少数据和 RTO恢复需要多长时间。对于生产环境RPO 建议不超 1 小时RTO 建议在 30 分钟内。三、生产级 etcd 备份与恢复实现以下是一套完整的备份脚本和 Kubernetes CronJob 配置package etcdbackup import ( context fmt os os/exec path/filepath time clientv3 go.etcd.io/etcd/client/v3 ) // BackupConfig 备份配置 type BackupConfig struct { Endpoints []string // etcd 成员地址 CertFile string // TLS 客户端证书 KeyFile string // TLS 客户端私钥 CAFile string // CA 证书 BackupDir string // 本地备份目录 RetentionDays int // 备份保留天数 } // ETCDBackup 管理 etcd 快照备份 type ETCDBackup struct { config BackupConfig client *clientv3.Client } // NewETCDBackup 创建备份管理器 func NewETCDBackup(cfg BackupConfig) (*ETCDBackup, error) { client, err : clientv3.New(clientv3.Config{ Endpoints: cfg.Endpoints, DialTimeout: 5 * time.Second, }) if err ! nil { return nil, fmt.Errorf(connect to etcd: %w, err) } return ETCDBackup{config: cfg, client: client}, nil } // CreateSnapshot 创建 etcd 快照 func (b *ETCDBackup) CreateSnapshot(ctx context.Context) (string, error) { timestamp : time.Now().Format(20060102-150405) filename : fmt.Sprintf(etcd-snapshot-%s.db, timestamp) filepath : filepath.Join(b.config.BackupDir, filename) // 使用 etcdctl 创建快照传入 TLS 证书 args : []string{ snapshot, save, filepath, --endpoints, b.config.Endpoints[0], --cert, b.config.CertFile, --key, b.config.KeyFile, --cacert, b.config.CAFile, } cmd : exec.CommandContext(ctx, etcdctl, args...) output, err : cmd.CombinedOutput() if err ! nil { return , fmt.Errorf(create snapshot: %w, output: %s, err, string(output)) } // 验证快照完整性 if err : b.verifySnapshot(ctx, filepath); err ! nil { os.Remove(filepath) return , fmt.Errorf(snapshot verification failed: %w, err) } // 清理过期备份 if err : b.cleanup(); err ! nil { // cleanup 失败不阻断备份流程 fmt.Fprintf(os.Stderr, cleanup old backups: %v\n, err) } return filepath, nil } // verifySnapshot 验证快照文件完整性 func (b *ETCDBackup) verifySnapshot(ctx context.Context, filepath string) error { args : []string{ snapshot, status, filepath, --write-out, table, } cmd : exec.CommandContext(ctx, etcdctl, args...) output, err : cmd.CombinedOutput() if err ! nil { return fmt.Errorf(verify snapshot: %w, output: %s, err, string(output)) } fmt.Printf(Snapshot verified: %s\n, string(output)) return nil } // Restore 从快照恢复 func (b *ETCDBackup) Restore(ctx context.Context, snapshotPath string) error { // 恢复时写入独立目录避免覆盖当前数据 restoreDir : filepath.Join(b.config.BackupDir, restored-etcd) args : []string{ snapshot, restore, snapshotPath, --data-dir, restoreDir, --name, restored-member, --initial-cluster, restored-memberhttp://localhost:2380, --initial-advertise-peer-urls, http://localhost:2380, } cmd : exec.CommandContext(ctx, etcdctl, args...) output, err : cmd.CombinedOutput() if err ! nil { return fmt.Errorf(restore snapshot: %w, output: %s, err, string(output)) } fmt.Printf(Data restored to %s\n, restoreDir) return nil } // cleanup 删除超过保留期的备份文件 func (b *ETCDBackup) cleanup() error { cutoff : time.Now().AddDate(0, 0, -b.config.RetentionDays) entries, err : os.ReadDir(b.config.BackupDir) if err ! nil { return fmt.Errorf(read backup dir: %w, err) } for _, entry : range entries { info, err : entry.Info() if err ! nil { continue } if info.ModTime().Before(cutoff) { path : filepath.Join(b.config.BackupDir, entry.Name()) if err : os.Remove(path); err ! nil { return fmt.Errorf(remove %s: %w, path, err) } } } return nil } // GetClusterHealth 检查 etcd 集群健康状态作为备份前置检查 func (b *ETCDBackup) GetClusterHealth(ctx context.Context) error { for _, ep : range b.config.Endpoints { _, err : b.client.Status(ctx, ep) if err ! nil { return fmt.Errorf(endpoint %s unhealthy: %w, ep, err) } } return nil }对应的 Kubernetes CronJob 定义apiVersion: batch/v1 kind: CronJob metadata: name: etcd-backup namespace: kube-system spec: # 每小时执行一次备份 schedule: 0 * * * * # 保留最近 3 个成功的 Job successfulJobsHistoryLimit: 3 # 保留最近 1 个失败的 Job failedJobsHistoryLimit: 1 jobTemplate: spec: backoffLimit: 2 template: spec: containers: - name: backup image: etcd-backup:latest env: - name: ETCD_ENDPOINTS value: https://10.0.1.10:2379,https://10.0.1.11:2379,https://10.0.1.12:2379 - name: BACKUP_DIR value: /backups - name: RETENTION_DAYS value: 7 volumeMounts: - name: etcd-certs mountPath: /etc/etcd/certs readOnly: true - name: backup-storage mountPath: /backups volumes: - name: etcd-certs secret: secretName: etcd-client-certs - name: backup-storage persistentVolumeClaim: claimName: etcd-backup-pvc restartPolicy: OnFailure这套实现的关键点备份完成后立即验证快照完整性。用etcdctl snapshot status检查快照文件是否可读。如果验证失败立即删除无效文件并告警。备份前置健康检查。创建快照前先检查所有 etcd 成员的节点健康状态。任何一个成员不健康都中断备份并告警。自动清理过期备份。按RetentionDays配置删除老旧快照避免存储空间耗尽。四、恢复场景与验证流程场景一单节点数据损坏。从健康成员的最新快照恢复数据目录重启损坏节点etcd 通过 Raft 日志自动同步差异数据。场景二全集群数据丢失最严重。从最新快照恢复一个新集群配置--initial-cluster参数启动然后重新注册所有 Kubernetes 组件。场景三误删数据。如果只是误删了某些 key如一个 ConfigMap不需要全量恢复。可以从快照中用etcdctl get提取指定 key 的内容再etcdctl put回去。恢复后必须做的验证清单kubectl get nodes确认所有节点状态正常kubectl get pods -A确认核心组件 Pod 全部 Running检查 Secret 和 ConfigMap 数量是否匹配预期验证 Ingress 和 Service 的端点是否可达执行一次冒烟测试部署确认调度器正常工作备份文件安全问题。etcd 快照包含所有 Secret 和 ConfigMap 的明文数据。备份文件必须加密存储用 GPG 或 KMS传输过程用 TLS。存储备份的 PVC 需要独立的 RBAC 权限控制。备份策略的定期演练。备份文件存在不代表能恢复成功。建议每个季度做一次恢复演练在隔离的测试环境中验证恢复流程记录实际的 RTO 数据。五、总结etcd 备份是 Kubernetes 运维中最容易被忽略但后果最严重的一环。核心方案是定时全量快照 完整性验证 自动清理通过 CronJob 自动化执行。关键实践备份前进行集群健康检查备份后用snapshot status验证完整性快照文件加密存储恢复后执行 5 步验证清单每季度做恢复演练。RPO 建议不超过 1 小时存储保留 7 天以上。没有备份的集群就是一颗定时炸弹。