06 深入专题:备份与恢复

06 深入专题:备份与恢复

这一篇聚焦 MySQL 备份与恢复:XtraBackup 物理备份原理、mysqldump/mysqlpump 逻辑备份、binlog/redo 恢复、误操作回滚、克隆插件与数据校验。内容整理自爱可生开源社区《大智小技》系列(2019/2020 两册)技术文章精选。

XtraBackup 全量备份中,为什么要在拷贝完 InnoDB 数据文件后加全局读锁(FTWRL)?

全局读锁的目的是保证两件事:①非事务资源(如 MyISAM、frm 表结构)自身的一致性;②非事务资源与事务资源(InnoDB)的一致性。加锁期间没有新数据写入,XtraBackup 在此期间拷贝 binlog 位点信息、frm 表结构、MyISAM 等非事务表,并通过 SHOW MASTER STATUS 记录 binlog 位点作为恢复起点;解锁后该位点与 redo 回放后的 InnoDB 状态对应同一时刻。注意:非事务引擎数据文件较多时,全局读锁的持有时会比较长,影响业务写入,这也是 8.0 引入备份锁(LOCK INSTANCE FOR BACKUP)优化该痛点的原因。

XtraBackup 全量备份为什么要先复制 redo log,而不是直接开始复制数据文件?

XtraBackup 基于 InnoDB 的 crash recovery 机制工作。由于是热备,备份过程中数据库持续有写入,直接复制出来的数据文件可能包含缺失或被修改的页;而 redo log 记录了 InnoDB 引擎的所有事务日志。恢复时通过回放 redo log 即可补全数据文件中缺失/被修改的页,因此必须’先开始复制 redo log’,确保 redo log 一定包含备份过程中涉及的数据页。

XtraBackup 全量备份恢复流程中,为什么先’停止复制 redo log’再’解锁全局读锁’?

同样是为了保证’非事务资源与事务资源的一致性’。先停 redo log 复制,确保后续回放 redo log 得到的 InnoDB 数据状态对应读锁时刻;再解锁。这样通过 redo log 回放后的 InnoDB 数据,与非 InnoDB 数据(读锁期间拷贝)处于同一时刻位点。恢复时 InnoDB 被恢复到备份结束(全局读锁时)状态,与非 InnoDB 数据一致;最后重建 redo log 为启动数据库做准备。

XtraBackup 全量恢复(–prepare)完成后,InnoDB 与非 InnoDB 数据是否达成一致?

是的。–prepare 阶段模拟 MySQL 进行 recover,将 redo log 回放到数据文件,InnoDB 数据被恢复至备份结束时(全局读锁时)的状态;而非 InnoDB 数据本身即在全局读锁时被复制出来,因此二者数据一致。prepare 通常执行一次(也可再跑一次重建 redo/undo),之后用 –copy-back/–move-back 将数据文件拷回空的数据目录(需先停库),并依据 xtrabackup_binlog_info 记录的位点/GTID 接入复制,完成整库恢复。

XtraBackup 增量备份中,非事务类信息(如 MyISAM)能否做增量?

不能。MySQL 没有为非事务类信息提供增量机制,增量备份中非事务类信息是直接全部拷贝和覆盖到临时目录的(与 InnoDB 按 LSN 增量不同)。因此增量备份仍需要全局读锁来保证非事务类信息的一致性——与全量备份机制一致。代价是含大 MyISAM 表时,每次增量都对这部分全拷,增量优势被削弱;若实例仅 InnoDB,则可借 8.0 备份锁避免 FTWRL 长时间持锁。另外纯 MyISAM 场景增量并不比全量小多少,规划备份策略时应优先将表转为 InnoDB,并把非事务表纳入单独的备份窗口。

XtraBackup 增量备份如何识别哪些 InnoDB 数据是增量的?

通过数据页上的 LSN(日志序列号)识别。LSN 可视为数据页的’变更时间戳’,每次页面修改都会推进。全量备份时每个数据页都带 LSN;增量备份只拷贝 LSN 大于上一次备份 to_lsn 的数据页(文中示例为 LSN>400 的页),其余页跳过。备份结束后 xtrabackup_checkpoints 记录本次 from_lsn/to_lsn,下一次增量便以此为基准,无需逐行比对,增量大小取决于实际变更的页数量。

