Files
ljwx-k8s/K8S-BACKUP-ARCHITECTURE.md
2026-02-20 22:50:45 +08:00

31 KiB
Raw Permalink Blame History

Kubernetes 生产级数据库备份架构方案

基于实战经验的企业级备份恢复完整方案

设计时间: 2026-01-03 适用场景: 生产环境数据库备份(MySQL, PostgreSQL, Redis) 核心原则: 强一致性优先 + 多层冗余 + 可验证恢复


📋 目录

  1. 架构设计原则
  2. 三层备份架构
  3. 技术选型和对比
  4. 实施方案
  5. 恢复演练流程
  6. 监控和告警
  7. 故障案例分析

架构设计原则

✅ 核心原则(不可妥协)

  1. 强一致性优先

    • 备份必须 first-write 到 POSIX 兼容的持久化存储
    • 禁止依赖"最终一致性"存储作为第一备份点
    • 每次备份必须经过完整性校验(checksum)
  2. 多层冗余

    • 第一层:本地 PVC(强一致性,快速恢复)
    • 第二层:对象存储(异地容灾,成本优化)
    • 第三层:冷存储/归档(合规性,长期保留)
  3. 可验证恢复

    • 每次备份自动生成恢复测试脚本
    • 定期执行恢复演练(至少每月一次)
    • 恢复时间目标(RTO)< 30 分钟
    • 恢复点目标(RPO)< 24 小时
  4. 失败可见

    • 备份失败必须触发告警(不能静默失败)
    • 日志必须包含完整的执行轨迹
    • 监控指标必须包括备份成功率、大小趋势

三层备份架构

┌─────────────────────────────────────────────────────────────────┐
│                     数据库实例(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 文件

根因:

  1. MinIO 单节点 + 本地 PVC
  2. Pod 重启 9 次导致数据不一致
  3. Versioning + Lifecycle 组合触发已知 bug
  4. Metadata 写入成功,Data block 未落盘

影响范围:

  • PostgreSQL 备份: 15 小时无有效备份
  • MySQL 备份: 24 小时无有效备份

解决方案:

  1. 立即禁用 Lifecycle 规则
  2. 暂停 Versioning
  3. 重启 MinIO 清除异常状态
  4. 验证问题仍存在 → 判定 MinIO 不可靠
  5. 迁移到 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 告警
  • 确认告警接收人

持续运维

  • 每周检查备份成功率
  • 每月执行恢复演练
  • 每季度审查保留策略
  • 每年测试灾难恢复流程

总结

核心要点

  1. 强一致性优先: PVC 本地备份是第一选择
  2. 多层冗余: 本地 + 对象存储 + 冷存储
  3. 可验证恢复: 定期演练,确保 RTO/RPO 达标
  4. 监控告警: 备份失败必须可见

不推荐做法

  • ❌ 直接依赖对象存储作为唯一备份
  • ❌ 使用 MinIO 单节点生产备份
  • ❌ 不验证备份可恢复性
  • ❌ 备份失败静默忽略

推荐实践

  • ✅ PVC 本地备份 + SHA256 校验
  • ✅ 异步上传到可靠对象存储
  • ✅ 每月恢复演练
  • ✅ Prometheus 监控 + 告警

文档版本: 1.0 最后更新: 2026-01-03 维护者: DevOps Team