没有经过验证的备份,等于没有备份。——运维第一定律


一、备份基础:先搞清楚"救什么"和"救多快"

在讨论工具和脚本之前,先回答两个灵魂问题:

  • 数据丢了,能丢多少? → RPO(Recovery Point Objective,恢复点目标)

  • 系统挂了,多久必须恢复? → RTO(Recovery Time Objective,恢复时间目标)

业务类型

典型 RPO

典型 RTO

说明

核心交易系统

0(零丢失)

< 15 分钟

需要同步复制 + 热备

内部管理系统

≤ 1 小时

< 4 小时

增量备份 + 温备

开发测试环境

≤ 24 小时

< 24 小时

每日全量备份即可

日志归档

≤ 1 周

< 1 周

异步归档到对象存储

3-2-1 备份原则

这是业界公认的黄金法则:

3 份数据副本
├── 1 份:生产环境原始数据
├── 1 份:本地备份(快速恢复)
└── 1 份:异地/云端备份(灾难恢复)

2 种不同存储介质
└── 例如:本地磁盘 + 对象存储

1 份异地存放
└── 至少一份备份与生产环境物理隔离

3-2-1-1-0 升级版(现代实践):

  • 最后一个 1:1 份离线/不可变备份(防勒索软件)

  • 最后一个 0:0 个未验证的备份(所有备份必须定期恢复测试)

备份类型速览

全量备份(Full)
├── 备份所有数据
├── 恢复最快,空间最大
└── 适合:周度基线备份

增量备份(Incremental)
├── 只备份上次备份后变化的数据
├── 备份最快,恢复需要完整链
└── 适合:每日/每小时备份

差异备份(Differential)
├── 备份上次全量备份后所有变化
├── 备份适中,恢复只需全量 + 最新差异
└── 适合:折中方案

经典备份轮转策略(GFS)

Grandfather-Father-Son 策略
├── Son:每日增量/差异,保留 7 天
├── Father:每周全量,保留 4 周
└── Grandfather:每月全量,保留 12 个月

二、文件系统备份:rsync / tar / restic

2.1 rsync:增量同步的瑞士军刀

# 基础用法:同步目录到备份服务器
rsync -avz --delete \
  --exclude='*.tmp' \
  --exclude='.cache/' \
  /data/app/ backup-server:/backup/app/

# 带宽限速(避免打满生产带宽)
rsync -avz --bwlimit=50000 /data/ backup-server:/backup/

# 指定 SSH 端口和密钥
rsync -avz -e "ssh -p 2222 -i /root/.ssh/backup_key" \
  /data/ backup-server:/backup/

# 断点续传大文件
rsync -avz --partial --progress \
  /data/bigfile.tar.gz backup-server:/backup/

自动化 rsync 备份脚本

#!/bin/bash
# /opt/scripts/rsync_backup.sh
# 每日增量备份 + 日志

set -euo pipefail

SRC="/data/"
DEST="backup-server:/backup/$(hostname)/"
LOG="/var/log/backup/rsync_$(date +%Y%m%d).log"
MAX_LOG_DAYS=30

exec > >(tee -a "$LOG") 2>&1

echo "===== 备份开始: $(date) ====="

# 增量快照(利用硬链接节省空间)
SNAPSHOT_DIR="/backup/snapshots/$(hostname)"
LATEST="$SNAPSHOT_DIR/latest"
CURRENT="$SNAPSHOT_DIR/$(date +%Y%m%d)"

mkdir -p "$SNAPSHOT_DIR"

LINK_OPT=""
if [ -d "$LATEST" ]; then
    LINK_OPT="--link-dest=$LATEST"
fi

rsync -avz --delete \
  --exclude='*.tmp' --exclude='.cache/' --exclude='node_modules/' \
  $LINK_OPT \
  "$SRC" "$CURRENT"

# 更新 latest 链接
rm -f "$LATEST"
ln -s "$CURRENT" "$LATEST"

# 同步到远程
rsync -avz "$CURRENT/" "$DEST"

echo "===== 备份完成: $(date) ====="

# 清理旧日志
find /var/log/backup/ -name "rsync_*.log" -mtime +$MAX_LOG_DAYS -delete

2.2 tar:传统打包备份

# 全量打包 + gzip 压缩
tar -czf /backup/full_$(date +%Y%m%d).tar.gz \
  --exclude='*.log' --exclude='.cache' \
  /data/

