MySQL数据库备份与恢复实战:全量备份、增量备份与恢复验证

有一句运维圈的玩笑话很实在:"没有做过恢复演练的备份,等于没有备份。"真实的灾难往往发生在半年没关注过备份的那个人值班的夜里:磁盘阵列坏了、rm -rf 执行了、或者一条没有 WHERE 的 UPDATE 成功了。备份方案的价值不在"有没有",而在 RTO(恢复时长目标)和 RPO(可容忍的数据丢失量)能不能达标。本文围绕 mysqldump、Percona XtraBackup 与 MySQL 8.0 的 Clone 插件三条主线,给出可直接使用的命令、增量/时间点恢复流程与一套恢复验证方法。

一、先选方案:逻辑备份还是物理备份

方案类型备份速度恢复速度优点局限
mysqldump逻辑慢(单线程)慢(需重放 SQL)自带、跨版本兼容、可筛表筛数据、文本可 grep大库耗时长;不含 binlog 位置时需额外记录
mydumper逻辑快(多线程)快(myloader 多线程)多线程、输出压缩、一致性快照需额外安装
XtraBackup(PXB)物理快(文件拷贝)快(prepare 后直接用)支持增量、支持压缩加密、不停机版本需与 MySQL 匹配;只能整机/整实例
Clone Plugin(8.0.17+)物理快快官方内置、可远程克隆给新实例克隆期间捐赠者只读,且目标实例会被清空
LVM / 存储快照物理极快快对数据库几乎无侵入需 LVM 或支持快照的存储

一个实用的选择基准:数据量小于 50GB 用 mysqldump 足够简单;50GB 到 1TB 强烈建议 Percona XtraBackup 或 mydumper;TB 级且追求分钟级 RTO 需要考虑物理备份加延迟从库组合。

二、mysqldump 实战

2.1 一条标准的全量备份命令

mysqldump -h127.0.0.1 -uroot -p'密码' \
  --single-transaction \
  --master-data=2 \
  --routines --triggers --events \
  --default-character-set=utf8mb4 \
  --hex-blob \
  --flush-logs \
  --set-gtid-purged=ON \
  --all-databases | gzip > /backup/full_$(date +\%F).sql.gz

逐个解释关键选项:

  • --single-transaction:对 InnoDB 开启一个一致性读事务,全程不加表锁,业务可读可写。这是热备份的基础。注意:它只对 InnoDB 有效,如果库里还有 MyISAM 表,必须配合 --lock-all-tables,否则非事务表的数据会不一致。
  • --master-data=2:在输出文件里以注释形式写入 CHANGE MASTER TO MASTER_LOG_FILE=..., MASTER_LOG_POS=...,搭建从库或做时间点恢复时全靠它。=1 是不注释的可执行语句(导入时会自动 change master),一般只用 2。
  • --routines / --triggers / --events:默认不带存储过程、触发器、事件调度器,漏了会导致恢复后业务报错。
  • --set-gtid-purged=ON:启用 GTID 的实例建议保留(默认 AUTO),如果只想做数据迁移不想保留 GTID 信息则用 OFF。
  • --hex-blob:二进制字段以十六进制输出,避免特殊字符导致导入失败。
  • --flush-logs:切换 binlog,让备份文件与 binlog 的边界更清晰。

2.2 常见的其他用法

# 只导出一张表的部分数据(按条件分片导出)
mysqldump -uroot -p appdb orders --where="id > 100000 AND id <= 200000" --single-transaction > orders_part2.sql

# 只导出表结构,不含数据
mysqldump -uroot -p --no-data appdb > appdb_schema.sql

# 导出时不锁表且不想记录 binlog 位置(纯迁移场景)
mysqldump -uroot -p --single-transaction --set-gtid-purged=OFF --master-data=0 appdb > appdb.sql

# crontab 定时全备(注意 % 在 crontab 中要转义)
0 2 * * * /usr/bin/mysqldump -uroot -p'密码' --single-transaction --master-data=2 \
  --routines --triggers --events -A | gzip > /backup/full_$(date +\%F).sql.gz

