为什么备份策略值得认真设计#
数据库备份是那种「平时用不到,用到的时候不能出错」的东西。很多团队的备份是有的,但真正到恢复的时候才发现:备份文件损坏、恢复流程没人走过、时间点恢复的 binlog 对不上。
我见过的最惨的案例:一个团队每天做 mysqldump 备份,某次误操作删了核心表的数据,去恢复才发现 mysqldump 命令写错了参数,三个月来备份文件都是空的。
好的备份方案有三个要素:能备上、能恢复、定期验证过。
备份策略设计#
全量 + 增量 + 二进制日志#
一个生产级 MySQL 备份策略通常有三层:
全量备份(每周/每天)
↓
增量备份(每天/每小时)
↓
二进制日志(binlog)连续归档
全量备份:完整的数据快照,恢复起点。大数据量下(50GB+)用 XtraBackup 热备,小数据量(<10GB)用 mysqldump 也可以。
增量备份:基于上次全量或增量的变化量,减少存储和备份时间。XtraBackup 支持真正的增量备份(基于 LSN)。
binlog 归档:记录所有 DDL 和 DML,是时间点恢复(PITR)的基础。应该实时或近实时同步到安全位置。
RTO(恢复时间目标)和 RPO(恢复点目标)决定了策略的具体参数:
| 场景 | RPO | RTO | 策略 |
|---|---|---|---|
| 核心业务数据库 | < 5 分钟 | < 1 小时 | 每日全量 + 实时 binlog |
| 一般业务 | < 1 小时 | < 4 小时 | 每日全量 + 每小时增量 |
| 非核心/测试 | < 24 小时 | < 8 小时 | 每日全量 |
mysqldump:逻辑备份的基础工具#
适用场景#
- 数据量小(< 10GB),可以接受备份窗口期
- 需要跨版本迁移(5.7 → 8.0)或跨引擎迁移
- 需要只备份部分表或数据库
- 输出格式是 SQL,便于审查和修改
核心命令#
全库备份:
mysqldump \
--single-transaction \ # InnoDB 一致性快照,不锁表(关键!)
--master-data=2 \ # 在注释里记录当前 binlog 位置
--flush-logs \ # 刷新 binlog,便于增量管理
--routines \ # 包含存储过程和函数
--triggers \ # 包含触发器
--events \ # 包含事件调度
--hex-blob \ # BLOB 字段用十六进制,避免编码问题
-h 127.0.0.1 -u backup_user -p \
--all-databases \
| gzip > /backup/full_$(date +%Y%m%d_%H%M%S).sql.gz
指定库备份:
mysqldump \
--single-transaction \
--master-data=2 \
-h 127.0.0.1 -u backup_user -p \
myapp_db \
| gzip > /backup/myapp_$(date +%Y%m%d).sql.gz
恢复:
# 解压并恢复
gunzip < /backup/full_20260411.sql.gz | mysql -h 127.0.0.1 -u root -p
# 恢复指定库
gunzip < /backup/myapp_20260411.sql.gz | mysql -h 127.0.0.1 -u root -p myapp_db
# 查看 binlog 位置(用于后续 PITR)
zcat /backup/full_20260411.sql.gz | grep "CHANGE MASTER"
# 输出类似:-- CHANGE MASTER TO MASTER_LOG_FILE='binlog.000123', MASTER_LOG_POS=4567890;
备份用户权限最小化#
CREATE USER 'backup_user'@'127.0.0.1' IDENTIFIED BY 'strong_password';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, RELOAD, REPLICATION CLIENT ON *.* TO 'backup_user'@'127.0.0.1';
-- 如果需要备份 --master-data
GRANT SUPER ON *.* TO 'backup_user'@'127.0.0.1';
FLUSH PRIVILEGES;
XtraBackup:生产环境的物理热备#
为什么选 XtraBackup#
XtraBackup 是 Percona 开发的 InnoDB 热备工具,核心优势:
- 不锁表:备份过程中数据库正常服务(对 InnoDB 表,MyISAM 需要短暂锁)
- 速度快:物理拷贝而非逻辑导出,100GB 数据 mysqldump 可能要几小时,XtraBackup 通常 30 分钟内
- 支持增量备份:基于 InnoDB 的 LSN(Log Sequence Number)
- 流式备份:可以直接流到远端,无需本地临时存储
安装#
# Percona XtraBackup 8.0(适配 MySQL 8.0)
# Ubuntu/Debian
apt install percona-xtrabackup-80
# CentOS/RHEL
yum install percona-xtrabackup-80
全量备份流程#
执行备份:
xtrabackup \
--backup \
--target-dir=/backup/full_$(date +%Y%m%d) \
--user=backup_user \
--password=strong_password \
--host=127.0.0.1 \
--compress \ # 压缩,节省空间
--compress-threads=4 \
--parallel=4 # 并行拷贝线程数
准备阶段(apply-log):
备份完成后还不能直接用于恢复,需要 apply 备份时的 redo log,使数据达到一致性状态:
xtrabackup \
--prepare \
--target-dir=/backup/full_20260411
恢复:
# 停止 MySQL
systemctl stop mysql
# 清空数据目录(或备份原数据)
mv /var/lib/mysql /var/lib/mysql_old
# 拷贝备份文件到数据目录
xtrabackup \
--copy-back \
--target-dir=/backup/full_20260411
# 修正权限
chown -R mysql:mysql /var/lib/mysql
# 启动 MySQL
systemctl start mysql
增量备份#
# 第一步:做全量备份(每周日)
xtrabackup --backup \
--target-dir=/backup/full_sunday \
--user=backup_user --password=xxx
# 第二步:做增量备份(后续每天)
xtrabackup --backup \
--target-dir=/backup/inc_monday \
--incremental-basedir=/backup/full_sunday \ # 基于哪个备份做增量
--user=backup_user --password=xxx
# 第三天增量,基于前一天的增量
xtrabackup --backup \
--target-dir=/backup/inc_tuesday \
--incremental-basedir=/backup/inc_monday \
--user=backup_user --password=xxx
增量备份的 prepare 流程(注意顺序):
# 1. prepare 全量(不提交,因为还要合并增量)
xtrabackup --prepare --apply-log-only --target-dir=/backup/full_sunday
# 2. 合并第一天增量
xtrabackup --prepare --apply-log-only \
--target-dir=/backup/full_sunday \
--incremental-dir=/backup/inc_monday
# 3. 合并第二天增量(最后一个不加 --apply-log-only)
xtrabackup --prepare \
--target-dir=/backup/full_sunday \
--incremental-dir=/backup/inc_tuesday
基于 binlog 的时间点恢复(PITR)#
前提条件#
MySQL 必须开启 binlog:
# my.cnf
[mysqld]
server-id = 1
log_bin = /var/log/mysql/binlog
binlog_format = ROW # 推荐 ROW 格式,记录行变化而非语句
expire_logs_days = 14 # binlog 保留天数(MySQL 8.0 用 binlog_expire_logs_seconds)
binlog_expire_logs_seconds = 1209600 # 14 天(MySQL 8.0)
max_binlog_size = 1G
PITR 实战流程#
场景:今天 14:30 有人误执行了 DELETE FROM orders WHERE 1=1,需要恢复到 14:29:59 的状态。
步骤一:找到最近的全量备份和 binlog 位置
# 从全量备份的 xtrabackup_info 获取备份时的 binlog 位置
cat /backup/full_20260411/xtrabackup_info | grep binlog_pos
# 输出:binlog_pos = filename 'binlog.000123', position '12345678'
步骤二:恢复全量备份(参考前文 XtraBackup 恢复流程)
步骤三:从 binlog 提取全量备份后到故障点之前的 SQL
# 查看 binlog 文件列表
mysqlbinlog --no-defaults /var/log/mysql/binlog.index
# 提取指定时间范围的 binlog(从全量备份时间到故障时间)
mysqlbinlog \
--no-defaults \
--start-datetime="2026-04-11 02:00:00" \ # 全量备份完成时间
--stop-datetime="2026-04-11 14:29:59" \ # 故障发生前一秒
--database=myapp_db \
/var/log/mysql/binlog.000123 \
/var/log/mysql/binlog.000124 \
> /tmp/recovery.sql
或者基于 binlog position(更精确):
mysqlbinlog \
--no-defaults \
--start-position=12345678 \ # 全量备份时的 position
--stop-datetime="2026-04-11 14:29:59" \
/var/log/mysql/binlog.000123 \
/var/log/mysql/binlog.000124 \
> /tmp/recovery.sql
步骤四:重放 binlog
mysql -u root -p myapp_db < /tmp/recovery.sql
步骤五:验证数据
-- 确认 orders 表数据已恢复
SELECT COUNT(*) FROM orders;
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
GTID 模式下的 PITR#
如果启用了 GTID(MySQL 5.6+),mysqlbinlog 命令有所不同:
# GTID 模式,跳过特定事务
mysqlbinlog \
--no-defaults \
--exclude-gtids="a1b2c3d4-...:1-1000" \ # 跳过全量备份前的事务
--stop-datetime="2026-04-11 14:29:59" \
/var/log/mysql/binlog.000123 \
> /tmp/recovery.sql
# 恢复时需要跳过 GTID 检查
mysql -u root -p -e "SET @@GLOBAL.GTID_PURGED='a1b2c3d4-...:1-1000';"
mysql -u root -p myapp_db < /tmp/recovery.sql
AWS RDS 的备份机制#
如果数据库跑在 AWS RDS,备份机制有一些重要差异。
自动备份 vs 手动快照#
自动备份:
- 开启后,RDS 在每天的备份窗口(默认随机,可指定)做全量快照
- 同时持续备份 binlog(transaction logs),支持 PITR 到秒级
- 保留期可设置 1-35 天,超期自动删除
- 数据库实例删除时自动备份会被删除(除非创建 final snapshot)
手动快照:
- 手动触发,不受保留期限制,除非手动删除
- 适合重大变更前(部署新版本、做数据迁移)
- 跨区域复制快照,实现异地容灾
# AWS CLI 创建手动快照
aws rds create-db-snapshot \
--db-instance-identifier myapp-prod-db \
--db-snapshot-identifier myapp-prod-before-migration-20260411 \
--region us-west-2
# 查看快照状态
aws rds describe-db-snapshots \
--db-snapshot-identifier myapp-prod-before-migration-20260411 \
--query 'DBSnapshots[0].Status'
RDS PITR#
RDS 的时间点恢复会创建新的 RDS 实例(不是原地恢复):
# 恢复到指定时间点(会创建新实例)
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier myapp-prod-db \
--target-db-instance-identifier myapp-prod-db-restored \
--restore-time 2026-04-11T14:29:59Z \
--db-instance-class db.r6g.large \
--availability-zone us-west-2a
注意:PITR 最早只能恢复到最老的自动备份时间点,不能超出保留期。
备份验证:定期恢复演练#
备份的价值只有在成功恢复时才被证明。我见过很多团队有备份但从未验证过,直到真正需要用的时候才发现问题。
自动化恢复验证脚本#
#!/bin/bash
# backup_verify.sh - 每周自动恢复验证
set -e
BACKUP_DIR="/backup"
RESTORE_HOST="restore-test.internal"
RESTORE_DB="myapp_db_verify"
ALERT_WEBHOOK="https://hooks.example.com/alert"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
notify() {
curl -s -X POST "$ALERT_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": \"$1\"}"
}
# 找最新的全量备份
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/full_* | head -1)
log "使用备份: $LATEST_BACKUP"
# 恢复到测试实例
log "开始恢复..."
xtrabackup --prepare --target-dir="$LATEST_BACKUP" 2>/dev/null
xtrabackup --copy-back --target-dir="$LATEST_BACKUP" \
--datadir=/var/lib/mysql_test 2>/dev/null
# 启动测试 MySQL 实例
mysqld_safe --defaults-file=/etc/mysql/mysql_test.cnf \
--datadir=/var/lib/mysql_test &
sleep 10
# 验证数据完整性
RESULT=$(mysql -h "$RESTORE_HOST" -u verify_user -p"$VERIFY_PASS" \
-e "SELECT COUNT(*) FROM ${RESTORE_DB}.orders;" 2>/dev/null)
if [[ -z "$RESULT" ]]; then
log "ERROR: 恢复验证失败"
notify "ALERT: MySQL 备份恢复验证失败!备份: $LATEST_BACKUP"
exit 1
fi
ROW_COUNT=$(echo "$RESULT" | tail -1)
log "验证成功,orders 表行数: $ROW_COUNT"
notify "INFO: MySQL 备份验证通过,orders 行数: $ROW_COUNT,备份: $LATEST_BACKUP"
# 清理测试实例
mysqladmin -h "$RESTORE_HOST" shutdown 2>/dev/null
rm -rf /var/lib/mysql_test
log "验证完成"
在生产环境里,这个脚本建议每周跑一次,结果发到告警渠道。
踩坑记录#
坑1:mysqldump 不加 –single-transaction 导致锁表#
现象:备份期间生产数据库出现大量锁等待,慢查询激增,应用报错。
原因:mysqldump 默认会对每张表执行 LOCK TABLES,再进行导出。对于有大量并发写入的 InnoDB 表,这会持锁几十秒甚至几分钟。
解决:InnoDB 表必须加 --single-transaction,它利用 InnoDB 的 MVCC 机制,在不锁表的情况下获取一致性快照。
# 错误写法(会锁表)
mysqldump -u root -p myapp_db > backup.sql
# 正确写法
mysqldump --single-transaction -u root -p myapp_db > backup.sql
注意:--single-transaction 只对 InnoDB 有效,如果有 MyISAM 表,还是需要 --lock-tables(默认开启)。两者互斥,所以混合引擎的库没有完美的无锁备份方案——这也是迁移到纯 InnoDB 的理由之一。
坑2:GTID 模式下恢复报错#
现象:恢复 mysqldump 文件时报错:
ERROR 1839 (HY000): @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is empty.
原因:mysqldump 的备份文件里有 SET @@GLOBAL.GTID_PURGED=... 语句,但目标实例的 gtid_executed 不为空(比如这个实例已经运行过一些事务)。
解决:
方法一:恢复前重置 GTID 状态(会清空所有 GTID 信息,谨慎):
RESET MASTER;
方法二:导出时加 --set-gtid-purged=OFF,跳过 GTID 设置,适合只恢复部分数据到已有实例:
mysqldump --set-gtid-purged=OFF --single-transaction \
-u root -p myapp_db > backup_no_gtid.sql
方法三:手动编辑备份文件,删除 SET @@GLOBAL.GTID_PURGED 那行(对于大文件可以用 sed):
zcat backup.sql.gz | grep -v "GTID_PURGED" | mysql -u root -p myapp_db
坑3:XtraBackup 恢复后 MySQL 启动失败#
现象:copy-back 完成后,systemctl start mysql 失败,日志里:
[ERROR] InnoDB: Cannot open file '/var/lib/mysql/ib_logfile0'. OS error: 13
原因:copy-back 后没有修正文件权限,文件属于 root 而不是 mysql 用户。
解决:
# copy-back 之后必须执行这步
chown -R mysql:mysql /var/lib/mysql
# 检查 SELinux 是否也在拦截
ls -laZ /var/lib/mysql/ | head -5
restorecon -R /var/lib/mysql/ # 恢复 SELinux 上下文
坑4:binlog 位置对不上#
现象:PITR 时找不到全量备份对应的 binlog 文件,或者文件存在但 position 之前的内容已被 rotate 清理。
解决:
- binlog 保留时间设置要比备份周期长,比如全量备份每周一次,binlog 至少保留 14 天
- binlog 文件要定期同步到对象存储(S3/OSS),不能只存在数据库机器本地
- 全量备份完成后,立即记录当前 binlog 位置并入档
# 备份完成后,记录当前 binlog 位置
mysql -u root -p -e "SHOW MASTER STATUS\G" >> /backup/full_$(date +%Y%m%d)/binlog_pos.txt
备份存储和安全#
3-2-1 原则#
- 3 份数据副本
- 2 种不同存储介质
- 1 份异地存储
实践上:本地磁盘 + 同区域 S3 + 跨区域 S3 复制,能覆盖大部分故障场景。
备份加密#
# 备份时加密(使用 openssl)
mysqldump --single-transaction -u root -p myapp_db \
| gzip \
| openssl enc -aes-256-cbc -salt -k "$BACKUP_ENCRYPTION_KEY" \
> /backup/myapp_$(date +%Y%m%d).sql.gz.enc
# 解密恢复
openssl enc -d -aes-256-cbc -k "$BACKUP_ENCRYPTION_KEY" \
< /backup/myapp_20260411.sql.gz.enc \
| gunzip \
| mysql -u root -p myapp_db
密钥不要存在备份文件旁边,存 AWS Secrets Manager 或 HashiCorp Vault。
一张决策表#
| 场景 | 推荐工具 | 理由 |
|---|---|---|
| 数据量 < 5GB,允许短暂锁 | mysqldump | 简单,无需额外安装 |
| 数据量 5-100GB,生产在线 | XtraBackup | 热备,速度快 |
| 数据量 > 100GB | XtraBackup + 流式压缩 | 减少本地存储依赖 |
| 需要时间点恢复 | 任意全量 + binlog | 两者配合 |
| 跨版本迁移 | mysqldump | 逻辑格式,版本无关 |
| 托管 RDS | 自动备份 + 手动快照 | 云托管,无需自建 |
备份这件事,最重要的不是选哪个工具,而是把备份和恢复当成一个持续运行的系统来维护:自动化执行、监控是否成功、定期演练恢复流程。没有演练过的备份,只是一个心理安慰。