# 增量备份(基于 snar 快照文件)
# 第一次全量
tar --listed-incremental=/backup/snapshot.snar \
    -czf /backup/full_$(date +%Y%m%d).tar.gz /data/

# 后续增量(自动记录变化)
tar --listed-incremental=/backup/snapshot.snar \
    -czf /backup/incr_$(date +%Y%m%d_%H%M).tar.gz /data/

# 恢复:先恢复全量,再按顺序恢复增量
tar -xzf /backup/full_20260601.tar.gz -C /
tar -xzf /backup/incr_20260602_0200.tar.gz -C /
tar -xzf /backup/incr_20260603_0200.tar.gz -C /

# 验证备份完整性(不解压)
tar -tzf /backup/full_20260601.tar.gz > /dev/null && echo "✅ 备份文件完整" || echo "❌ 备份文件损坏"

2.3 restic:现代加密去重备份(强烈推荐)

restic 是新一代备份工具,支持加密、去重、多后端,非常适合企业级场景。

# 安装(Debian/Ubuntu)
apt install restic
# 或二进制安装
wget https://github.com/restic/restic/releases/download/v0.16.5/restic_0.16.5_linux_amd64.bz2
bzip2 -d restic_*.bz2 && chmod +x restic && mv restic /usr/local/bin/

# 初始化仓库(支持本地、SFTP、S3、B2、Azure、GCS)
restic -r /backup/restic-repo init
restic -r sftp:backup-server:/backup/restic init
restic -r s3:s3.amazonaws.com/my-backup-bucket init
restic -r s3:http://minio.local:9000/backup-bucket init

# 设置密码(首次使用会提示,也可以用文件)
export RESTIC_PASSWORD_FILE="/etc/restic/password"
echo "YourStrongPassword123!" > /etc/restic/password
chmod 600 /etc/restic/password

# 备份
restic -r /backup/restic-repo backup /data \
  --exclude='*.tmp' \
  --exclude='node_modules' \
  --tag "daily" \
  --verbose

# 查看快照列表
restic -r /backup/restic-repo snapshots
restic -r /backup/restic-repo snapshots --tag daily

# 恢复
restic -r /backup/restic-repo restore latest --target /restore/

# 恢复特定文件
restic -r /backup/restic-repo restore latest \
  --target /restore/ --include "/data/config/"

# 保留策略(自动清理旧快照)
restic -r /backup/restic-repo forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 12 \
  --keep-yearly 3 \
  --prune

# 检查仓库完整性
restic -r /backup/restic-repo check
restic -r /backup/restic-repo check --read-data  # 校验所有数据块

restic 自动化脚本(S3 后端)

#!/bin/bash
# /opt/scripts/restic_backup.sh
set -euo pipefail

export AWS_ACCESS_KEY_ID="AKIAXXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export RESTIC_REPOSITORY="s3:http://minio.local:9000/backups"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
export RESTIC_COMPRESSION="auto"

LOG="/var/log/backup/restic_$(date +%Y%m%d).log"
exec > >(tee -a "$LOG") 2>&1

echo "===== Restic 备份开始: $(date) ====="

# 备份关键目录
restic backup /data /etc /opt/scripts \
  --exclude='*.tmp' --exclude='node_modules' \
  --tag "$(hostname)" --tag "scheduled"

# 清理旧快照
restic forget \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --keep-yearly 2 \
  --prune

# 完整性检查(每周日执行)
if [ "$(date +%u)" -eq 7 ]; then
    echo "执行周度完整性检查..."
    restic check --read-data-subset=5%
fi

echo "===== 备份完成: $(date) ====="

三、数据库备份:MySQL / PostgreSQL / Redis

3.1 MySQL 备份

mysqldump(逻辑备份)

# 单库备份
mysqldump -u root -p --single-transaction --routines --triggers \
  --set-gtid-purged=OFF mydb > /backup/mysql/mydb_$(date +%Y%m%d).sql

# 全库备份(带压缩)
mysqldump -u root -p --all-databases --single-transaction \
  --routines --triggers --events \
  | gzip > /backup/mysql/all_$(date +%Y%m%d).sql.gz

# 恢复
mysql -u root -p mydb < /backup/mysql/mydb_20260610.sql
gunzip < /backup/mysql/all_20260610.sql.gz | mysql -u root -p

xtrabackup(物理备份,推荐生产环境)

# 安装
apt install percona-xtrabackup-80

# 全量备份
xtrabackup --backup --target-dir=/backup/xtrabackup/full \
  --user=backup --password='BackupPass123!'

