MySQL全量备份实战之从mysqldump到XtraBackup的选型与恢复方法
作者:王少冬
这是一份很值得认真对待的题目。做 MySQL 运维的人都知道一句话:备份是最后一道防线,这句话说了一万遍,但真正能把“全量备份”做得滴水不漏的人,其实不多。 很多人以为全量备份就是 mysqldump 一把梭,或者 xtrabackup 拷贝一下数据目录,跑完就算完事。但“中级”这两个字的分量在于:你不仅要会备份,还要懂为什么这么备、备份对业务有什么影响、出了故障如何快速恢复、自动化之后如何保证不出幺蛾子。这个层级,是日常开发工程师和专职 DBA 的分水岭。
这篇内容不会只停在命令层面,我会把全量备份的设计思路、工具选型、一致性原理、实操命令、恢复验证、自动化脚本、常见坑位都展开讲透。 无论你现在用的是 MySQL 5.7 还是 8.0,无论你是刚接手数据库维护的开发者,还是想系统补全备份体系的运维,这篇文章都应该能给你一套可以落地、可以复盘的完整参考。
1. 选型先行:逻辑备份还是物理备份,别等出事才后悔
1.1 备份模式怎么选:mysqldump 和 xtrabackup 的适用分界
接触全量备份,第一个绕不开的选择题就是:用 mysqldump 还是用 XtraBackup 。很多人觉得“既然都是全量备份,哪个顺手用哪个”,这种想法在数据量小的时候没问题,可一旦数据库进入以 GB 甚至 TB 计量的阶段,选错工具会让你在备份窗口和恢复时间上吃尽苦头。
mysqldump 属于逻辑备份。它做的事情是把表结构、数据、触发器等对象通过 SQL 语句的形式导出来,生成一个可读的 .sql 文件。它最大的优点是灵活、通用、跨版本迁移方便,你可以只导出某几张表,也可以只导出结构不带数据。缺点是备份和恢复速度都慢,因为要经过 SQL 层的解析和重放,而且对于 InnoDB 大表,导出过程会对 CPU 和 IO 造成明显压力。
XtraBackup 属于物理备份。它直接拷贝 InnoDB 的数据文件,中间还会利用 redo log 做一致性处理。它的核心优势是备份速度快、恢复速度快,对在线业务的影响相对小,尤其适合大容量数据库。缺点是工具本身有一定学习成本,备份集文件不像 .sql 那样直观,跨大版本恢复时需要更谨慎地评估兼容性。
两者的分界,我个人给的参考线是: 单实例数据量在 50GB 以内,业务允许在低峰期做逻辑导出,且你不需要分钟级恢复,mysqldump 完全够用;数据量超过 100GB,或者业务对恢复时间目标非常敏感,XtraBackup 才是正解。 如果你处于 50GB 到 100GB 之间的中间地带,那就看你对恢复窗口的要求,以及服务器 IO 能力是否充裕。
1.2 全量备份与增量备份的搭配逻辑
“中级”这个概念,还体现在你对备份体系的整体理解上,不是只盯全量。全量备份是整个备份体系的基座,但它通常不会单独存在,需要和 binlog 增量备份配合,才能实现“恢复到任意时间点”的能力。
全量备份解决的是“我有一个可用的基线”,binlog 解决的是“从基线到故障时刻之间的所有变更”。举个例子,你在凌晨 2 点做了一次全量备份,上午 11 点有人误删了一张核心表,那么恢复路径就是:先恢复凌晨 2 点的全量备份,再重放 2 点到 11 点之间的 binlog,跳过误删的那条语句。这个链路里,全量备份的完整性直接决定了恢复的上限。 全量备份如果做得不干净、不一致,后面 binlog 重放得再正确也白搭,因为根已经是坏的。
所以做全量备份时,脑子里要有“上下游”的概念:上游是数据变更记录(binlog),下游是恢复演练。备份文件生成之后,不光要归档保存,还要定期做恢复演练,验证备份链路是通的。关于恢复演练,后面我会单独用一节来讲,那是很多人忽略的重头戏。
2. 一致性原理:为什么备份时数据不会“半新半旧”
2.1 让备份一致性不再抽象:从 MVCC 和 redo log 说起
中级水平的一个标志,是你开始关心一致性问题。全量备份最怕的事情,是备份出来的数据“不伦不类”——有的表数据在时间点 A,有的表数据在时间点 B,中间还夹杂着事务只提交了一半的状态。这种备份即使能恢复,数据也是错乱的,而且很难被察觉,等到业务跑起来发现问题,已经晚了。
InnoDB 解决这个问题的机制,简单说可以归纳成两条线。一条线是 MVCC(多版本并发控制)。事务在读取数据时,通过 undo log 可以构建出某个历史版本的数据快照。另一条线是 redo log,它记录了所有物理变更,用于崩溃恢复时重放。 mysqldump 在做 InnoDB 备份时,如果你指定了 --single-transaction ,它会启动一个一致性的读事务,利用 MVCC 机制读取到的是一个固定快照,不受其他事务提交的影响。而 XtraBackup 走的是另一条路:它在拷贝数据文件的过程中,同时持续跟踪 redo log,等数据文件拷贝完成,再用 redo log 把备份集恢复到某个一致的时间点。
这个原理听起来有点绕,但你可以用一个生活化的类比来理解: MVCC 像是给整个数据库拍了一张“定格照片”,拍照瞬间所有事物都静止了,之后再发生什么变动跟这张照片无关;redo log 则像是一个录像机,XtraBackup 一边拍照一边录像,最后把录像回放一遍,确保所有画面的时间线对齐。
2.2 锁策略对比:FTWRL 和 --single-transaction 的真实代价
不同备份方式的一致性保障手段不同,对业务的影响也不同,这是选型时必须权衡的点。 mysqldump 如果不加 --single-transaction ,默认会对表执行 LOCK TABLES ,也就是在备份过程中锁住所有表,业务写入会全部阻塞。在低峰期、数据量小的情况下,这种方案还勉强可以用。
加了 --single-transaction 之后,InnoDB 表走的是 MVCC 快照,也就是非锁定读,业务照常写入。但是这里有两个坑:第一,这个参数只对 InnoDB 表生效,对于 MyISAM 表依然会加锁;第二,也是更隐蔽的——即使有 --single-transaction , mysqldump 依然会在开始时执行 FLUSH TABLES WITH READ LOCK (FTWRL)来获取一个全局一致的 binlog 位置信息,虽然这个锁持有时间很短,但如果此时存在长事务或大查询,FTWRL 会排队等待,进而阻塞所有写入。
XtraBackup 的锁处理更加克制。它通过后台线程复制 InnoDB 数据文件,加上 redo log 跟踪机制,全程不需要对 InnoDB 表加全局锁,只在最后短暂加锁获取 binlog 坐标信息。这就是为什么它能在 TB 级数据库上做到对业务影响最小。
实操中的经验是:无论用哪种工具,都要选择业务低峰期执行备份,并提前排查是否存在长时间未提交的事务。 否则一个看起来无伤大雅的 --single-transaction 参数,也可能因为长事务让备份期间所有写入卡死。这个问题在故障复盘里很常见。
3. 实操落地:一套可以抄作业的 MySQL 全量备份方案
3.1 基于 mysqldump 的全量逻辑备份标准命令与参数拆解
我给出一套在生产实践中验证过比较稳妥的逻辑备份命令,适用于 5.7 和 8.0,关键在于参数的取舍。
mysqldump -h127.0.0.1 -uBackupUser -p --set-gtid-purged=OFF \ --single-transaction --master-data=2 \ --routines --triggers --events \ --databases db_core db_order db_log \ --gtid-mode=OFF | gzip > backup_$(date +%F_%H-%M).sql.gz
逐个参数解释一下我为什么这么写:
--single-transaction 是必须加的,刚才已经说了,保证 InnoDB 一致性快照。 --master-data=2 会在备份文件中记录 binlog 文件名和偏移量,注释形式写入,方便恢复时确定从哪个 binlog 位置继续回放增量。 注意,如果你开启了 GTID,建议改成 --set-gtid-purged=ON ,否则恢复时 GTID 信息会丢失。 上面命令里我用了 OFF ,只是因为我做了显式兼容处理。
--routines 、 --triggers 、 --events 这三个参数分别是存储过程、触发器、定时事件,很多人会忘记导出。如果漏掉它们,恢复出来的数据库表面上看表和数量都一致,但应用一跑就报错,因为缺了后台逻辑。 --databases 指定导出哪些库,好处是恢复时目标库可以灵活选择。 --gtid-mode=OFF 是为了避免 8.0 上 GTID 相关告警日志刷屏,如果你已经全局开启 GTID,就不需要这个参数,直接依赖 --set-gtid-purged=ON 即可。
还有一个细节: -h127.0.0.1 建议写成具体的 IP 而不是 localhost ,因为 localhost 在某些系统上会走 socket,权限验证方式会变。备份账号建议单独创建最小权限账号,不要直接用 root,例如:
CREATE USER 'backup_user'@'127.0.0.1' IDENTIFIED BY 'your_strong_password'; GRANT SELECT, RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT, SHOW VIEW, EVENT ON *.* TO 'backup_user'@'127.0.0.1';
注意, PROCESS 权限在 8.0 里是执行 FTWRL 必需的,少了它备份会直接报权限错误。
3.2 基于 XtraBackup 的物理全量备份详细步骤与参数调优
当你的数据库体量上来了,逻辑导出时长让人无法接受,就该切到物理备份。我以当前主流的 XtraBackup 8.0 版本为例,演示一套完整的全量备份命令序列。
第一步,做一次全量备份。这一步我们称为 prepare 前的拷贝阶段:
xtrabackup --defaults-file=/etc/my.cnf \ --host=127.0.0.1 --port=3306 \ --user=backup_user --password='your_password' \ --backup --target-dir=/data/backup/full_$(date +%F) \ --parallel=4 --compress --compress-threads=4
--parallel=4 表示拷贝数据文件时的并行线程数,一般建议设置为物理 CPU 核数的一半到全部。 --compress 是采用压缩模式,可以大幅减小磁盘占用,代价是备份时会多一些 CPU 开销。如果服务器 CPU 资源不宽裕,可以去掉压缩,改用 Linux 层面的 tar + zstd 事后压缩。
第二步,备份完成后,需要执行 prepare 阶段,让数据文件恢复到一致状态:
xtrabackup --defaults-file=/etc/my.cnf \ --prepare --target-dir=/data/backup/full_$(date +%F) \ --apply-log-only
这一步非常关键,它做的事情是应用 redo log,把备份文件里的数据推进到一个一致的时间点。 注意,prepare 阶段不要省略,也尽量不要在拷贝完数据文件之后就立刻用这些文件来启动数据库,否则大概率会因为 redo log 不一致导致启动失败。
第三步就是恢复。恢复时先把目标实例的数据目录清空或者移走,再把备份集拷贝回去,最后设置好权限:
xtrabackup --defaults-file=/etc/my.cnf \ --copy-back --target-dir=/data/backup/full_$(date +%F)
然后执行 chown -R mysql:mysql /var/lib/mysql ,就可以启动 MySQL 了。
实操中,我会在备份脚本里额外把 binlog 坐标信息记录下来,方便以后结合 binlog 做时间点恢复。XtraBackup 会在备份目录下生成 xtrabackup_binlog_info 文件,里面记录了备份时刻的 binlog 文件名和坐标, 一定要把这个文件保存好,最好随着备份集一起归档,恢复时先读它。
4. 容易翻车的细节与自动化实践
4.1 备份账号、空间估算与定期清理策略
所谓“中级”,很多工夫花在细节上。备份账号的权限前面已经提到,这里补充一个坑:MySQL 8.0 默认开启了 caching_sha2_password 认证插件,老版本的 mysqldump/xtrabackup 可能无法正常连接,此时需要在创建账号时显式指定 mysql_native_password ,否则你会看到一堆 authentication 相关的报错。
磁盘空间永远是备份方案的第一风险点。 建议在备份脚本里增加前置和后置的空间检查,空间剩余不足全量备份预估大小的 1.5 倍时,报警而不是直接执行。 同时计算备份集增长趋势,设置合理的保留周期。例如保留最近 7 天的每日全量,加上每月一份归档全量,避免备份目录无限膨胀后把磁盘写满,导致数据库本身出问题。
压缩格式选择上,我个人推荐 .sql.gz 或 .xb.gz 这种传统格式,因为解压工具到处都有,碰到紧急恢复时不用为了找一个解压器浪费时间。 zstd 的压缩比高且速度快,但要在所有可能执行恢复的机器上都装好 zstd 工具,否则恢复现场会手足无措。
4.2 自动化备份脚本的骨架与告警通知
手动执行备份偶尔一次没问题,但作为规范化体系,自动化是必须的。下面给一个简化但可用的脚本骨架,里面包含告警通知和状态记录:
#!/bin/bash
# MySQL full backup script
BACKUP_DIR="/data/backup"
TARGET_DIR="${BACKUP_DIR}/full_$(date +%F_%H-%M)"
LOG_FILE="${BACKUP_DIR}/backup.log"
mkdir -p "$TARGET_DIR"
# pre-check disk
DISK_AVAIL=$(df -m "$BACKUP_DIR" | awk 'NR==2 {print $4}')
DATA_SIZE=$(du -sm /var/lib/mysql | awk '{print $1}')
MIN_NEED=$((DATA_SIZE * 2))
if [ "$DISK_AVAIL" -lt "$MIN_NEED" ]; then
echo "[$(date +'%F %T')] ERROR: Not enough disk space." >> "$LOG_FILE"
exit 1
fi
# run backup
xtrabackup --defaults-file=/etc/my.cnf \
--host=127.0.0.1 --user=backup_user --password='...' \
--backup --target-dir="$TARGET_DIR" \
--parallel=4 --compress --compress-threads=4 \
>> "$LOG_FILE" 2>&1
# check backup result
if [ $? -eq 0 ]; then
echo "[$(date +'%F %T')] INFO: Backup succeeded to $TARGET_DIR" >> "$LOG_FILE"
# send success notification (webhook/email/sms)
else
echo "[$(date +'%F %T')] ERROR: Backup failed to $TARGET_DIR" >> "$LOG_FILE"
# send alert notification
fi
# retain 7 daily full backups, delete older
find "$BACKUP_DIR" -maxdepth 1 -type d -name "full_*" -mtime +7 -exec rm -rf {} \;注意脚本中几个关键点:前置空间检查必须做,失败时要有告警而不是仅仅写日志,因为半夜备份失败如果没通知到人,相当于这晚的备份是白做的。备份的日志要定期清理,避免日志文件撑爆磁盘。
告警通知的方式,公司有统一监控平台就接 webhook,没有的话可以用邮件的 mailx 或者企业微信机器人、钉钉机器人。技术选型不是重点,重点是“失败必须第一时间大声喊出来”。
4.3 脚本中加入 binlog 坐标记录,打通增量恢复链路
全量备份如果不和 binlog 增量形成链路,恢复点就只能停留在备份时刻。所以脚本中应当额外解析并保存 binlog 坐标信息。
使用 mysql 命令获取当前 binlog 坐标:
mysql -h127.0.0.1 -uBackupUser -p'...' -e "SHOW MASTER STATUS\G" | grep -E "File|Position"
注意, SHOW MASTER STATUS 在 MySQL 8.0 中依然可用,但在未来的新版本中该语法可能被调整,建议改用 SHOW BINARY LOG STATUS 。获取到坐标后,写入一个专门的状态文件:
echo "binlog_file=$log_file" > "$TARGET_DIR/binlog_position.txt" echo "binlog_pos=$log_pos" >> "$TARGET_DIR/binlog_position.txt"
这个文件非常重要,恢复时先读它,再根据备份点之后的 binlog 做增量重放。 我在实际恢复演练中,有好几次都是先找这个文件,如果哪天发现脚本漏写了这一步,恢复难度会明显上升。
5. 恢复验证与常见故障排查
5.1 每个月做一次恢复演练:备份不等于安全
备份做得再勤,没验证过就是“薛定谔的备份”——你永远不知道它到底能不能用,直到出事故那一刻。正确的是: 至少每个月做一次完整的恢复演练,恢复到一个独立的实例上,校验表数量、行数、关键业务数据是否与主库一致。
恢复演练的自动化,可以比备份更轻量。比如用脚本自动拉起一个临时实例,恢复最近一次全量备份,执行几条简单的数据校验 SQL,然后销毁实例。下面是简化版的恢复校验命令示例:
# 解压并 copy-back 到一个临时目录 cd /data/backup/full_$(date +%F) xtrabackup --prepare --target-dir=/data/backup/full_$(date +%F) # 然后复制到临时实例的 datadir xtrabackup --copy-back --target-dir=/data/backup/full_$(date +%F) # 启动临时实例,指定端口和 socket mysqld --defaults-file=/tmp/restore_test.cnf --port=3307 & sleep 10 # 执行一致性检查 mysql -h127.0.0.1 -P3307 -uroot -e "SELECT COUNT(*) FROM db_core.orders;"
注意,每次启动临时实例前要清空临时 datadir,避免初始化冲突。如果全量备份是压缩的,prepare 时会自动解压,但要求磁盘空间足够,这个也要在演练脚本里检查。
5.2 高频故障场景速查表:命令失败与恢复失败的对症处理
为了便于排查,我总结了以下高频故障场景和处理方案:
| 场景 | 典型报错 | 根因与对策 |
|---|---|---|
| mysqldump 执行时报权限不足 | Access denied; you need (at least one of) the PROCESS privilege(s) | 备份账号缺少 PROCESS 权限,执行 GRANT PROCESS ON *.* TO 'backup_user'@'127.0.0.1'; |
| XtraBackup 备份时找不到 redo log | Failed to find redo log files | 数据目录的 redo log 被清理或路径不一致,检查 datadir 配置,确认 redo log 文件完整 |
| FTWRL 等待时间过长 | -- For more information ... FLUSH TABLES WITH READ LOCK blocked | 存在长事务,备份前用 SELECT * FROM information_schema.innodb_trx\G 排查并 Kill 长时间事务 |
| 恢复后表数据不一致 | 数据条数对不上、部分表为空 | 备份时混合了 MyISAM 和 InnoDB 表, --single-transaction 未覆盖 MyISAM。建议强制所有表改为 InnoDB |
| 8.0 上备份工具连接失败 | Authentication plugin 'caching_sha2_password' cannot be loaded | 备份工具版本过老,或创建账号时未指定 mysql_native_password |
| 磁盘空间不足导致备份中断 | No space left on device | 备份前必须检查空间,建立保留策略并定期清理 |
| 恢复后无法启动数据库 | Unable to find the redo log | 未执行 prepare 或 copy-back 后没设置正确的 datadir 权限 |
5.3 锁等待与长事务:备份期间最隐蔽的坑
这是一个值得单独拿出来讲的坑,因为在低峰期也会遇到。假设你的备份任务在凌晨 2 点执行,正常情况下两分钟跑完,偏偏某一天备份跑了 20 分钟还没结束,业务第二天反馈说凌晨有段时间数据库写入特别慢。排查后发现,备份工具执行 FTWRL 时恰好有一个客户端连接里存在一个未提交的事务,虽然它处于 sleep 状态,但事务没有结束,导致 FTWRL 等待事务释放资源,进而堵住了所有写入。
解决办法有几个层面。第一,靠监控提前发现长事务,除了看 information_schema.innodb_trx ,还可以关注 SHOW ENGINE INNODB STATUS\G 中 TRANSACTION 部分的输出;第二,对连接执行 KILL <thread_id> 清理长时间 idle 事务;第三,对于已知的报表连接或开发客户端,设置合理的 wait_timeout 和 lock_wait_timeout ,避免它们长时间占用事务资源。 我自己的习惯是,在备份前脚本里先执行一次长事务检查,超过一定时长的连接直接告警,而不是默默让备份卡在那里。
5.4 binlog 增量衔接的排错思路
全量备份恢复成功只是第一步,要恢复到故障前的状态,还必须重放 binlog。常见的问题有几个:一是全量备份的 binlog 坐标没记录,导致不知道从哪个 binlog 开始回放;二是 binlog 文件被过早清理,增量链路断裂;三是 GTID 环境下没有正确导入 GTID 信息,导致回放冲突。
针对第一点,前面已经强调过要记录 binlog 坐标。针对第二点,必须把 binlog_expire_logs_seconds (8.0)或 expire_logs_days (5.7)设置成大于全量备份周期的时间, 比如每天一次全量备份,binlog 至少要保留 3 天以上,建议 7 天。 针对第三点,恢复 binlog 时使用 --skip-gtid-mode 或者在 SQL 文件中 SET @@GLOBAL.GTID_PURGED='...' 要谨慎。这里不展开讲 GTID 的全部细节,但你需要记住:GTID 环境下做恢复,多花时间搞懂 GTID 的语义,远比踩坑之后再补救更划算。
6. 备份体系的中级进阶:从能用到好用
做到这一步,你的全量备份已经不是“会跑脚本”的层次了。但如果你对备份体系还有更高的追求,可以继续做三件事。
一件事是备份集中管理。当数据库实例多了以后,每个实例各自为政的备份脚本会变成运维噩梦。可以用跳板机集中接收所有实例上报的备份结果,或者统一从堡垒机下发备份任务。规划上,可以让备份文件落地到一个集中的备份存储节点,按实例名和日期分目录,避免备份文件散落在各个生产服务器上——那样既消耗生产磁盘,出故障时又难以统一找回。
第二件事是制定分级备份策略。核心业务库每天全量 + 实时 binlog,外围业务库每日全量,日志或临时库只做定期归档。分级的意义在于让成本花在刀刃上,不要所有库都按同样的频率和保留周期来做,那样资源浪费严重,尤其在云数据库环境下,备份存储是要计入成本的。
第三件事是建立备份健康度报表。每天备份任务结束后,汇总成功/失败信息,统计备份时长、备份集大小、存储消耗趋势。 做报表不是为了好看,而是让你能敏锐地发现异常:某天备份时长突然翻倍,说明有表数据量暴涨或系统 IO 出现瓶颈;某天备份集大小异常缩小,说明数据可能被意外大规模删除。 这些东西都是事故的前兆。
三件事做完,你会发现备份真正变成了一个“体系”,而不是一个个孤立的脚本。
7. 最后给几点实操经验和建议
做备份越久,越会发现它是一项“枯燥但绝不能出错”的工作。 我可以分享几个直接用的建议:
备份账号的密码不要写在命令行里,用 my.cnf 的独立配置文件并限制权限为 600,防止进程列表泄露密码。备份脚本要加入 set -euo pipefail ,避免管道中某个环节静默失败。
恢复演练尽量提前约定好要校验哪些表。不要只验证“实例能启动、数据库能查询”,那只是最低要求。把核心业务表挑出来做行数对比,时间允许的话再抽查几张表的数据内容与主库对比。 00 全量备份的校验工作做在平时,真正故障时你才有底气说“我有把握恢复”。
在云环境或者容器化环境里做备份,还要额外关注底层存储的快照能力。云盘快照和逻辑备份可以互补,不过云盘快照的一致性取决于云厂商的实现机制,不能想当然地认为快照就是一致性备份。 如果要靠云盘快照做 MySQL 备份,先搞清楚它和存储层、数据库层的配合关系,再决定是否纳入备份体系。
用 XtraBackup 做物理备份的时候,我习惯在备份集目录里同时生成一个 RELEASE_NOTES 文件,记录 MySQL 版本、XtraBackup 版本、备份时间、binlog 坐标、恢复方式说明。因为半年后当你或同事面对这个备份集时,光靠文件名称根本想不起当初是怎么处理的。多写几行说明,恢复时可以省半小时的思考时间。
备份链路的状态监控也不可忽视。哪怕只是简单地给备份脚本出口加一个返回码判断、失败时发一条通知,整体可靠性就能提升一大截。很多事故的起因不是没做备份,而是备份在半夜悄悄失败、没人知道,等到要用了才发现。
还记得一位经验丰富的前辈和我说过的话:备份做得好不好,不是看备份的时候有多顺利,而是看恢复的时候能有多从容。 这句话一直是我的准则。关于全量备份,最容易犯的错不是命令写错,而是备份完就撒手不管、从不验证、从不演练。如果你看完这篇文章,愿意在下个月做一次完整的恢复演练,那这篇内容的价值就真正体现出来了。
到此这篇关于MySQL全量备份实战之从mysqldump到XtraBackup的选型与恢复方法的文章就介绍到这了,更多相关mysql全量备份内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