XtraBackup 增量备份过程中某数据页被更新,该页会进入增量备份吗?

增量备份中该数据页是否落在增备文件里不确定(可能被拷也可能没被拷),但该更新一定会被 redo log 记录并包含在备份中。由于备份并非原子快照,一个页可能在被更新前或后被复制、状态新旧不一,但恢复时 redo log 会被’安全地’回放成功,消除数据新旧不一致。这与全量备份中数据页新旧不一致问题的解决方案相同——都靠恢复阶段回放 redo log 把数据修正到一致状态,因此增量同样依赖 redo 完整性。

XtraBackup 增量恢复时 apply-log-only 的正确用法是什么?

除最后一个增备外,所有备份(全备及中间增备)恢复都应加 –apply-log-only(only 指只回放 redo log 阶段、跳过 undo 阶段),避免未完成事务被回滚。典型流程:先对 BASE 做 –prepare –apply-log-only,再对每个增量用 –prepare –apply-log-only –incremental-dir=INC 依次合并;仅最后一个增量合并后才用不带 only 的 –prepare 正常应用 undo、完成回滚并重建 redo。文档参考 Percona xtrabackup 2.3 的 cmdoption-xtrabackup-apply-log-only。

XtraBackup 恢复时为什么要加 apply-log-only 参数?什么场景不加会丢数据?

用全备+增备恢复时,若对全备(及中间增备)不加 –apply-log-only,XtraBackup 会在 prepare 最后一步应用 undo,把未提交事务回滚。但增量备份时可能有’跨备份边界’的未提交事务(如全备时事务2只提交了部分 B->F)。若此时被回滚,后续增备回放事务2剩余部分(E->G)时,数据文件就丢失了这部分,造成数据不一致。–apply-log-only 让 prepare 只做 redo 前滚、跳过 undo 回滚,从而保留跨边界事务的已提交片段。

MySQL 8.0 Clone Plugin 相比 XtraBackup 有哪些不同?

①克隆恢复时需先启动实例并授权,xtrabackup 不需要;②xtrabackup 备份后需 apply log,克隆类似 mysqlbackup 的 backup-and-apply-log 合并做;③xtrabackup 备份文件权限等于执行者权限,恢复时需 chown,克隆后权限与原数据一致免 chown;④xtrabackup 恢复后需 reset master 并设 gtid_purged,克隆默认克隆完即可建复制(自动传输复制位置信息);⑤走 MySQL 监听端口(非 scp 22 端口)。

MySQL 8.0.17 的 Clone Plugin 有哪两种克隆方式?远程克隆对版本和环境有什么要求?

两种方式:本地克隆(克隆到同服务器/节点另一目录)和远程克隆(默认删除接受者数据目录并替换为捐赠者数据,可选克隆到其他目录)。要求:捐赠者/接受者都需安装 clone 插件;分别需 BACKUP_ADMIN/CLONE_ADMIN 权限;相同版本号且 >=8.0.17;同平台同架构(不能 linux→windows、x64→x32);相同 innodb_page_size、innodb_data_file_path、lower_case_table_names 等;不克隆 binlog、配置 my.cnf、非 InnoDB 表(只克隆 InnoDB)。

使用 MySQL 8.0 Clone Plugin 需要什么权限与关键参数?

需备份锁权限 BACKUP_ADMIN(8.0 新特性,比 FTWRL 轻量)。本地克隆:INSTALL PLUGIN clone SONAME ‘mysql_clone.so’,执行 CLONE LOCAL DATA DIRECTORY=’/path’(要求目录不存在)。远程克隆:捐赠者授权 BACKUP_ADMIN,接受者授权 CLONE_ADMIN(=BACKUP_ADMIN+SHUTDOWN,因接受者需 restart);接受者设 clone_valid_donor_list 含捐赠者地址;执行 CLONE INSTANCE FROM user@host:port IDENTIFIED BY ‘pwd’。可用 clone=FORCE_PLUS_PERMANENT 强制插件未初始化则启动失败。

如何用 Clone Plugin 搭建主从/组复制,以及如何监控克隆进度?