# 增量备份
xtrabackup --backup --target-dir=/backup/xtrabackup/incr1 \
  --incremental-basedir=/backup/xtrabackup/full \
  --user=backup --password='BackupPass123!'

# 恢复流程
# 1. 准备全量
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtrabackup/full
# 2. 应用增量
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtrabackup/full \
  --incremental-dir=/backup/xtrabackup/incr1
# 3. 最终准备
xtrabackup --prepare --target-dir=/backup/xtrabackup/full
# 4. 恢复
systemctl stop mysqld
rm -rf /var/lib/mysql/*
xtrabackup --copy-back --target-dir=/backup/xtrabackup/full
chown -R mysql:mysql /var/lib/mysql
systemctl start mysqld

MySQL 自动化备份脚本

#!/bin/bash
# /opt/scripts/mysql_backup.sh
set -euo pipefail

BACKUP_DIR="/backup/mysql"
RETENTION_DAYS=30
MYSQL_USER="backup"
MYSQL_PASS_FILE="/etc/mysql/backup.cnf"
LOG="/var/log/backup/mysql_$(date +%Y%m%d).log"

mkdir -p "$BACKUP_DIR"
exec > >(tee -a "$LOG") 2>&1

echo "===== MySQL 备份开始: $(date) ====="

# 获取所有数据库列表
DATABASES=$(mysql --defaults-file="$MYSQL_PASS_FILE" -N -e "SHOW DATABASES;" \
  | grep -Ev "^(information_schema|performance_schema|sys)$")

for DB in $DATABASES; do
    echo "备份数据库: $DB"
    DUMP_FILE="$BACKUP_DIR/${DB}_$(date +%Y%m%d_%H%M).sql.gz"
    
    mysqldump --defaults-file="$MYSQL_PASS_FILE" \
      --single-transaction --routines --triggers --events \
      "$DB" | gzip > "$DUMP_FILE"
    
    # 验证备份文件非空
    if [ -s "$DUMP_FILE" ]; then
        SIZE=$(du -h "$DUMP_FILE" | cut -f1)
        echo "  ✅ $DB 备份成功 ($SIZE)"
    else
        echo "  ❌ $DB 备份失败(文件为空)" >&2
        exit 1
    fi
done

# 清理旧备份
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "清理 $RETENTION_DAYS 天前的备份完成"

echo "===== MySQL 备份完成: $(date) ====="

3.2 PostgreSQL 备份

# 逻辑备份
pg_dump -U postgres -Fc mydb > /backup/pg/mydb_$(date +%Y%m%d).dump
pg_dumpall -U postgres | gzip > /backup/pg/all_$(date +%Y%m%d).sql.gz

# 恢复
pg_restore -U postgres -d mydb -c /backup/pg/mydb_20260610.dump
gunzip < /backup/pg/all_20260610.sql.gz | psql -U postgres

# WAL 归档(支持 PITR,Point-in-Time Recovery)
# postgresql.conf 配置:
# archive_mode = on
# archive_command = 'cp %p /backup/pg/wal/%f'
# wal_level = replica

# 基础备份 + WAL 归档实现 PITR
pg_basebackup -U postgres -D /backup/pg/base -Ft -z -P

# 恢复到指定时间点
# 1. 恢复基础备份
# 2. 配置 recovery.conf / postgresql.conf
# restore_command = 'cp /backup/pg/wal/%f %p'
# recovery_target_time = '2026-06-10 12:00:00'

3.3 Redis 备份

# RDB 快照备份
redis-cli BGSAVE
cp /var/lib/redis/dump.rdb /backup/redis/dump_$(date +%Y%m%d_%H%M).rdb

# AOF 备份(更实时)
redis-cli BGREWRITEAOF
cp /var/lib/redis/appendonly.aof /backup/redis/aof_$(date +%Y%m%d_%H%M).aof

# 恢复
systemctl stop redis
cp /backup/redis/dump_20260610_0200.rdb /var/lib/redis/dump.rdb
chown redis:redis /var/lib/redis/dump.rdb
systemctl start redis

# Redis 自动化备份脚本
#!/bin/bash
BACKUP_DIR="/backup/redis"
RETENTION_DAYS=7
mkdir -p "$BACKUP_DIR"

# 触发 RDB 快照
redis-cli BGSAVE
sleep 5  # 等待快照完成

# 检查快照状态
LASTSAVE=$(redis-cli LASTSAVE)
echo "Redis LASTSAVE: $LASTSAVE"

# 备份
TIMESTAMP=$(date +%Y%m%d_%H%M)
cp /var/lib/redis/dump.rdb "$BACKUP_DIR/dump_${TIMESTAMP}.rdb"

# 同时备份 AOF(如果启用)
if [ -f /var/lib/redis/appendonly.aof ]; then
    redis-cli BGREWRITEAOF
    sleep 3
    cp /var/lib/redis/appendonly.aof "$BACKUP_DIR/aof_${TIMESTAMP}.aof"
fi

# 上传到对象存储
aws --endpoint-url http://minio.local:9000 \
    s3 cp "$BACKUP_DIR/dump_${TIMESTAMP}.rdb" s3://redis-backups/

# 清理旧备份
find "$BACKUP_DIR" -name "dump_*.rdb" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "aof_*.aof" -mtime +$RETENTION_DAYS -delete

echo "Redis 备份完成: $TIMESTAMP"

四、对象存储备份:OSS / S3 / MinIO

4.1 阿里云 OSS

# 安装 ossutil
wget https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip
unzip ossutil*.zip && mv ossutil*/ossutil64 /usr/local/bin/ossutil

