31 KiB
31 KiB
Kubernetes 生产级数据库备份架构方案
基于实战经验的企业级备份恢复完整方案
设计时间: 2026-01-03 适用场景: 生产环境数据库备份(MySQL, PostgreSQL, Redis) 核心原则: 强一致性优先 + 多层冗余 + 可验证恢复
📋 目录
架构设计原则
✅ 核心原则(不可妥协)
-
强一致性优先
- 备份必须 first-write 到 POSIX 兼容的持久化存储
- 禁止依赖"最终一致性"存储作为第一备份点
- 每次备份必须经过完整性校验(checksum)
-
多层冗余
- 第一层:本地 PVC(强一致性,快速恢复)
- 第二层:对象存储(异地容灾,成本优化)
- 第三层:冷存储/归档(合规性,长期保留)
-
可验证恢复
- 每次备份自动生成恢复测试脚本
- 定期执行恢复演练(至少每月一次)
- 恢复时间目标(RTO)< 30 分钟
- 恢复点目标(RPO)< 24 小时
-
失败可见
- 备份失败必须触发告警(不能静默失败)
- 日志必须包含完整的执行轨迹
- 监控指标必须包括备份成功率、大小趋势
三层备份架构
┌─────────────────────────────────────────────────────────────────┐
│ 数据库实例(MySQL/PostgreSQL) │
│ 运行在 K8s StatefulSet/Deployment │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ 第一层:本地 PVC 备份 │ ← 核心:强一致性保证
│ - ReadWriteMany PVC │
│ - 保留 7 天 │
│ - SHA256 校验 │
│ - 立即可用(RTO < 5min) │
└────────┬───────────────────┘
│
├─→ (异步复制) ────────────────────┐
│ │
▼ ▼
┌────────────────────────────┐ ┌────────────────────────────┐
│ 第二层:对象存储(S3/OSS) │ │ 第二层备选:NFS/NAS │
│ - 保留 30 天 │ │ - 企业 NAS 存储 │
│ - 异地容灾 │ │ - 保留 30 天 │
│ - 成本优化 │ │ - 局域网高速访问 │
└────────┬───────────────────┘ └───────────────────────────┘
│
├─→ (生命周期转换)
│
▼
┌────────────────────────────┐
│ 第三层:冷存储/归档 │
│ - 保留 365 天(或更长) │
│ - 合规性要求 │
│ - 极低成本 │
└────────────────────────────┘
架构说明
第一层:本地 PVC(核心层)
用途:
- 主备份存储,保证数据真实落盘
- 快速恢复的首选来源
技术要求:
- 存储类型:
local-path/hostPath/nfs-client(POSIX 兼容) - 访问模式:
ReadWriteMany(允许恢复 Pod 访问) - 容量规划: 数据库大小 × 7 天 × 1.5 倍压缩率
- 性能要求: 至少 100 MB/s 写入速度
保留策略:
# 每日全量备份,保留 7 天
# 超过 7 天的自动删除(或移动到第二层)
关键特性:
- ✅ POSIX 文件系统,支持原子写入
- ✅ 支持文件锁和 fsync
- ✅ Kubernetes 原生集成
- ✅ 本地磁盘性能,秒级访问
不适合的存储:
- ❌ MinIO 单节点(如本次故障案例)
- ❌ 对象存储直接挂载(goofys/s3fs)
- ❌ 网络文件系统(延迟 > 10ms)
第二层:对象存储(容灾层)
用途:
- 异地容灾备份
- 成本优化(存储单价更低)
- 跨区域数据分发
推荐服务商:
- 云厂商: 阿里云 OSS / 腾讯云 COS / AWS S3
- 自建: MinIO 多节点集群(至少 4 节点 + 纠删码)
- 备选: Ceph RGW / SeaweedFS
关键配置:
# 对象存储配置要求
versioning: false # 禁用版本控制(避免数据丢失)
lifecycle: 30days # 30 天后自动删除或转冷存储
replication: enabled # 跨区域复制(可选)
encryption: AES256 # 静态加密
上传策略:
# 异步上传(不阻塞备份主流程)
# 重试机制(失败自动重试 3 次)
# 带宽限制(避免影响业务)
第三层:冷存储(归档层)
用途:
- 长期合规性存储(如金融行业 7 年要求)
- 极低成本归档
技术选型:
- 云归档: 阿里云归档存储 / AWS Glacier
- 磁带库: LTO-9 磁带(企业级)
- 离线硬盘: 冷备硬盘异地存放
生命周期:
第二层对象存储(30 天)
↓ (自动转换)
冷存储(365 天)
↓ (到期删除/永久保留)
技术选型和对比
PVC 存储类选型
| 存储类型 | 一致性 | 性能 | 成本 | 推荐度 | 备注 |
|---|---|---|---|---|---|
| local-path | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 强烈推荐 | OrbStack/K3s 默认 |
| hostPath | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 推荐 | 需手动管理路径 |
| nfs-client | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ 推荐 | 需要 NFS 服务器 |
| Longhorn | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⚠️ 需测试 | 分布式存储 |
| Ceph RBD | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⚠️ 需测试 | 复杂度高 |
| MinIO (单节点) | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ 禁用 | 本次故障根因 |
对象存储选型
| 服务 | 可靠性 | 成本 | 延迟 | 推荐度 | 备注 |
|---|---|---|---|---|---|
| 阿里云 OSS | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ 强烈推荐 | 99.995% SLA |
| 腾讯云 COS | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ 强烈推荐 | 性价比高 |
| AWS S3 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ✅ 推荐 | 国际业务 |
| MinIO 集群 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 推荐 | 需 4+ 节点 |
| MinIO 单节点 | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ❌ 禁用 | 数据不可靠 |
实施方案
方案架构图
┌──────────────────────────────────────────────────────────────────┐
│ K8s Namespace: infra │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ MySQL StatefulSet │ │ PostgreSQL Helm │ │
│ │ - mysql-0 │ │ - postgres-primary │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ CronJob: database-backup │ │
│ │ Schedule: 0 2 * * * │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Container: │ │ Container: │ │
│ │ db-backup │ │ upload-worker │ │
│ │ │ │ (异步上传) │ │
│ │ 1. 执行 dump │ │ 1. 等待备份完成 │ │
│ │ 2. 写入 PVC │ │ 2. SHA256校验 │ │
│ │ 3. SHA256 │ │ 3. 上传对象存储 │ │
│ │ 4. 通知上传容器 │ │ 4. 验证上传 │ │
│ └────────┬─────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ PVC: database-backups │ │
│ │ - StorageClass: local-path │ │
│ │ - Size: 100Gi │ │
│ │ - AccessMode: ReadWriteMany │ │
│ │ │ │
│ │ /backups/ │ │
│ │ ├── mysql/ │ │
│ │ │ ├── 2026-01-03.sql.gz │ │
│ │ │ ├── 2026-01-03.sha256 │ │
│ │ │ └── ... │ │
│ │ ├── postgres/ │ │
│ │ └── metadata/ │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│
│ (异步上传)
▼
┌──────────────────────────┐
│ 对象存储(可选) │
│ - 阿里云 OSS / COS │
│ - 或其他 S3 兼容存储 │
└──────────────────────────┘
核心组件
1. PVC 持久化卷
# database-backup-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-backups
namespace: infra
labels:
app: database-backup
component: storage
spec:
accessModes:
- ReadWriteMany # 允许多个 Pod 访问(恢复时需要)
storageClassName: local-path # 或 nfs-client / hostpath
resources:
requests:
storage: 100Gi # 根据数据库大小调整
容量规划:
数据库大小: 10 GB
压缩率: 1:5 (gzip) = 2 GB
保留天数: 7 天
所需容量: 2 GB × 7 × 1.5 (冗余) = 21 GB
推荐配置: 50-100 GB(留有充足空间)
2. 备份脚本 ConfigMap
# backup-scripts-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: backup-scripts
namespace: infra
data:
backup-mysql.sh: |
#!/bin/bash
set -euo pipefail
# 配置
BACKUP_DIR="/backups/mysql"
BACKUP_DATE=$(date +%Y-%m-%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/mysql-backup-${BACKUP_DATE}.sql.gz"
CHECKSUM_FILE="${BACKUP_FILE}.sha256"
METADATA_FILE="${BACKUP_DIR}/metadata/${BACKUP_DATE}.json"
echo "========================================="
echo "MySQL Backup - Production Grade"
echo "========================================="
echo "Started at: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# 创建目录
mkdir -p "${BACKUP_DIR}" "${BACKUP_DIR}/metadata"
# [1/5] 连接测试
echo "[1/5] Testing MySQL connection..."
if mysql -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" -u root -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" &>/dev/null; then
echo " ✓ Connection successful"
else
echo " ✗ Connection failed"
exit 1
fi
# [2/5] 获取数据库列表
echo ""
echo "[2/5] Getting database list..."
DB_LIST=$(mysql -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" -u root -p"${MYSQL_ROOT_PASSWORD}" \
-e "SHOW DATABASES;" | grep -Ev "^(Database|information_schema|performance_schema|mysql|sys)$")
DB_COUNT=$(echo "$DB_LIST" | wc -l)
echo " ✓ Found ${DB_COUNT} databases to backup"
# [3/5] 执行备份
echo ""
echo "[3/5] Creating backup..."
START_TIME=$(date +%s)
mysqldump \
-h "${MYSQL_HOST}" \
-P "${MYSQL_PORT}" \
-u root \
-p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction \
--quick \
--lock-tables=false \
--all-databases \
--triggers \
--routines \
--events \
--hex-blob \
2>&1 | gzip -9 > "${BACKUP_FILE}"
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
echo " ✓ Backup created: ${BACKUP_SIZE}"
echo " ✓ Duration: ${DURATION}s"
# [4/5] 校验和
echo ""
echo "[4/5] Generating checksum..."
CHECKSUM=$(sha256sum "${BACKUP_FILE}" | cut -d' ' -f1)
echo "${CHECKSUM} $(basename ${BACKUP_FILE})" > "${CHECKSUM_FILE}"
echo " ✓ SHA256: ${CHECKSUM:0:16}..."
# [5/5] 元数据
echo ""
echo "[5/5] Writing metadata..."
cat > "${METADATA_FILE}" <<EOF
{
"backup_date": "${BACKUP_DATE}",
"backup_file": "$(basename ${BACKUP_FILE})",
"backup_size_bytes": $(stat -c%s "${BACKUP_FILE}"),
"backup_size_human": "${BACKUP_SIZE}",
"database_count": ${DB_COUNT},
"duration_seconds": ${DURATION},
"sha256": "${CHECKSUM}",
"mysql_version": "$(mysql -h ${MYSQL_HOST} -P ${MYSQL_PORT} -u root -p${MYSQL_ROOT_PASSWORD} -e 'SELECT VERSION()' -s -N)",
"hostname": "${MYSQL_HOST}",
"status": "success"
}
EOF
echo " ✓ Metadata saved"
# 标记完成(供上传容器使用)
touch "${BACKUP_DIR}/.backup-${BACKUP_DATE}.ready"
echo ""
echo "========================================="
echo "✅ Backup completed successfully"
echo "========================================="
echo "File: ${BACKUP_FILE}"
echo "Size: ${BACKUP_SIZE}"
echo "Checksum: ${CHECKSUM:0:16}..."
echo "Finished at: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="
backup-postgres.sh: |
#!/bin/bash
set -euo pipefail
# 配置
export PATH=$PATH:/opt/bitnami/postgresql/bin
BACKUP_DIR="/backups/postgres"
BACKUP_DATE=$(date +%Y-%m-%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/postgres-backup-${BACKUP_DATE}.sql.gz"
CHECKSUM_FILE="${BACKUP_FILE}.sha256"
METADATA_FILE="${BACKUP_DIR}/metadata/${BACKUP_DATE}.json"
echo "========================================="
echo "PostgreSQL Backup - Production Grade"
echo "========================================="
echo "Started at: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# 创建目录
mkdir -p "${BACKUP_DIR}" "${BACKUP_DIR}/metadata"
# [1/5] 连接测试
echo "[1/5] Testing PostgreSQL connection..."
export PGPASSWORD="${POSTGRES_PASSWORD}"
if psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -c "SELECT 1" &>/dev/null; then
echo " ✓ Connection successful"
else
echo " ✗ Connection failed"
exit 1
fi
# [2/5] 获取数据库列表
echo ""
echo "[2/5] Getting database list..."
DB_LIST=$(psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" \
-t -c "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres';" | tr -d ' ')
DB_COUNT=$(echo "$DB_LIST" | grep -v '^$' | wc -l)
echo " ✓ Found ${DB_COUNT} databases to backup"
# [3/5] 执行备份
echo ""
echo "[3/5] Creating backup..."
START_TIME=$(date +%s)
pg_dumpall \
-h "${POSTGRES_HOST}" \
-p "${POSTGRES_PORT}" \
-U "${POSTGRES_USER}" \
--clean \
--if-exists \
2>&1 | gzip -9 > "${BACKUP_FILE}"
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
echo " ✓ Backup created: ${BACKUP_SIZE}"
echo " ✓ Duration: ${DURATION}s"
# [4/5] 校验和
echo ""
echo "[4/5] Generating checksum..."
CHECKSUM=$(sha256sum "${BACKUP_FILE}" | cut -d' ' -f1)
echo "${CHECKSUM} $(basename ${BACKUP_FILE})" > "${CHECKSUM_FILE}"
echo " ✓ SHA256: ${CHECKSUM:0:16}..."
# [5/5] 元数据
echo ""
echo "[5/5] Writing metadata..."
PG_VERSION=$(psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -t -c "SELECT version()" | tr -d ' ')
cat > "${METADATA_FILE}" <<EOF
{
"backup_date": "${BACKUP_DATE}",
"backup_file": "$(basename ${BACKUP_FILE})",
"backup_size_bytes": $(stat -c%s "${BACKUP_FILE}"),
"backup_size_human": "${BACKUP_SIZE}",
"database_count": ${DB_COUNT},
"duration_seconds": ${DURATION},
"sha256": "${CHECKSUM}",
"postgres_version": "${PG_VERSION}",
"hostname": "${POSTGRES_HOST}",
"status": "success"
}
EOF
echo " ✓ Metadata saved"
# 标记完成
touch "${BACKUP_DIR}/.backup-${BACKUP_DATE}.ready"
echo ""
echo "========================================="
echo "✅ Backup completed successfully"
echo "========================================="
echo "File: ${BACKUP_FILE}"
echo "Size: ${BACKUP_SIZE}"
echo "Checksum: ${CHECKSUM:0:16}..."
echo "Finished at: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="
cleanup-old-backups.sh: |
#!/bin/bash
set -euo pipefail
RETENTION_DAYS=${RETENTION_DAYS:-7}
BACKUP_BASE="/backups"
echo "========================================="
echo "Cleanup Old Backups"
echo "Retention: ${RETENTION_DAYS} days"
echo "========================================="
for DB_TYPE in mysql postgres; do
BACKUP_DIR="${BACKUP_BASE}/${DB_TYPE}"
if [ ! -d "${BACKUP_DIR}" ]; then
echo "Skipping ${DB_TYPE} (directory not found)"
continue
fi
echo ""
echo "Cleaning ${DB_TYPE} backups..."
# 删除超过保留期的备份文件
DELETED=$(find "${BACKUP_DIR}" -name "*.sql.gz" -type f -mtime +${RETENTION_DAYS} -delete -print | wc -l)
# 删除对应的校验和文件
find "${BACKUP_DIR}" -name "*.sha256" -type f -mtime +${RETENTION_DAYS} -delete
# 删除元数据
find "${BACKUP_DIR}/metadata" -name "*.json" -type f -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true
# 删除标记文件
find "${BACKUP_DIR}" -name ".backup-*.ready" -type f -mtime +${RETENTION_DAYS} -delete 2>/dev/null || true
echo " ✓ Deleted ${DELETED} old backups"
# 显示当前备份
CURRENT=$(find "${BACKUP_DIR}" -name "*.sql.gz" -type f | wc -l)
echo " ✓ Current backups: ${CURRENT}"
done
echo ""
echo "========================================="
echo "✅ Cleanup completed"
echo "========================================="
3. CronJob 备份任务
# database-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
namespace: infra
labels:
app: database-backup
component: cronjob
spec:
schedule: "0 2 * * *" # 每天 02:00
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid # 禁止并发执行
jobTemplate:
metadata:
labels:
app: database-backup
component: job
spec:
ttlSecondsAfterFinished: 86400 # 24小时后清理
backoffLimit: 2
template:
metadata:
labels:
app: database-backup
component: pod
spec:
restartPolicy: OnFailure
serviceAccountName: database-backup
# Init Container: 清理旧备份
initContainers:
- name: cleanup-old-backups
image: busybox:latest
command: ["/bin/sh", "-c"]
args:
- |
/scripts/cleanup-old-backups.sh
env:
- name: RETENTION_DAYS
value: "7"
volumeMounts:
- name: backup-storage
mountPath: /backups
- name: scripts
mountPath: /scripts
containers:
# MySQL 备份
- name: backup-mysql
image: mysql:latest
command: ["/bin/bash", "/scripts/backup-mysql.sh"]
env:
- name: MYSQL_HOST
value: "mysql-infra.infra.svc.cluster.local"
- name: MYSQL_PORT
value: "3306"
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-credentials
key: root-password
volumeMounts:
- name: backup-storage
mountPath: /backups
- name: scripts
mountPath: /scripts
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 2000m
memory: 1Gi
# PostgreSQL 备份
- name: backup-postgres
image: harbor.eu.lingjingwanxiang.cn/library/bitnami-postgresql:bitnami
command: ["/bin/bash", "/scripts/backup-postgres.sh"]
env:
- name: POSTGRES_HOST
value: "postgres-infra-postgresql.infra.svc.cluster.local"
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
volumeMounts:
- name: backup-storage
mountPath: /backups
- name: scripts
mountPath: /scripts
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 2000m
memory: 1Gi
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: database-backups
- name: scripts
configMap:
name: backup-scripts
defaultMode: 0755
恢复演练流程
1. 快速恢复(从 PVC)
# quick-restore-mysql.sh
#!/bin/bash
set -euo pipefail
echo "=== MySQL Quick Restore from PVC ==="
# 选择备份文件
BACKUP_DIR="/backups/mysql"
echo "Available backups:"
ls -lh ${BACKUP_DIR}/*.sql.gz | tail -10
read -p "Enter backup filename: " BACKUP_FILENAME
BACKUP_FILE="${BACKUP_DIR}/${BACKUP_FILENAME}"
# 校验
echo "Verifying checksum..."
cd "${BACKUP_DIR}"
sha256sum -c "${BACKUP_FILENAME}.sha256"
# 恢复
echo "Restoring database..."
gunzip -c "${BACKUP_FILE}" | mysql -h mysql-infra -u root -p
echo "✅ Restore completed"
2. 定期演练(自动化)
# backup-drill-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-drill
namespace: infra
spec:
schedule: "0 4 1 * *" # 每月 1 号 04:00
jobTemplate:
spec:
template:
spec:
containers:
- name: drill
image: mysql:latest
command: ["/bin/bash"]
args:
- -c
- |
# 1. 获取最新备份
LATEST_BACKUP=$(ls -t /backups/mysql/*.sql.gz | head -1)
# 2. 校验完整性
cd /backups/mysql
sha256sum -c $(basename ${LATEST_BACKUP}).sha256
# 3. 测试恢复(到临时数据库)
mysql -h mysql-test -e "CREATE DATABASE restore_test_$(date +%s)"
gunzip -c ${LATEST_BACKUP} | mysql -h mysql-test restore_test_*
# 4. 验证数据
mysql -h mysql-test restore_test_* -e "SHOW TABLES"
# 5. 清理
mysql -h mysql-test -e "DROP DATABASE restore_test_*"
echo "✅ Backup drill passed"
volumeMounts:
- name: backup-storage
mountPath: /backups
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: database-backups
监控和告警
Prometheus 监控指标
# backup-monitoring-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: database-backup-metrics
namespace: observability
spec:
namespaceSelector:
matchNames:
- infra
selector:
matchLabels:
app: database-backup
endpoints:
- port: metrics
interval: 30s
关键指标
# 备份成功率(最近 7 天)
sum(kube_job_status_succeeded{job_name=~"database-backup.*"})
/
sum(kube_job_status_succeeded{job_name=~"database-backup.*"} + kube_job_status_failed{job_name=~"database-backup.*"})
# 最近备份时间(小时)
(time() - max(kube_job_status_completion_time{job_name=~"database-backup.*"})) / 3600
# 备份大小趋势(MB)
sum(node_filesystem_size_bytes{mountpoint="/backups"} - node_filesystem_avail_bytes{mountpoint="/backups"}) / 1024 / 1024
告警规则
# backup-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: database-backup-alerts
namespace: observability
spec:
groups:
- name: database-backup
interval: 30s
rules:
# 备份失败告警
- alert: DatabaseBackupFailed
expr: |
kube_job_status_failed{job_name=~"database-backup.*"} > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Database backup job failed"
description: "Backup job {{ $labels.job_name }} failed in namespace {{ $labels.namespace }}"
# 备份超时告警
- alert: DatabaseBackupTimeout
expr: |
(time() - max(kube_job_status_completion_time{job_name=~"database-backup.*"})) > 86400 * 2
for: 1h
labels:
severity: warning
annotations:
summary: "No successful backup in 48 hours"
description: "Last successful backup was more than 48 hours ago"
# 存储空间告警
- alert: BackupStorageAlmostFull
expr: |
(node_filesystem_avail_bytes{mountpoint="/backups"} / node_filesystem_size_bytes{mountpoint="/backups"}) < 0.2
for: 30m
labels:
severity: warning
annotations:
summary: "Backup storage almost full"
description: "Backup storage has less than 20% free space"
故障案例分析
案例:MinIO 单节点数据丢失
时间: 2026-01-03 影响: PostgreSQL/MySQL 备份虚假成功,实际数据未持久化
故障现象:
✓ Upload successful (mc cp 返回成功)
✗ Object does not exist (mc stat 查询失败)
✗ 文件系统无 .sql 文件
根因:
- MinIO 单节点 + 本地 PVC
- Pod 重启 9 次导致数据不一致
- Versioning + Lifecycle 组合触发已知 bug
- Metadata 写入成功,Data block 未落盘
影响范围:
- PostgreSQL 备份: 15 小时无有效备份
- MySQL 备份: 24 小时无有效备份
解决方案:
- 立即禁用 Lifecycle 规则
- 暂停 Versioning
- 重启 MinIO 清除异常状态
- 验证问题仍存在 → 判定 MinIO 不可靠
- 迁移到 PVC 本地备份架构
经验教训:
- ❌ 对象存储不适合作为第一备份点
- ❌ MinIO 单节点不具备生产级可靠性
- ✅ 备份必须 first-write 到 POSIX 文件系统
- ✅ 必须实施恢复演练验证备份可用性
实施检查清单
部署前检查
- PVC 存储类型已选择(推荐 local-path / nfs-client)
- PVC 容量已规划(数据库大小 × 7 × 1.5)
- 数据库凭据已配置到 Secret
- 备份脚本已部署到 ConfigMap
- CronJob 调度时间已确认(避开业务高峰)
- ServiceAccount 权限已配置
部署后验证
- 手动触发备份测试:
kubectl create job --from=cronjob/database-backup test-backup -n infra - 检查 PVC 中备份文件存在
- 验证 SHA256 校验和正确
- 验证 metadata.json 生成
- 执行快速恢复测试
- 配置 Prometheus 告警
- 确认告警接收人
持续运维
- 每周检查备份成功率
- 每月执行恢复演练
- 每季度审查保留策略
- 每年测试灾难恢复流程
总结
核心要点
- 强一致性优先: PVC 本地备份是第一选择
- 多层冗余: 本地 + 对象存储 + 冷存储
- 可验证恢复: 定期演练,确保 RTO/RPO 达标
- 监控告警: 备份失败必须可见
不推荐做法
- ❌ 直接依赖对象存储作为唯一备份
- ❌ 使用 MinIO 单节点生产备份
- ❌ 不验证备份可恢复性
- ❌ 备份失败静默忽略
推荐实践
- ✅ PVC 本地备份 + SHA256 校验
- ✅ 异步上传到可靠对象存储
- ✅ 每月恢复演练
- ✅ Prometheus 监控 + 告警
文档版本: 1.0 最后更新: 2026-01-03 维护者: DevOps Team