2.3 恢复

# 压缩备份
zcat /backup/full_2025-01-01.sql.gz | mysql -uroot -p
# 或先解压再导入
gunzip < /backup/full_2025-01-01.sql.gz | mysql -uroot -p appdb

# 大文件导入提速(会话级关闭双一)
mysql -uroot -p -e "SET GLOBAL innodb_flush_log_at_trx_commit=0; SET GLOBAL sync_binlog=0; SET GLOBAL foreign_key_checks=0;"
mysql -uroot -p < /backup/full_2025-01-01.sql
mysql -uroot -p -e "SET GLOBAL innodb_flush_log_at_trx_commit=1; SET GLOBAL sync_binlog=1; SET GLOBAL foreign_key_checks=1;"

# 查看备份文件里记录的 binlog 位置
zcat /backup/full_2025-01-01.sql.gz | head -30 | grep -i "CHANGE MASTER"

三、Percona XtraBackup:物理全备与增量

XtraBackup 通过拷贝 InnoDB 数据文件并回放 redo log 来保证一致性,备份速度接近文件复制速度,是 TB 级库的首选。

3.1 全量备份

# 创建专用备份账号(最小权限)
CREATE USER 'xtra'@'localhost' IDENTIFIED BY 'Str0ng_P@ss';
GRANT BACKUP_ADMIN, PROCESS, RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'xtra'@'localhost';
GRANT SELECT ON performance_schema.* TO 'xtra'@'localhost';

# 全量备份
xtrabackup --backup \
  --user=xtra --password='Str0ng_P@ss' \
  --target-dir=/backup/xtra/full_$(date +\%F) \
  --parallel=4 --compress --compress-threads=4

# prepare:把备份里的 redo 应用到数据文件,使其达到一致状态(必须做)
xtrabackup --prepare --target-dir=/backup/xtra/full_2025-01-01