# 配置
ossutil config -e oss-cn-hangzhou.aliyuncs.com \
  -i LTAI4XXXXXXXX -k XXXXXXXXXXXXXXXXXXX

# 上传备份
ossutil cp /backup/mysql/ oss://my-backup-bucket/mysql/ -r --update

# 设置生命周期策略(通过控制台或 API)
# 30 天后转为低频访问,90 天后转为归档存储,365 天后删除

# 带服务端加密
ossutil cp /backup/ oss://my-backup-bucket/ \
  -r --meta=x-oss-server-side-encryption:AES256

# 跨区域复制(CRR):在控制台配置源桶到目标桶的复制规则

4.2 AWS S3 / MinIO

# 安装 AWS CLI
pip install awscli

# 配置 MinIO 或 S3
aws configure set aws_access_key_id "AKIAXXXXXXXXXX"
aws configure set aws_secret_access_key "xxxxxxxxxxxxxxxx"
aws configure set default.region "us-east-1"

# 上传(使用 MinIO endpoint)
aws --endpoint-url http://minio.local:9000 \
    s3 sync /backup/ s3://server-backups/ --storage-class STANDARD_IA

# 设置桶策略:版本控制 + 不可变存储(防勒索)
aws --endpoint-url http://minio.local:9000 \
    s3api put-bucket-versioning --bucket server-backups \
    --versioning-configuration Status=Enabled

# 不可变存储(WORM)
aws --endpoint-url http://minio.local:9000 \
    s3api put-object-lock-configuration --bucket server-backups \
    --object-lock-configuration '{
      "ObjectLockEnabled": true,
      "Rule": {"DefaultRetention": {"Mode": "COMPLIANCE", "Days": 90}}
    }'

# S3 生命周期规则
cat > lifecycle.json << 'EOF'
{
  "Rules": [
    {
      "ID": "BackupLifecycle",
      "Status": "Enabled",
      "Filter": {"Prefix": "backups/"},
      "Transitions": [
        {"Days": 30, "StorageClass": "STANDARD_IA"},
        {"Days": 90, "StorageClass": "GLACIER"}
      ],
      "Expiration": {"Days": 365}
    }
  ]
}
EOF

aws --endpoint-url http://minio.local:9000 \
    s3api put-bucket-lifecycle-configuration \
    --bucket server-backups \
    --lifecycle-configuration file://lifecycle.json

五、云服务器快照

5.1 阿里云 ECS 快照

# 使用阿里云 CLI 创建快照
aliyun ecs CreateSnapshot \
  --DiskId d-bp1xxxxxxxxxxxxxxx \
  --SnapshotName "backup-$(date +%Y%m%d)" \
  --Description "自动每日快照"

# 创建自动快照策略
aliyun ecs CreateAutoSnapshotPolicy \
  --RegionId cn-hangzhou \
  --RepeatWeekdays "1,2,3,4,5,6,7" \
  --TimePoints "2,4,6" \
  --RetentionDays 30 \
  --AutoSnapshotPolicyName "daily-snapshot-policy"

# 绑定磁盘到快照策略
aliyun ecs ApplyAutoSnapshotPolicy \
  --AutoSnapshotPolicyId sp-bp1xxxxxxxxxxxxxxx \
  --diskIds '["d-bp1xxxxxxxxxxxxxxx"]'

5.2 AWS EC2 快照

