数据库备份与灾难恢复完整方案:MySQL自动备份、异地存储与RTO/RPO保障实战
备份是数据安全的最后一道防线。很多站长以为配置了主从同步就万事大吉,但主从同步无法防御误删除、逻辑损坏或勒索软件——这些灾难会同步到所有副本。本文建立一套完整的备份与恢复体系。
一、备份策略设计
RPO与RTO目标定义
| 指标 | 全称 | 含义 | 典型目标 |
|---|---|---|---|
| RPO | Recovery Point Objective | 可接受的最大数据丢失量(时间) | 电商:1小时内 / 博客:24小时内 |
| RTO | Recovery Time Objective | 从灾难发生到业务恢复的最长时间 | 电商:4小时内 / 博客:24小时内 |
3-2-1备份原则
<code">3 份数据副本(1份生产 + 2份备份)
↓
2 种不同的存储介质(如本地磁盘 + 对象存储)
↓
1 份异地存储(不在同一物理位置)
实现方案:
本地备份:香港VPS本地磁盘(/backup/)→ 1份
异地备份:Cloudflare R2对象存储(香港以外节点)→ 1份
长期归档:Backblaze B2(低成本冷存储)→ 1份二、MySQL全量备份脚本
<code">#!/bin/bash
# /usr/local/bin/mysql-backup.sh
# 完整的MySQL备份脚本,含压缩、加密、验证、上传
set -euo pipefail
# ============ 配置 ============
DB_USER="backup_user"
DB_PASS="${MYSQL_BACKUP_PASSWORD}" # 从环境变量读取
DB_HOST="localhost"
BACKUP_DIR="/backup/mysql"
LOG_FILE="/var/log/mysql-backup.log"
RETENTION_DAYS=7 # 本地保留7天
ENCRYPTION_KEY="${BACKUP_ENCRYPTION_KEY}" # GPG加密密钥ID
# R2配置
R2_BUCKET="my-db-backups"
R2_ENDPOINT="https://账号ID.r2.cloudflarestorage.com"
AWS_ACCESS_KEY_ID="${R2_ACCESS_KEY}"
AWS_SECRET_ACCESS_KEY="${R2_SECRET_KEY}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DATE_DIR=$(date +%Y/%m/%d)
BACKUP_FILE="${BACKUP_DIR}/${TIMESTAMP}.sql.gz"
ENCRYPTED_FILE="${BACKUP_FILE}.gpg"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
# ============ 创建专用备份账号(首次运行时执行) ============
create_backup_user() {
mysql -u root -p -e "
CREATE USER IF NOT EXISTS 'backup_user'@'localhost'
IDENTIFIED BY '${DB_PASS}';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES,
PROCESS, REPLICATION CLIENT ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;"
}
# ============ 执行备份 ============
mkdir -p "$BACKUP_DIR"
log "开始备份 MySQL..."
mysqldump \
--host="$DB_HOST" \
--user="$DB_USER" \
--password="$DB_PASS" \
--all-databases \
--single-transaction \ # 不锁表(InnoDB热备)
--flush-logs \ # 备份时刷新binlog
--master-data=2 \ # 记录binlog位置(用于时间点恢复)
--routines \
--triggers \
--events \
--quick \ # 大库必加,避免内存溢出
2>>"$LOG_FILE" \
| gzip -9 > "$BACKUP_FILE"
BACKUP_SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
log "备份完成,大小:${BACKUP_SIZE},文件:${BACKUP_FILE}"
# ============ GPG加密(防止备份泄露) ============
if [ -n "$ENCRYPTION_KEY" ]; then
gpg --batch --yes \
--recipient "$ENCRYPTION_KEY" \
--output "$ENCRYPTED_FILE" \
--encrypt "$BACKUP_FILE"
rm -f "$BACKUP_FILE"
UPLOAD_FILE="$ENCRYPTED_FILE"
log "加密完成:${ENCRYPTED_FILE}"
else
UPLOAD_FILE="$BACKUP_FILE"
fi
# ============ 备份验证 ============
verify_backup() {
local file="$1"
log "验证备份文件完整性..."
if [[ "$file" == *.gpg ]]; then
# 验证GPG文件格式
gpg --batch --list-packets "$file" > /dev/null 2>&1 && \
log "GPG加密验证通过" || { log "❌ GPG文件损坏!"; return 1; }
else
# 验证gzip文件完整性
gzip -t "$file" && log "Gzip验证通过" || { log "❌ Gzip文件损坏!"; return 1; }
fi
}
verify_backup "$UPLOAD_FILE"
# ============ 上传到R2异地存储 ============
log "上传到Cloudflare R2..."
AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
aws s3 cp "$UPLOAD_FILE" \
"s3://${R2_BUCKET}/mysql/${DATE_DIR}/$(basename $UPLOAD_FILE)" \
--endpoint-url "$R2_ENDPOINT" \
--storage-class STANDARD \
2>>"$LOG_FILE"
log "R2上传完成:mysql/${DATE_DIR}/$(basename $UPLOAD_FILE)"
# ============ 清理本地旧备份 ============
find "$BACKUP_DIR" -name "*.gz*" -mtime +$RETENTION_DAYS -delete
log "已清理${RETENTION_DAYS}天前的本地备份"
# ============ 发送备份完成通知 ============
curl -s -X POST "${TELEGRAM_WEBHOOK}" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
-d "text=✅ 数据库备份完成
时间:$(date)
大小:${BACKUP_SIZE}
文件:$(basename $UPLOAD_FILE)
已上传至R2异地存储" > /dev/null
log "备份流程完成 ✅"<code"># 定时任务配置
crontab -e
# 每天凌晨2点全量备份
0 2 * * * MYSQL_BACKUP_PASSWORD=密码 BACKUP_ENCRYPTION_KEY=密钥ID \
R2_ACCESS_KEY=R2密钥 R2_SECRET_KEY=R2密钥 \
/usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup-cron.log 2>&1三、增量备份(Binlog)
<code"># 全量备份之间,用binlog实现增量(可恢复到任意时间点)
# 1. 确认binlog已开启
mysql -u root -p -e "SHOW VARIABLES LIKE 'log_bin';"
# log_bin = ON
# 2. 每小时归档当前binlog
#!/bin/bash
# /usr/local/bin/binlog-backup.sh
BINLOG_DIR="/var/lib/mysql"
ARCHIVE_DIR="/backup/binlog/$(date +%Y%m%d)"
mkdir -p "$ARCHIVE_DIR"
# 刷新binlog,让MySQL切换到新文件
mysql -u root -p密码 -e "FLUSH BINARY LOGS;"
# 将除当前binlog外的所有binlog归档
for binlog in $(ls ${BINLOG_DIR}/mysql-bin.* | head -n -1); do
if [ -f "$binlog" ]; then
gzip -c "$binlog" > "${ARCHIVE_DIR}/$(basename $binlog).gz"
echo "已归档:$binlog"
fi
done四、灾难恢复演练
<code">#!/bin/bash
# /usr/local/bin/backup-restore-test.sh
# 每月执行一次,验证备份真实可恢复
set -e
BACKUP_DIR="/backup/mysql"
TEST_DB="restore_test_$(date +%Y%m)"
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.gz 2>/dev/null | head -1)
if [ -z "$LATEST_BACKUP" ]; then
echo "❌ 未找到备份文件!"
exit 1
fi
echo "=== 备份恢复测试 ==="
echo "使用备份:$LATEST_BACKUP"
# 创建测试数据库
mysql -u root -p密码 -e "DROP DATABASE IF EXISTS ${TEST_DB}; CREATE DATABASE ${TEST_DB};"
# 恢复到测试数据库
echo "正在恢复..."
gunzip < "$LATEST_BACKUP" | mysql -u root -p密码 "$TEST_DB" # 验证恢复结果(检查关键表是否存在) TABLES=$(mysql -u root -p密码 "$TEST_DB" -e "SHOW TABLES;" 2>/dev/null | wc -l)
echo "已恢复 ${TABLES} 张表"
if [ "$TABLES" -gt 0 ]; then
echo "✅ 备份恢复测试通过"
RESULT="✅ 本月备份恢复测试通过,恢复表数:${TABLES}"
else
echo "❌ 备份恢复测试失败!"
RESULT="❌ 备份恢复测试失败!请立即检查备份"
fi
# 清理测试数据库
mysql -u root -p密码 -e "DROP DATABASE ${TEST_DB};"
# 发送测试结果通知
curl -s -X POST "${TELEGRAM_WEBHOOK}" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
-d "text=${RESULT}"五、时间点恢复(PITR)
<code">#!/bin/bash
# 从全量备份 + binlog恢复到指定时间点
# 场景:昨天下午3:30误删了重要数据,需要恢复到3:29的状态
FULL_BACKUP="/backup/mysql/20260101_020000.sql.gz"
TARGET_TIME="2026-01-02 15:29:59"
echo "=== 时间点恢复(恢复到 $TARGET_TIME 之前的状态)==="
# Step 1: 恢复最近一次全量备份
echo "Step 1: 恢复全量备份..."
gunzip < "$FULL_BACKUP" | mysql -u root -p密码
# Step 2: 从全量备份中找到binlog位置
BINLOG_INFO=$(gunzip -c "$FULL_BACKUP" | grep "MASTER_LOG_FILE" | head -1)
echo "Binlog起始位置:$BINLOG_INFO"
# Step 3: 应用binlog到目标时间点
echo "Step 2: 应用增量binlog..."
mysqlbinlog \
--stop-datetime="$TARGET_TIME" \
--base64-output=DECODE-ROWS \
/backup/binlog/20260101/mysql-bin.000123.gz \
/backup/binlog/20260102/mysql-bin.000124.gz \
| mysql -u root -p密码
echo "✅ 时间点恢复完成,已恢复到 $TARGET_TIME 的状态"六、备份监控告警
<code">#!/bin/bash
# /usr/local/bin/backup-monitor.sh
# 每天上午9点检查备份是否正常执行
BACKUP_DIR="/backup/mysql"
MAX_AGE_HOURS=26 # 超过26小时未备份则告警
# 查找最近的备份文件
LATEST=$(find "$BACKUP_DIR" -name "*.gz*" -type f -printf '%T@ %p\n' 2>/dev/null | \
sort -n | tail -1 | awk '{print $2}')
if [ -z "$LATEST" ]; then
MSG="❌ 严重告警:未找到任何备份文件!"
else
AGE_SECONDS=$(( $(date +%s) - $(stat -c %Y "$LATEST") ))
AGE_HOURS=$(( AGE_SECONDS / 3600 ))
if [ "$AGE_HOURS" -gt "$MAX_AGE_HOURS" ]; then
MSG="⚠️ 备份告警:最近备份已有 ${AGE_HOURS} 小时,超过预期!最新文件:$(basename $LATEST)"
else
MSG="✅ 备份正常:最近备份于 ${AGE_HOURS} 小时前完成($(basename $LATEST))"
fi
fi
echo "$MSG"
# 发送到Telegram
curl -s -X POST "${TELEGRAM_WEBHOOK}" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
-d "text=$MSG" > /dev/null七、总结
完整的备份体系:全量备份(每日)+ Binlog增量(每小时)+ 异地存储(R2)+ 定期恢复测试(每月)+ 备份监控告警(每日)。这五个环节缺一不可——没有恢复测试的备份是不可靠的备份。IDC.Net的香港VPS支持快照功能,可以与数据库备份结合,在系统级别提供额外的保护层。
版权声明:
作者:后浪云
链接:https://idc.net/help/442869/
文章版权归作者所有,未经允许请勿转载。
THE END