# 恢复:目标数据目录必须为空,且 mysqld 已停止
systemctl stop mysqld
rm -rf /var/lib/mysql/*
xtrabackup --copy-back --target-dir=/backup/xtra/full_2025-01-01
chown -R mysql:mysql /var/lib/mysql
systemctl start mysqld

3.2 增量备份

增量基于已有的全备(或上一个增量),只拷贝变化的页:

# 周一全备
xtrabackup --backup --user=xtra --password='Str0ng_P@ss' --target-dir=/backup/xtra/base

# 周二、周三增量
xtrabackup --backup --user=xtra --password='Str0ng_P@ss' \
  --target-dir=/backup/xtra/inc1 --incremental-basedir=/backup/xtra/base

xtrabackup --backup --user=xtra --password='Str0ng_P@ss' \
  --target-dir=/backup/xtra/inc2 --incremental-basedir=/backup/xtra/inc1

# 恢复时的 prepare 顺序:除最后一个增备外都要加 --apply-log-only
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtra/base
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtra/base --incremental-dir=/backup/xtra/inc1
xtrabackup --prepare --target-dir=/backup/xtra/base --incremental-dir=/backup/xtra/inc2   # 最后一个不加 apply-log-only

# 然后 copy-back
systemctl stop mysqld
rm -rf /var/lib/mysql/*
xtrabackup --copy-back --target-dir=/backup/xtra/base
chown -R mysql:mysql /var/lib/mysql && systemctl start mysqld

--apply-log-only 的作用是"只应用 redo,不做未完成事务的回滚",因为后续还要叠加新的增量。忘记加会导致增量无法继续应用;最后一个 prepare 必须不加这个参数,否则崩溃恢复阶段不会完成。

四、基于 binlog 的时间点恢复(PITR)

"误删了一张表"或"UPDATE 忘了带 WHERE"是最常见的人为事故。这时候全量备份加 binlog 可以精确恢复到出事前一秒。

# 1) 先确认当前还保留着哪些 binlog
mysql -uroot -p -e "SHOW BINARY LOGS;"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';"   # 8.0 起以此参数控制保留时长
mysql -uroot -p -e "SHOW VARIABLES LIKE 'expire_logs_days';"            # 5.7 及以前

# 2) 找到误操作的位置(GTID 或 pos)
mysqlbinlog --base64-output=decode-rows -v /var/lib/mysql/mysql-bin.000042 | grep -n -B5 -A10 "DROP TABLE"
mysqlbinlog --start-datetime="2025-01-10 10:00:00" --stop-datetime="2025-01-10 10:20:00" \
  --base64-output=decode-rows -v /var/lib/mysql/mysql-bin.000042 > /tmp/binlog_read.txt

# 3) 恢复到一台临时实例,而不是直接动生产
#    先恢复全备,再把 binlog 回放到一个时间段之前
mysqlbinlog --start-position=4 --stop-position=873210 /var/lib/mysql/mysql-bin.000042 | mysql -uroot -p

# 4) GTID 环境下更推荐按 GTID 集合回放
mysqlbinlog --include-gtids='3E11FA47-71CA-11E1-9E33-C80AA9429562:1-8932' \
  /var/lib/mysql/mysql-bin.000042 | mysql -uroot -p

关键点:永远不要在生产实例上直接回放。正确做法是找一台空白实例恢复全备 + binlog,核对无误后,用 mysqldump 或 pt-table-sync 把目标表捞回生产。

五、恢复验证怎么做:隔离恢复、一致性校验与 RTO 口径

恢复演练的原则只有一条:永远不要在生产实例上验证。正确做法是把备份恢复到一台隔离实例上,确认无误后再决定如何把数据捞回生产。

5.1 用隔离实例做恢复

隔离实例要满足三个条件:独立的数据目录、独立的端口与 socket、以及不允许业务连接。物理恢复时建议先用 --skip-networking 启动,彻底关闭 TCP 监听,只留 socket 供本机验证;同时关闭 binlog(--skip-log-bin)避免这次恢复的写入污染 binlog 或被误传到别的从库。

# 1) 准备一个空目录
mkdir -p /verify/3307/{data,log,tmp}
chown -R mysql:mysql /verify

# 2) 恢复 XtraBackup 备份
xtrabackup --copy-back --target-dir=/backup/xtra/base --datadir=/verify/3307/data
chown -R mysql:mysql /verify/3307/data

# 3) 以隔离方式启动:独立 port/socket、关 TCP、关 binlog、跳过主从自动启动
mysqld --no-defaults \
  --datadir=/verify/3307/data \
  --port=3307 --socket=/verify/3307/mysql.sock --pid-file=/verify/3307/mysqld.pid \
  --skip-networking --skip-log-bin --skip-slave-start \
  --log-error=/verify/3307/log/error.log \
  --innodb_buffer_pool_size=4G &

# 4) 用 socket 登录验证
mysql -S /verify/3307/mysql.sock -uroot -p -e "SELECT VERSION(); SHOW DATABASES;"

# 5) 逻辑备份的隔离导入同理
mysql -S /verify/3307/mysql.sock -uroot -p < /backup/full_2025-01-01.sql

5.2 恢复后的一致性校验

实例起来了不代表数据正确。至少要做三层校验:对象层(表、视图、存储过程、触发器、事件的数量与生产一致)、行数层(关键业务表的粗校验)、内容层(CHECKSUM TABLE 比对。注意 InnoDB 的 table_rows 是采样估算值,只能做粗校验,精确到行数要用 COUNT(*))。最强的校验是把它挂成从库,用 pt-table-checksum 做全表比对。

# 对象层:表数量核对
SELECT table_schema, COUNT(*) AS tables FROM information_schema.tables
WHERE table_schema NOT IN ('mysql','sys','performance_schema','information_schema')
GROUP BY table_schema;

# 对象层:存储过程、函数、触发器、事件不能漏(mysqldump 需要 --routines --triggers --events)
SELECT COUNT(*) FROM information_schema.routines WHERE routine_schema NOT IN ('sys','mysql');
SELECT COUNT(*) FROM information_schema.triggers;
SHOW EVENT STATUS\G

# 行数层:InnoDB 的 table_rows 为估算,只做粗校验
SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='appdb';
# 精确校验请 COUNT(*)
SELECT COUNT(*) FROM appdb.orders;

# 内容层
CHECKSUM TABLE appdb.orders;
CHECK TABLE appdb.orders;

# 最强校验:把它挂成从库后做全表一致性比对
pt-table-checksum --user=root --password='密码' --databases=appdb

5.3 RTO 与 RPO 的计时口径

演练记录必须写明从什么时刻开始算,否则数据无法横向比较。建议统一口径:

  • RTO(Recovery Time Objective):从"决定执行恢复"开始计时,到"业务可以正常读写该实例"为止。中间包含:申请/准备机器、传输备份文件、xtrabackup --prepare 或 SQL 导入、启动与实例自检、业务校验。很多团队只统计导入时间,导致 RTO 被严重低估。
  • RPO(Recovery Point Objective):恢复完成后丢失的数据量对应的时长,等于"最后一次可用备份与最后一次成功事务之间的时间差"。--master-data=2 记录的 binlog 位置与磁盘上残留的 binlog 决定了 RPO 下限。
  • 每次演练都要记录:备份文件大小、传输耗时、prepare 耗时、导入耗时、总耗时、数据量。随着数据增长,这些数字应当定期复测(建议每季度一次)。
  • 经验参考:逻辑备份的恢复耗时通常是备份耗时的 3-10 倍;物理备份的恢复耗时接近文件拷贝时间加 prepare 时间。这也是大库优先选物理备份的核心原因。

5.4 增量合并命令复核

验证带增量的备份集时,--apply-log-only 的顺序是整个流程最容易出错的地方,标准命令序列如下:

# base = 全备;inc1、inc2 = 两次增量,inc2 是最后一个
# 1) 先对全备做"仅应用日志",不做未完成事务的回滚
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtra/base

# 2) 逐个叠加增量(除最后一个外,都要带 --apply-log-only)
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtra/base \
           --incremental-dir=/backup/xtra/inc1

# 3) 最后一个增量:不加 --apply-log-only,让它完成崩溃恢复
xtrabackup --prepare --target-dir=/backup/xtra/base \
           --incremental-dir=/backup/xtra/inc2

# 4) 恢复到隔离实例
systemctl stop mysqld
rm -rf /verify/3307/data/*
xtrabackup --copy-back --target-dir=/backup/xtra/base --datadir=/verify/3307/data
chown -R mysql:mysql /verify/3307/data

如果第 2 步忘记加 --apply-log-only,后续增量将无法继续叠加(XtraBackup 会提示 LSN 不匹配);如果第 3 步也加了,实例启动时会多做一次崩溃恢复,多数情况下能自愈但不可依赖。另外注意 XtraBackup 版本必须与 MySQL 大版本匹配(MySQL 8.0 用 PXB 8.0,MySQL 5.7 用 PXB 2.4)。

5.5 演练执行清单

  1. 每周至少做一次真实恢复演练,把备份恢复到一台与生产同规格的隔离实例。
  2. 启动后检查错误日志有没有 InnoDB 崩溃恢复相关的告警:grep -i 'crash recovery\|corrupt' /var/log/mysqld.log。
  3. 核对对象数量:表数、视图数、存储过程数、触发器数、事件数,与生产 information_schema 对比。
  4. 核对数据一致性:对关键表跑计数和 checksum。
  5. 启动复制验证:用备份文件里的 CHANGE MASTER 位置把这台实例挂成从库,看是否有 1032/1062 报错。
  6. 记录本次恢复的真实耗时,与 RTO 目标对比,作为扩容或改方案的依据。
  7. 把演练结果(成功/失败、耗时、发现的问题)写进运维周报,形成闭环。
# 数据量核对
SELECT table_schema, COUNT(*) AS tables FROM information_schema.tables
WHERE table_schema NOT IN ('mysql','sys','performance_schema','information_schema')
GROUP BY table_schema;

# 表行数核对(InnoDB 的 table_rows 是采样估算,只做粗校验)
SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='appdb';

# 关键表 checksum 校验
CHECKSUM TABLE appdb.orders;
CHECK TABLE appdb.orders;

# 复制一致性更强校验:pt-table-checksum / pt-table-sync
pt-table-checksum --user=root --password='密码' --databases=appdb

六、备份文件的存放、加密与常见故障

备份本身也是一种资产,同样需要保护。最常见的问题是"备份就在数据库本机那块盘上"——磁盘坏了,数据和备份一起丢失。至少要遵循 3-2-1 原则:三份副本、两种不同介质、一份异地。

6.1 落地规范

  • 异地拷贝:全备完成后立刻推到对象存储或另一机房的备份机,用 rsync / rclone 均可,关键是要记录传输结果并对 md5 做校验。
  • 保留策略:建议保留最近 7 天日备、最近 4 周周备、最近 3 个月月备;策略要写成脚本自动清理,不要靠人工删。
  • 权限与加密:备份文件包含全部业务数据,必须 chmod 600;含敏感字段的建议用 openssl 或 gpg 加密后再上传,密钥单独存放。
  • 不要把密码写在命令行里:环境变量或 ~/.my.cnf(权限 600)里的 [client] 段更安全,避免被 ps 或 shell history 看到。
# 用配置文件免密,避免明文出现在命令行
cat > ~/.my.cnf << 'CNF'
[client]
user=root
password=Str0ng_P@ss
CNF
chmod 600 ~/.my.cnf

# 备份后做校验并同步到异地
mysqldump --single-transaction --master-data=2 -A | gzip > /backup/full_$(date +\%F).sql.gz
md5sum /backup/full_$(date +\%F).sql.gz > /backup/full_$(date +\%F).md5
rsync -avz --password-file=/etc/rsync.pass /backup/ backup@10.0.9.9::bak/mysql/

6.2 三类典型故障

现象原因处理
mysqldump 报 Table doesn't exist备份过程中另一会话做了 DROP/RENAME,--single-transaction 的一致性快照不含该表重做备份;DDL 避开备份窗口
从库复制报 1236 Could not find first log file主库 binlog 已被 Purge,从库要的位置不存在用最新全备重建从库;调长 binlog_expire_logs_seconds
XtraBackup 报 version mismatchXtraBackup 版本与 MySQL 版本不匹配(如 2.4 无法备份 8.0)MySQL 8.0 用 PXB 8.0;5.7 用 PXB 2.4

6.3 备份期间为什么业务会卡

逻辑备份会执行大量 SELECT,可能把 buffer pool 里的热点页挤出去,导致业务在读高峰期出现抖动;物理备份则是文件级的数据文件拷贝,加上 XtraBackup 在收尾时对 redo 的处理,也会有一定 IO 压力。解决办法是把备份窗口挪到业务低峰,限速限并发(XtraBackup 的 --throttle、--parallel,mydumper 的 --threads、--rows),并且优先在从库上做备份,主库只在万不得已时才承担备份任务。

小结

备份方案的设计起点是 RTO 与 RPO,而不是工具本身:50GB 以内的库用 mysqldump --single-transaction --master-data=2 就能应付;更大的库应该上 XtraBackup 并配周期性的增量 + binlog 做 PITR;痛点在"慢备份恢复"时再考虑 mydumper 和 Clone 插件。避坑清单:不要把备份和数据库放在同一块盘;不要以为 --single-transaction 能保护 MyISAM 表;不要把 binlog_expire_logs_seconds(或 expire_logs_days)设得太短,否则想恢复时 binlog 已经被清理;不要在 XtraBackup prepare 增量时漏掉 --apply-log-only;不要跳过恢复演练;不要给备份脚本里的明文密码设置 644 权限。遵循 3-2-1 原则(3 份副本、2 种介质、1 份异地),绝大多数灾难都能扛过去。