# 创建 EBS 快照
aws ec2 create-snapshot \
  --volume-id vol-0123456789abcdef0 \
  --description "Daily backup $(date +%Y%m%d)" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Name,Value=daily-backup}]'

# 使用 Data Lifecycle Manager 自动化
aws dlm create-lifecycle-policy \
  --description "Daily EBS snapshots" \
  --state ENABLED \
  --execution-role-arn arn:aws:iam::123456789012:role/DLMRole \
  --policy-details '{
    "PolicyType": "EBS_SNAPSHOT_MANAGEMENT",
    "ResourceTypes": ["VOLUME"],
    "TargetTags": [{"Key": "Backup", "Value": "daily"}],
    "Schedules": [{
      "Name": "DailySnapshots",
      "CreateRule": {"Interval": 24, "IntervalUnit": "HOURS", "Times": ["03:00"]},
      "RetainRule": {"Count": 14}
    }]
  }'

# 跨区域复制快照(灾难恢复)
aws ec2 copy-snapshot \
  --source-region cn-north-1 \
  --source-snapshot-id snap-0123456789abcdef0 \
  --destination-region cn-northwest-1 \
  --description "DR copy"

六、灾难恢复计划(DRP)

6.1 DRP 核心要素

灾难恢复计划 DRP(Disaster Recovery Plan)
├── 1. 风险评估
│   ├── 自然灾害(地震、洪水)
│   ├── 硬件故障(磁盘、电源、网络)
│   ├── 人为失误(误删、误操作)
│   ├── 安全事件(勒索软件、入侵)
│   └── 供应商故障(云服务商宕机)
│
├── 2. 业务影响分析(BIA)
│   ├── 识别关键业务系统
│   ├── 量化停机损失
│   └── 确定 RPO/RTO
│
├── 3. 恢复策略
│   ├── 热备(Hot Standby):秒级切换
│   ├── 温备(Warm Standby):分钟级切换
│   ├── 冷备(Cold Standby):小时级切换
│   └── 多活(Active-Active):零停机
│
├── 4. 恢复流程文档
│   ├── 联系人列表
│   ├── 逐步恢复手册
│   ├── 验证清单
│   └── 回退方案
│
└── 5. 定期演练
    ├── 桌面推演(每季度)
    ├── 模拟恢复(每半年)
    └── 全面切换演练(每年)

6.2 恢复流程检查清单模板

## 灾难恢复检查清单

### 阶段一:评估与通知(0-15 分钟)
- [ ] 确认故障范围和影响
- [ ] 通知技术负责人和管理层
- [ ] 启动 DRP 流程
- [ ] 记录故障时间点

### 阶段二:基础设施恢复(15-60 分钟)
- [ ] 启动备用服务器/云实例
- [ ] 恢复网络配置(DNS、防火墙、VPN)
- [ ] 挂载存储卷
- [ ] 验证基础设施连通性

### 阶段三:数据恢复(1-4 小时)
- [ ] 恢复数据库(从最近有效备份)
- [ ] 恢复应用配置文件
- [ ] 恢复 SSL 证书
- [ ] 同步最新数据(如增量备份)

### 阶段四:应用恢复与验证(1-2 小时)
- [ ] 启动应用服务
- [ ] 运行健康检查脚本
- [ ] 验证核心业务流程
- [ ] 通知用户恢复完成

### 阶段五:收尾
- [ ] 监控系统稳定性(至少 24 小时)
- [ ] 记录故障报告
- [ ] 执行事后复盘(Post-mortem)
- [ ] 更新 DRP 文档

6.3 MySQL 主从切换脚本(灾难恢复场景)

#!/bin/bash
# /opt/scripts/mysql_failover.sh
# 从库提升为主库

set -euo pipefail

SLAVE_HOST="10.0.1.12"
SLAVE_USER="admin"
SLAVE_PASS="AdminPass123!"

echo "===== MySQL 灾难切换开始: $(date) ====="

# 1. 确认从库状态
SLAVE_STATUS=$(mysql -h "$SLAVE_HOST" -u "$SLAVE_USER" -p"$SLAVE_PASS" \
  -e "SHOW SLAVE STATUS\G" 2>/dev/null)

IO_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_IO_Running:" | awk '{print $2}')
SQL_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}')
SECONDS_BEHIND=$(echo "$SLAVE_STATUS" | grep "Seconds_Behind_Master:" | awk '{print $2}')

echo "IO Thread: $IO_RUNNING"
echo "SQL Thread: $SQL_RUNNING"
echo "延迟: ${SECONDS_BEHIND}秒"

