MySQL 能恢复到半个月内任意一秒的状态,这是怎么做到的?它依赖:全量备份(状态快照) + 增量备份(Binlog 重放)。
1. 全量备份
备份数据库全部的文件、数据库表、配置文件等。
进行全量备份前,应始终作如下准备工作:检查备份服务器的磁盘空间,保证其可用空间大于数据库所占容量;检查 MySQL 服务器超时时间,防止时间过小导致备份中断;检查 MySQL 数据包大小限制参数;检查备份用户是否被赋予正当的权限;始终保证在系统负载较低的情况下进行备份。
全量备份的主要实现方法可以分为全量逻辑备份和全量物理备份。
1.1 全量逻辑备份
全量逻辑备份就是在备份时生成可以重建数据库的 SQL 语句,恢复时将这些 SQL 逐句执行。由于备份的是标准 SQL,因此只要是兼容 SQL 的数据库都可以对其进行恢复(仅需少量语法调整)。并且其灵活性较高,可以选择备份整个数据库、单个数据库、单个表,甚至只备份表结构或只备份数据。但是其在备份和恢复的速度上都远逊色于物理备份,因此对于中小型数据库更加适用。全量逻辑备份的常见方式有如下几种:
-
mysqldump
为了保证数据一致性,mysqldump 默认会在备份开始时对表加一个读锁。在此期间,表可读但不可写(温备份),对于大表或繁忙的数据库,这会导致业务中断。因此对于 InnoDB 表,我们一般在启动 mysqldump 时使用
--single-transaction选项来开启一个事务,利用 MVCC 获取一个一致性的数据快照。此时,备份过程不会阻塞写入操作(热备份),同时保证了数据一致性。
我们通过 mysqldump 日志来看看
--single-transaction选项在背后做了哪些事:
Connect backup_user@localhost on using SSL/TLS 连接数据库服务器。
FLUSH /*!40101 LOCAL */ TABLES 关闭所有已打开的表,并清除表缓存。LOCAL 字段表示此时会立即清除未正在被使用的表缓存,而正在被使用的表将被标记为在下次使用后关闭,不会阻塞等待。这一步是为下一步的读锁做准备,使下一步需要等待的表尽量少。
FLUSH TABLES WITH READ LOCK 短暂加读锁。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 设置隔离级别为可重复读。
START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */ 以上都是准备工作,现在真正开启一个事务并创建一致性快照。在此事务中所有的 SELECT 操作都会看到这个一致的数据视图。
SHOW MASTER STATUS 这条语句在下面详细讲,因为涉及到了其他选项。
UNLOCK TABLES 释放全局读锁,锁仅持有了很短暂的时间。
--source-data是另外一个很重要的 mysqldump 选项,当我们使用这个选项时,在日志中就会看到SHOW MASTER STATUS这条语句。其用法包括:
--source-data=1:将CHANGE MASTER TO语句以非注释形式写入备份文件。
--source-data=2:将CHANGE MASTER TO语句以注释形式写入备份文件。 例如:
sql-- 使用 --source-data=2 生成的备份文件开头: -- -- Position to start replication or point-in-time recovery from -- -- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=107; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 可以看出,它在备份中的关键作用是记录当前(备份开始时)二进制日志的坐标。这在基于时间点的恢复和主从复制中非常重要,它告诉了我们从节点该从哪里开始应用主节点的 binlog。在 mysqldump 日志中可以看出,
SHOW MASTER STATUS是在其持有全局读锁时执行的,这是为了确保其精确记录开始备份时的 binlog 坐标。 由于这里是全量备份,可以考虑使用
--flush-logs选项在备份开始前创建一个新的二进制日志文件在备份后写入,这样可以使日志边界更清晰,因为新的日志文件只包含备份后的操作。但如果是频繁的备份操作,就不宜使用这个选项了,会产生很多几乎为空的日志文件。 mysqldump 是单线程的,并且由于其逻辑备份特性,备份大型数据库时会比较慢。对于大型数据库,应考虑使用物理备份。
-
Mydumper
第三方开源的多线程逻辑备份方案,通常用以替代 mysqldump。
-
MySQL Shell & util.dumpInstance
MySQL Shell 是 Oracle 官方维护的高级、跨平台的客户端,它不仅支持 SQL ,还支持 NoSQL(如文档存储)、JavaScript 和 Python 脚本。其备份组件
util.dumpInstance(备份整个 MySQL 实例)、util.schemas(备份单个数据库)是更现代的逻辑备份工具选择。 其自带多线程备份以及多线程恢复功能, 可以直接替代 mysqldump。
1.2 全量物理备份
物理备份就是直接复制数据库的物理数据文件,由于其绕过了 MySQL Server 层,备份和恢复的速度都非常快。其缺点在于备份粒度较粗,并且恢复时依赖相同的硬件和 MySQL 版本。适用于数据量较大的数据库,追求最快的备份和恢复速度。
-
**Percona **
最成熟的 MySQL 物理备份工具,由 Percona 公司开源且针对 InnoDB 实现热备份。
XtraBackup 的热备份实现原理与 mysqldump 完全不同,因为 XtraBackup 属于物理备份,它直接复制物理数据文件,无法依赖 MVCC 在备份开始时获取事务一致的视图。因此 XtraBackup 通过复制记录 Redo Log 来实现热备份,这个过程具体如下:
XtraBackup 在复制数据文件前记录 Redo Log 的当前 LSN,并使用一个后台线程持续追踪并复制 Redo Log 自备份开始后产生的新变化(必须复制走,因为 Redo Log 是循环日志,旧日志可能被新日志覆盖)。这些复制的 Redo Log 用于在后续重做备份过程中提交的事务。
XtraBackup 直接复制 InnoDB 的表空间文件(.ibd 和 ibdata)。由于不锁表,这些文件可能正在被修改,因此此时拷贝出来的数据文件可能包含未提交的事务,或者缺少已提交但尚未从日志刷新到数据文件的事务。这些事务依赖在上一步复制的 Redo Log 来重做,并使用 Undo Log 回滚。
数据文件复制完成后,XtraBackup 短暂地为数据库加读锁,然后终止 Redo Log 的复制,并精确记录当前 Binlog 的坐标,之后解锁。
备份完成后得到的数据文件需要经过一个独立的 "准备" 阶段,在这个阶段使用之前备份的 Redo Log 对 InnoDB 数据文件进行前滚(Redo)和回滚(Undo)。经过这个准备阶段,数据文件就达到了一个事务一致状态。使用
--apply-log选项来开启准备阶段。 需要注意的是,如果该全量备份后续还有增量备份(XtraBackup 也可以用于增量备份,后面会提到),使用
--apply-log选项时必须搭配--apply-log-only选项,直到最后一条需要应用的增量备份。这表示在恢复数据时,直到所有目标数据都准备完毕,再使用 Undo Log 回滚未完成事务,这才是符合预期的。如果不加这个选项,那么前面的备份已经回滚了未完成事务,后续的备份可能包含这些事务的后续操作,这就会造成数据不一致。 正确实例如下:
bash# 准备阶段 xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup \ --incremental-dir=/path/to/inc1 xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup \ --incremental-dir=/path/to/inc2 xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup \ --incremental-dir=/path/to/inc3 xtrabackup --prepare --target-dir=/path/to/full_backup # 开始恢复 xtrabackup --copy-back --target-dir=/path/to/full_backup
2. 增量备份
增量备份只备份⾃上次备份以来发⽣变化的数据。可以是上次全量备份以后,也可以是上次增量备份以后。
2.1 增量逻辑备份
-
Binlog
MySQL 的增量逻辑备份主要依赖 Binlog 来实现,这也是本篇文章介绍的重点。Binlog 属于 MySQL Server 层,无论使用什么存储引擎,只要发生表数据更新,就会产生 Binlog。
Binlog 的三种格式:
statement 格式直接记录不加任何处理的 SQL 语句,这样做的问题是,在恢复数据时,很多 SQL 中函数的运算结果会与原库不一致,比如 now()、UUID() 等。我们一般使用 row 格式来规避上述问题,row 格式不再记录原始 SQL 语句了,而是记录操作的具体数据的前后镜像,类似这样:
tex### UPDATE test.users ### WHERE ### @1=1 (@1: id=1) ### @2='John' (@2: name='John') ### @3=500 (@3: balance=500) ### @4='active' (@4: status='active') ### SET ### @1=1 (@1: id=1) ### @2='John' (@2: name='John') ### @3=600 (@3: balance=600) ### @4='active' (@4: status='active') 可以看出 row 是一种记录数据行变化的格式,恢复时服务器据此信息直接操作行数据。这也是 MySQL 默认的 binlog 格式。这种格式的缺点是空间占用较大,因为一条 SQL 可能修改成千上万行,而 row 格式会记录这成千上万行的数据,不过为了数据的绝对准确性,这样的空间消耗是可以接受的。
row 格式是二进制的,statement 是文本格式的,这是因为 row 记录的是结构化的行数据,使用二进制更高效和精确,但不直接可读,而 statement 记录的就是 SQL 语句,所以理所当然使用了文本格式。
这表示我们无法直接用 mysqlbinlog 像看 SQL 一样轻松地看懂 row 格式的 binlog,虽然 -v 选项可以将其翻译成伪 SQL,但失去了原始的 SQL 信息。
还有一种 mixed 格式,这种格式下 MySQL 会判断该条 SQL 是否可能引起数据不一致,如果可能就使用 row,如果不可能就使用 statement,但是 MySQL 的判断不一定非常准确,可能存在漏网之鱼,所以我们还是使用 row 格式。
Binlog 的刷盘策略:
当且仅当事务提交时,才将整个事务的 Binlog 一次持久化到磁盘日志文件中,此前 Binlog 维护在线程私有的 Binlog Cache 中。这表示 Binlog 是严格以完整的事务为单位进行写入的,每个线程拥有独立的缓冲区保证事务之间不会交叉记录(事务原子性写入)。
这样的设计与 Redo Log 有较大不同,Redo Log 的缓冲区是公有的,其持久化并不严格以事务为单位。也就是说磁盘上的 Redo Log 文件中并不保证记录的都是完整的事务,崩溃恢复时,依赖 Undo Log 来回滚那些不完整的事务。
为什么会有这样的设计差异呢?因为其设计目标完全不同。Redo Log 是一种物理日志,它更底层,面向数据页,它存在的目的只有一个,就是以最快速度将页面恢复到某个状态,至于这个状态中哪些事务有效,由 Undo Log 来判定。它面向的是快速灾救场景,需要保证速度,其日志内容在对应的数据页持久化后就没用了。而 Binlog 是一种逻辑日志,更上层,恢复它需要 MySQL 服务器的参与,因为其本质就是备份的一份 SQL 指令集。如果事务都是混在一起的,MySQL 服务器根本无法运行成功。
sync_binlog参数用以配置 Binlog 的刷盘策略:
sync_binlog=0事务提交时,只调用 write 将日志写入 OS 的 page cache,不主动调用 fsync 刷盘,由 OS 决定何时刷盘。此时意味着我们无法承受 OS 级别的故障。
sync_binlog=1事务提交时手动调用 fsync 刷盘,最大程度保证数据安全。
sync_binlog=N积累 N 个事务提交时才手动调用 fsync 刷盘,这表示 N 个事务可能因 OS 级别的故障而丢失。
两阶段提交:
两阶段提交是为了保证 Redo Log 和 Binlog 的数据一致性。
事务提交时,Redo Log Buffer 持久化后事务状态被标记为 prepare。Binlog Cache 持久化后会发送一个信号给 InnoDB,告知其 Binlog 写入成功。InnoDB 收到信号后将事务状态正式标记为 commit。
应用两阶段提交后,在崩溃恢复时,Redo Log 中如果某事务状态依然是 prepare,并且没有对应的 Binlog,那意味着该事务没有写入 Binlog,则 InnoDB 会回滚该事务。如果事务状态是 prepare,但是有对应的 Binlog,那意味着 Binlog 写入成功了,只是信号没有传递到 InnoDB,此时 InnoDB 不会回滚该事务。
Binlog 恢复:
使用 mysqlbinlog 客户端来解析指定范围内的所有日志,并生成一个包含对应 SQL 语句的文件。再将生成的 SQL 文件导入到已恢复全量备份的数据库中即可。
bash# 将 binlog 重放到指定时间点 mysqlbinlog \ --start-datetime="2023-10-01 00:00:00" \ --stop-datetime="2023-10-15 20:30:15" \ /path/to/binlog.000001 /path/to/binlog.000002 ... > binlog_replay.sql # 执行恢复 mysql -u root -p < binlog_replay.sql
2.2 增量物理备份
-
Percona XtraBackup
XtraBackup 也可以进行增量备份,它通过比较数据页的 LSN 来识别该页数据是否发生变化。如果 LSN 大于上次备份的 LSN,说明该页发生了新的修改,重新备份该页,否则则直接跳过。
具体的备份过程和 XtraBackup 的全量备份一致,都是靠一个后台线程持续复制 Redo Log 来实现热备份。