克隆出的接受者实例可直接与捐赠者建立复制。查位点:GTID 复制看 performance_schema.clone_status 或 @@GLOBAL.GTID_EXECUTED;传统复制看 clone_status 的 BINLOG_FILE/BINLOG_POSITION。监控进度:SELECT STAGE,STATE,END_TIME FROM performance_schema.clone_progress(阶段含 DROP DATA/FILE COPY/PAGE COPY/REDO COPY/FILE SYNC/RESTART/RECOVERY);SELECT STATE FROM clone_status 查状态;show global status like ‘Com_clone’ 看捐赠者计数。

mysqldump 导出的 SQL 默认会是’大事务’吗?如何控制单条 insert 的 SQL 大小?

MySQL 默认自提交,mysqldump 一条 SQL 即一条事务,默认非大事务。其按 –net-buffer-length 自动切分 SQL,默认 1M;最大可设 16777216(16M),超了自动调整为 16M。想控制每条 insert 大小就调此参数(设大→交互少、导入快;但导入受服务器 max_allowed_packet 限制,默认 4M,需调大否则报 ‘MySQL server has gone away’)。

调大 mysqldump 的 –net-buffer-length 对导出导入性能有什么影响?

该值越大,单条 INSERT 承载的行越多,客户端与数据库交互次数越少,导入越快。文中测试:–net-buffer-length=16K 导入 284M 表约 11 秒,=16M 约 8 秒。注意:它只改变导出的 SQL 文件按多大切块,并不减少单条语句影响行数带来的’大事务’风险(默认 autocommit,每条 INSERT 即一个事务);导出行数随之变化(默认 1M 时 225M 文件约 226 条 insert,16M 时约 15 条)。导入端需同步调大 max_allowed_packet(默认 4M)否则报 gone away。

MySQL 8.0 的备份锁(LOCK INSTANCE FOR BACKUP)是什么,和 FTWRL 有什么区别?

备份锁是 8.0 引入的轻量级锁,允许 online 备份时进行 DML,同时防止快照不一致。它禁止:文件创建/删除/改名、账户管理、REPAIR TABLE、TRUNCATE TABLE、OPTIMIZE TABLE。由 LOCK INSTANCE FOR BACKUP / UNLOCK INSTANCE 组成,需 BACKUP_ADMIN 权限。相比 FTWRL,长查询不会把整个系统 hung 住(FTWRL 会阻塞包括 use database 在内的所有查询)。Oracle MEB 8 和 Percona Xtrabackup 8 都用它。

Percona 自己的轻量级备份锁 LOCK TABLES FOR BACKUP 与 FTWRL 相比有何优势?

Percona MySQL 的 LOCK TABLES FOR BACKUP 比 FTWRL 更轻量:它不刷新表(存储引擎不强制关闭表,表不排出表缓存)。因此它只等待冲突语句(DDL、非 InnoDB 写)完成,不会等待 SELECT 或更新 InnoDB 表的语句完成,避免长时间阻塞。文中有对比测试显示 FTWRL 受长查询影响系统 hung 住、连 use database 都卡,而 lock instance for backup 没有这个问题。它是 Percona 对 8.0 备份锁的等价实现,被 mydumper/Xtrabackup 采用。

MySQL Shell 8.0.21 的逻辑备份工具(util.dumpSchemas 等)相比 mysqldump/mysqlpump/mydumper 优势在哪?

MySQL Shell 的 util.dumpInstance()/dumpSchemas()/loadDump() 用了 zstd 实时压缩+分块并行导出,load data 并行导入,备份恢复均优于非压缩非分块。测试:utli.dumpSchemas 备份 18s/恢复 86s,而 mysqldump(gzip) 169s/255s。注意禁用压缩会同时禁用分块。各工具对比:mysqldump 单线程(恢复最慢);mysqlpump 备份并行但恢复单线程是硬伤;mydumper 默认 gzip 瓶颈在压缩。

文中逻辑备份工具对比测试中各种工具的备份/恢复时间大致如何?

同 schema(混合大小表):utli.dumpSchemas(zstd) 备18s/恢86s 最优;mysqldump(gzip) 备169s/恢255s;mysqlpump(gzip) 备185s/恢121s(恢复单线程硬伤);mydumper(compress) 备164s/恢187s(瓶颈在压缩);mydumper(no compress no chunk) 备15s/恢158s。综合 MySQL Shell 因 zstd 压缩+并行(含单大表并行)最快。

MySQL 8.0 相比之前版本,备份工具有哪些新特性加持(MEB 视角)?