if [ "$IO_RUNNING" != "Yes" ] || [ "$SQL_SQL_RUNNING" != "Yes" ]; then
    echo "❌ 从库复制状态异常,需要手动检查"
    exit 1
fi

# 2. 等待复制追平
if [ "$SECONDS_BEHIND" != "NULL" ] && [ "$SECONDS_BEHIND" -gt 0 ]; then
    echo "等待复制追平(当前延迟 ${SECONDS_BEHIND}s)..."
    mysql -h "$SLAVE_HOST" -u "$SLAVE_USER" -p"$SLAVE_PASS" \
      -e "SELECT MASTER_POS_WAIT('mysql-bin.000099', 999999999);"
fi

# 3. 停止从库复制
mysql -h "$SLAVE_HOST" -u "$SLAVE_USER" -p"$SLAVE_PASS" \
  -e "STOP SLAVE; RESET SLAVE ALL;"

# 4. 确认提升成功
NEW_MASTER_STATUS=$(mysql -h "$SLAVE_HOST" -u "$SLAVE_USER" -p"$SLAVE_PASS" \
  -e "SHOW MASTER STATUS\G")
echo "新主库状态:"
echo "$NEW_MASTER_STATUS"

# 5. 更新应用配置(示例:更新配置中心)
# curl -X PUT http://config.local:8080/config/mysql.master \
#   -d "value=$SLAVE_HOST"

echo "===== 切换完成: $(date) ====="
echo "⚠️ 请手动验证应用连接和数据完整性"

七、备份监控与告警

7.1 备份失败告警脚本

#!/bin/bash
# /opt/scripts/backup_monitor.sh
# 检查最近备份状态,失败时告警

set -euo pipefail

BACKUP_LOG_DIR="/var/log/backup"
ALERT_WEBHOOK="https://hooks.dingtalk.com/robot/send?access_token=xxxx"
BACKUP_DIRS=("/backup/mysql" "/backup/restic" "/backup/redis")
MAX_AGE_HOURS=26  # 超过 26 小时未更新视为异常

ERRORS=()

for DIR in "${BACKUP_DIRS[@]}"; do
    if [ ! -d "$DIR" ]; then
        ERRORS+=("目录不存在: $DIR")
        continue
    fi
    
    # 检查最近修改的文件
    LATEST=$(find "$DIR" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1)
    
    if [ -z "$LATEST" ]; then
        ERRORS+=("目录为空: $DIR")
        continue
    fi
    
    FILE_TIME=$(echo "$LATEST" | awk '{print $1}' | cut -d. -f1)
    FILE_NAME=$(echo "$LATEST" | awk '{print $2}')
    NOW=$(date +%s)
    AGE_HOURS=$(( (NOW - FILE_TIME) / 3600 ))
    
    if [ "$AGE_HOURS" -gt "$MAX_AGE_HOURS" ]; then
        ERRORS+=("$DIR 最新备份已 ${AGE_HOURS} 小时: $FILE_NAME")
    else
        echo "✅ $DIR: ${AGE_HOURS}h ago ($FILE_NAME)"
    fi
done

