跳过正文
MySQL 备份与恢复实战:从 mysqldump 到 XtraBackup 的完整方案

MySQL 备份与恢复实战:从 mysqldump 到 XtraBackup 的完整方案

·1157 字·6 分钟·
目录

为什么备份策略值得认真设计
#

数据库备份是那种「平时用不到,用到的时候不能出错」的东西。很多团队的备份是有的,但真正到恢复的时候才发现:备份文件损坏、恢复流程没人走过、时间点恢复的 binlog 对不上。

我见过的最惨的案例:一个团队每天做 mysqldump 备份,某次误操作删了核心表的数据,去恢复才发现 mysqldump 命令写错了参数,三个月来备份文件都是空的。

好的备份方案有三个要素:能备上、能恢复、定期验证过


备份策略设计
#

全量 + 增量 + 二进制日志
#

一个生产级 MySQL 备份策略通常有三层:

全量备份(每周/每天)
    ↓
增量备份(每天/每小时)
    ↓
二进制日志(binlog)连续归档

全量备份:完整的数据快照,恢复起点。大数据量下(50GB+)用 XtraBackup 热备,小数据量(<10GB)用 mysqldump 也可以。

增量备份:基于上次全量或增量的变化量,减少存储和备份时间。XtraBackup 支持真正的增量备份(基于 LSN)。

binlog 归档:记录所有 DDL 和 DML,是时间点恢复(PITR)的基础。应该实时或近实时同步到安全位置。

RTO(恢复时间目标)和 RPO(恢复点目标)决定了策略的具体参数:

场景RPORTO策略
核心业务数据库< 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 清理。

解决

  1. binlog 保留时间设置要比备份周期长,比如全量备份每周一次,binlog 至少保留 14 天
  2. binlog 文件要定期同步到对象存储(S3/OSS),不能只存在数据库机器本地
  3. 全量备份完成后,立即记录当前 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热备,速度快
数据量 > 100GBXtraBackup + 流式压缩减少本地存储依赖
需要时间点恢复任意全量 + binlog两者配合
跨版本迁移mysqldump逻辑格式,版本无关
托管 RDS自动备份 + 手动快照云托管,无需自建

备份这件事,最重要的不是选哪个工具,而是把备份和恢复当成一个持续运行的系统来维护:自动化执行、监控是否成功、定期演练恢复流程。没有演练过的备份,只是一个心理安慰。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

MySQL 深度调优:从 Buffer Pool 到锁等待的生产手册

·1517 字·8 分钟
你有没有过这种体验:按网上教程把 innodb_buffer_pool_size 调到 75%、关了 query cache、打开了 innodb_file_per_table,然后告诉自己"MySQL 调优就这样了"?真正的调优是一个持续观察、假设、验证、回滚的过程。这篇文章把我在过去几年维护的十几套 MySQL 实例上积累的调参经验整理出来,每一条都能追到具体指标和业务效果。

PostgreSQL 膨胀治理:把 autovacuum 调到你真正需要的样子

·1457 字·7 分钟
大部分 PostgreSQL DBA 对 autovacuum 的理解停留在"它会自己跑",但一旦膨胀起来才发现:默认参数对现代硬件完全不够用,几十个 autovacuum_* 参数各管一摊,出了问题根本不知道从哪儿看。这篇文章把我在几套 PG 集群上治理膨胀的经验整理出来,从 MVCC 原理讲到参数调优、从监控到应急处置。