①Backup Lock:LOCK INSTANCE FOR BACKUP 轻量备份锁,只阻止 DDL/文件操作,不阻塞 DML(仅 InnoDB 表时只上备份锁;有非 InnoDB 表才上全局锁);②Redo Log Archiving(8.0.17):指定 innodb_redo_log_archive_dirs 归档 redo,避免高写入下 ibbackup_logfile 写不过来导致 redo 被覆写而备份失败;③Page Tracking:增量备份精确跟踪修改页,提升效率。

什么是 MEB 的 Page Tracking 增量备份,有哪几种扫描模式?

Page Tracking 为优化增量备份、减少不必要页扫描。三种扫描模式:page-track(利用 LSN 精确跟踪上次备份后修改的页,仅复制这些页,最快)、optimistic(扫描修改的数据文件找修改页,依赖系统时间,有限制)、full-scan(扫描所有 InnoDB 数据文件,最慢)。使用前需 INSTALL COMPONENT ‘file://component_mysqlbackup’,全备前 SELECT mysqlbackup_page_track_set(true),增量备份可用 –incremental=page-track(变更页<50% 时提速至少 1 倍)。

恢复实例后如何通过修改 server_id 避免数据丢失?还有哪些相关参数?

恢复实例时尽量修改 server_id,保证与集群其他实例不相同、也不与之前重复,即可避免被误过滤。也可用 –replicate-same-server-id=ON 让 io_thread 即使收到相同 server_id 的 binlog 也写入 relay log;但默认 OFF(避免复制回环),5.7 开启需先关 log_slave_updates,8.0 在 gtid_mode=ON 下可直接开。文中建议优先改 server_id,或用最新备份集导入。

用旧备份恢复实例并加回集群后丢失数据(如 test3 库),根因是什么?

根因是 server_id 过滤机制。旧备份只含 eefac7d8:1-2,恢复实例加入集群后通过新主 binlog 补偿数据。传输事务 eefac7d8:3 时,从库 io_thread 发现该事务记录的 server_id 与自己的 server_id 一致,认为’是自己执行过的事务’而过滤掉,未写入 relay log,导致数据缺少。新主 binlog 有该事务但 relay log 没有。

主从复制报错修复后做从库单表恢复,为什么直接备份表 t 恢复到从库会丢数据?

场景:主库备份表 t(快照 GTID aaaa:1-10000),从库停止复制时 GTID 为 aaaa:1-20000,恢复表 t 后启动复制,起始位点是 aaaa:20001。这样 aaaa:10000-20000 之间修改表 t 的事务不会在从库回放,若其中有改表 t 的数据则丢失。解决办法:备份开始到启动复制期间对表 t 加读锁,保证 aaaa:10000-20000 中没有修改表 t 的事务。大表可用可传输表空间减少锁表时间。

正确的从库单表恢复步骤是什么?

①对表 t 加读锁(保证备份快照到启动复制期间没有修改该表的事务);②主库上备份表 t;③停止从库复制;④恢复表 t 到从库;⑤启动复制;⑥解锁表 t。核心是’先锁表再备份’,否则备份快照与从库当前 GTID 之间的区间里若有修改 t 的事务,启动复制后会从快照之后开始回放,这部分更新被跳过而丢数据。大表推荐用可传输表空间方式备份/恢复以减少锁表时间,锁在整个窗口内需一直持有。实际操作可用 mysqldump –single-transaction 或可传输表空间导出表 t,恢复前确认从库已停 SQL 线程(STOP SLAVE SQL_THREAD)以缩小差异窗口,锁可用 FLUSH TABLES t WITH READ LOCK。

只有 .frm 和 .ibd 文件时,如何批量恢复 InnoDB 表?

步骤:①用 mysqlfrm 从 .frm 文件找回建表语句(mysqlfrm –diagnostic /path/t1.frm,或对整个目录生成 createtb.sql,需装 mysql-utilities);也可从同应用其他库 mysqldump –no-data 取结构;②建库并 source 建表;③ALTER TABLE … DISCARD TABLESPACE 丢弃空 .ibd;④把旧有数据的 .ibd 拷入目录并 chown mysql;⑤ALTER TABLE … IMPORT TABLESPACE 导入;⑥mysqlcheck 检查表。