# 检查备份日志中的错误
for LOG in "$BACKUP_LOG_DIR"/*.log; do
    if [ -f "$LOG" ]; then
        LOG_DATE=$(basename "$LOG" | grep -oP '\d{8}')
        TODAY=$(date +%Y%m%d)
        if [ "$LOG_DATE" = "$TODAY" ]; then
            if grep -qi "error\|failed\|❌" "$LOG"; then
                ERRORS+=("日志有错误: $(basename $LOG)")
            fi
        fi
    fi
done

# 发送告警
if [ ${#ERRORS[@]} -gt 0 ]; then
    MSG="🚨 备份告警 - $(hostname)\n\n"
    for ERR in "${ERRORS[@]}"; do
        MSG+="• $ERR\n"
    done
    
    # 钉钉/飞书/企业微信告警
    curl -s -X POST "$ALERT_WEBHOOK" \
      -H 'Content-Type: application/json' \
      -d "{
        \"msgtype\": \"text\",
        \"text\": {\"content\": \"$(echo -e "$MSG")\"}
      }"
    
    echo -e "$MSG"
    exit 1
else
    echo "✅ 所有备份检查通过"
fi

7.2 备份完整性校验

#!/bin/bash
# /opt/scripts/backup_verify.sh
# 定期校验备份可恢复性

set -euo pipefail

VERIFY_DIR="/tmp/backup_verify_$(date +%Y%m%d)"
mkdir -p "$VERIFY_DIR"
PASS=true

echo "===== 备份完整性校验: $(date) ====="

# 1. MySQL 备份验证
echo "--- MySQL 备份验证 ---"
LATEST_MYSQL=$(ls -t /backup/mysql/*.sql.gz 2>/dev/null | head -1)
if [ -n "$LATEST_MYSQL" ]; then
    # 测试解压 + 语法检查
    if gunzip -t "$LATEST_MYSQL" 2>/dev/null; then
        # 进一步验证:恢复到测试实例
        TEST_DB="verify_test_$(date +%s)"
        mysql -u root -p"RootPass" -e "CREATE DATABASE $TEST_DB;"
        if gunzip -c "$LATEST_MYSQL" | mysql -u root -p"RootPass" "$TEST_DB" 2>/dev/null; then
            TABLE_COUNT=$(mysql -u root -p"RootPass" -N -e \
              "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$TEST_DB';")
            echo "  ✅ MySQL 备份验证通过($TABLE_COUNT 张表)"
        else
            echo "  ❌ MySQL 备份恢复失败"
            PASS=false
        fi
        mysql -u root -p"RootPass" -e "DROP DATABASE $TEST_DB;"
    else
        echo "  ❌ MySQL 备份文件损坏"
        PASS=false
    fi
fi

# 2. Restic 快照验证
echo "--- Restic 快照验证 ---"
export RESTIC_REPOSITORY="s3:http://minio.local:9000/backups"
export RESTIC_PASSWORD_FILE="/etc/restic/password"

if restic check --no-lock 2>/dev/null; then
    SNAPSHOTS=$(restic snapshots --json 2>/dev/null | jq length)
    echo "  ✅ Restic 仓库完整($SNAPSHOTS 个快照)"
else
    echo "  ❌ Restic 仓库校验失败"
    PASS=false
fi

# 3. Redis 备份验证
echo "--- Redis 备份验证 ---"
LATEST_REDIS=$(ls -t /backup/redis/dump_*.rdb 2>/dev/null | head -1)
if [ -n "$LATEST_REDIS" ]; then
    FILE_SIZE=$(stat -f%z "$LATEST_REDIS" 2>/dev/null || stat -c%s "$LATEST_REDIS" 2>/dev/null)
    if [ "$FILE_SIZE" -gt 100 ]; then
        echo "  ✅ Redis 备份文件正常($(du -h "$LATEST_REDIS" | cut -f1))"
    else
        echo "  ❌ Redis 备份文件异常(${FILE_SIZE} bytes)"
        PASS=false
    fi
fi

# 清理
rm -rf "$VERIFY_DIR"

if [ "$PASS" = true ]; then
    echo "===== ✅ 所有备份验证通过 ====="
    exit 0
else
    echo "===== ❌ 部分备份验证失败,请检查 ====="
    exit 1
fi

八、完整备份方案模板

以下是一套可直接落地的企业级备份方案模板,适用于中等规模(10-50 台服务器)的生产环境。

8.1 备份架构总览

┌─────────────────────────────────────────────────────────┐
│                    生产服务器集群                          │
│  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐               │
│  │Web-1 │  │Web-2 │  │DB-M  │  │DB-S  │  ...          │
│  └──┬───┘  └──┬───┘  └──┬───┘  └──┬───┘               │
└─────┼────────┼────────┼────────┼──────────────────────┘
      │        │        │        │
      ▼        ▼        ▼        ▼
┌─────────────────────────────────────────────────────────┐
│              备份服务器(本地 NAS/存储)                    │
│  ┌──────────────────┐  ┌──────────────────┐            │
│  │   Restic 仓库     │  │   MySQL 物理备份   │            │
│  │  (去重加密存储)    │  │  (xtrabackup)     │            │
│  └──────────────────┘  └──────────────────┘            │
└─────────────────────────────┬───────────────────────────┘
                              │ 异步同步
                              ▼
┌─────────────────────────────────────────────────────────┐
│              异地备份(对象存储 / 另一机房)                 │
│  ┌──────────────────┐  ┌──────────────────┐            │
│  │  S3/OSS 归档存储   │  │  跨区域复制副本    │            │
│  │  (不可变存储)      │  │  (DR 站点)        │            │
│  └──────────────────┘  └──────────────────┘            │
└─────────────────────────────────────────────────────────┘

8.2 Crontab 配置汇总

# /etc/crontab - 备份调度总览

# === 文件系统备份 ===
# 每日凌晨 2:00 - rsync 增量同步到备份服务器
0 2 * * * root /opt/scripts/rsync_backup.sh

# 每日凌晨 3:00 - Restic 加密去重备份
0 3 * * * root /opt/scripts/restic_backup.sh

# === 数据库备份 ===
# 每日凌晨 4:00 - MySQL 全量备份
0 4 * * * root /opt/scripts/mysql_backup.sh

# 每 6 小时 - MySQL 增量备份(xtrabackup)
0 */6 * * * root /opt/scripts/mysql_incr_backup.sh

