没有经过验证的备份,等于没有备份。——运维第一定律
一、备份基础:先搞清楚"救什么"和"救多快"
在讨论工具和脚本之前,先回答两个灵魂问题:
数据丢了,能丢多少? → RPO(Recovery Point Objective,恢复点目标)
系统挂了,多久必须恢复? → RTO(Recovery Time Objective,恢复时间目标)
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 -delete2.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 -pxtrabackup(物理备份,推荐生产环境):
# 安装
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 mysqldMySQL 自动化备份脚本:
#!/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 "✅ 所有备份检查通过"
fi7.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.sh8.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 TB8.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"九、总结与最佳实践
核心原则
常见坑与建议
❌ 常见错误:
├── 只备份不验证(恢复时发现数据损坏)
├── 备份和生产在同一台机器(磁盘故障一起挂)
├── 备份密码写在脚本里(泄露风险)
├── 保留策略不合理(空间暴涨或关键数据被清理)
└── 没有恢复时间预估(老板问"多久能好"答不上来)
✅ 推荐做法:
├── 每月至少一次恢复演练
├── 备份密码使用密钥管理服务(KMS/Vault)
├── 备份脚本纳入版本控制(Git)
├── 保留策略与合规要求对齐
├── 建立 RTO/RPO 仪表盘,实时可视化
└── 每次变更后重新评估备份策略📌 本文是"服务器运维笔记"系列第 22 篇。 备份是运维的底线,灾难恢复是备份的终极检验。做好备份,睡个好觉。