# 每日凌晨 4:30 - PostgreSQL 备份
30 4 * * * root /opt/scripts/pg_backup.sh

# 每 2 小时 - Redis 快照备份
0 */2 * * * root /opt/scripts/redis_backup.sh

# === 对象存储备份 ===
# 每日凌晨 5:00 - 同步到 OSS/S3
0 5 * * * root /opt/scripts/oss_sync.sh

# === 监控与校验 ===
# 每日 8:00 - 备份状态检查
0 8 * * * root /opt/scripts/backup_monitor.sh

# 每周日 6:00 - 备份完整性校验
0 6 * * 0 root /opt/scripts/backup_verify.sh

# 每月 1 日 - 月度全量备份保留
0 1 1 * * root /opt/scripts/monthly_archive.sh

8.3 备份容量规划公式

所需存储空间 = 每日数据变化量 × 保留天数 × 安全系数

示例:
├── 数据库总大小:500 GB
├── 每日变化率:5%
├── 增量备份保留:30 天
├── 全量备份保留:4 份(每月)
├── 去重压缩比:约 3:1
│
├── 增量存储 = 500GB × 5% × 30天 / 3 = 250 GB
├── 全量存储 = 500GB × 4份 / 3 = 667 GB
├── 总计 ≈ 917 GB
└── 建议预留 1.5 倍 = 1.4 TB

8.4 告警通知集成(飞书机器人)

#!/bin/bash
# /opt/scripts/notify_feishu.sh
# 飞书机器人告警封装

FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx"

send_feishu_alert() {
    local title="$1"
    local content="$2"
    local color="${3:-red}"  # red/green/yellow
    
    curl -s -X POST "$FEISHU_WEBHOOK" \
      -H 'Content-Type: application/json' \
      -d "{
        \"msg_type\": \"interactive\",
        \"card\": {
          \"header\": {
            \"title\": {\"tag\": \"plain_text\", \"content\": \"$title\"},
            \"template\": \"$color\"
          },
          \"elements\": [{
            \"tag\": \"div\",
            \"text\": {\"tag\": \"lark_md\", \"content\": \"$content\"}
          }]
        }
      }"
}

# 使用示例
send_feishu_alert \
  "🚨 备份告警: $(hostname)" \
  "**MySQL 备份失败**\n时间: $(date)\n错误: 磁盘空间不足\n剩余: 2.1 GB" \
  "red"

send_feishu_alert \
  "✅ 备份成功: $(hostname)" \
  "**每日备份报告**\nMySQL: ✅ 128GB\nRestic: ✅ 压缩后 45GB\nRedis: ✅ 3.2GB" \
  "green"

九、总结与最佳实践

核心原则

原则

说明

3-2-1-1-0

3 份副本、2 种介质、1 份异地、1 份离线/不可变、0 份未验证

备份不等于恢复

定期执行恢复演练,验证备份可用性

自动化一切

手动备份 = 忘记备份

监控备份本身

备份失败不告警 = 没有备份

加密传输存储

备份数据是攻击者的高价值目标

文档化恢复流程

灾难时没有时间研究文档

常见坑与建议

❌ 常见错误:
├── 只备份不验证(恢复时发现数据损坏)
├── 备份和生产在同一台机器(磁盘故障一起挂)
├── 备份密码写在脚本里(泄露风险)
├── 保留策略不合理(空间暴涨或关键数据被清理)
└── 没有恢复时间预估(老板问"多久能好"答不上来)

✅ 推荐做法:
├── 每月至少一次恢复演练
├── 备份密码使用密钥管理服务(KMS/Vault)
├── 备份脚本纳入版本控制(Git)
├── 保留策略与合规要求对齐
├── 建立 RTO/RPO 仪表盘,实时可视化
└── 每次变更后重新评估备份策略

📌 本文是"服务器运维笔记"系列第 22 篇。 备份是运维的底线,灾难恢复是备份的终极检验。做好备份,睡个好觉。