共计 9477 个字符,预计需要花费 24 分钟才能阅读完成。
mysqldump 一致性热备的原理是什么,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。
首先用 mysqldump 执行一次一致性备份:
$ mysqldump -uroot -p --skip-opt --default-character-set=utf8 --single-transaction --master-data=2 --no-autocommit -B d1 backup.sql
关键参数解释:
–single-transaction:执行一致性备份。
–master-data=2:要求 dump 结果中以注释形式保存备份时的 binlog 位置信息。
-B:指定要 dump 的数据库,在这里 d1 是一个使用 InnoDB 作为存储引擎的库,其中只有一个表 t1。
执行完成后可以得到 mysqld 生成的 general log,里面记录了 mysqldump 在备份过程中传给 server 的指令。
其中关键的步骤我用框框作了标记,具体的解释请看下文。
mysqldump 一致性备份的主要执行流程
鸿蒙官方战略合作共建——HarmonyOS 技术社区
连接 server
两次关闭所有表,第二次关表同时加读锁
设置隔离级别为“可重复读”,开始事务并创建快照
获取当前 binlog 位置
解锁所有表
对指定的库与表进行 dump
下面结合 SQL 内容与源码对以上主要步骤进行依次介绍。
流程剖析
1. 连接 server
mysqldump 首先与 server 建立连接,并初始化 session,set 一些 session 级的变量,对应 SQL 如下图
其在 main 函数中对应的源码就是一个对 connect_to_db 函数的调用:
if (connect_to_db(current_host, current_user, opt_password)) { free_resources(); exit(EX_MYSQLERR);
2. 两次关闭所有表,第二次关表同时加读锁
连接建立后,mysqldump 紧接着执行两次关表操作,并在第二次关表同时给所有表加上读锁,对应 SQL 如下图:
这一部分在 main 函数中对应的源码为:
if ((opt_lock_all_tables || opt_master_data || (opt_single_transaction flush_logs)) do_flush_tables_read_lock(mysql)) goto err;
可以看到实际操作由 do_flush_tables_read_lock 函数进行,但是这里需要注意操作执行的前提条件,观察代码我们可以知道,这个关表操作只会在三种情况下进行:
鸿蒙官方战略合作共建——HarmonyOS 技术社区
通过 –lock-all-tables 选项显式要求给所有表加锁。
通过 –master-data 选项要求 dump 出来的结果中包含 binlog 位置。
通过 –single-transaction 指定了进行单事务的一致性备份,同时通过 –flush-logs 要求刷新 log 文件。
看到这里不难知道,除了第一种情况显式要求加锁之外,情况 3 要求刷新 log 前没有其他事务在进行写操作,自然要对所有表加上读锁。情况 2 要求 dump 结果中准确记录 dump 进行时刻的 binlog 位置,为了准确地得到当前 binlog 的位置,自然就需要给所有的表加共享锁,防止其他并行事务进行写操作导致 binlog 更新,因此这里才有一个关表、加读锁的动作。
这里有一个细节,我们知道 –single-transaction 选项可以执行一致性备份,那么在只有 –single-transaction 选项时为什么不需要进行关表与加读锁的动作呢?这是因为 –single-transaction 所保证的一致性备份依赖于支持事务的存储引擎(如 InnoDB),在后面会提到,mysqldump 通过执行 START TRANSACTION WITH CONSISTENT SNAPSHOT 会创建一个数据库当前的快照与一个事务 id,所有在该事务之后的事务所进行的数据更新都会被过滤,以此来保证备份的一致性。这种方式的优势在于不会在进行一致性备份时干扰其他事务的正常进行,实现了所谓的“热备”,但是缺点在于其依赖事务型存储引擎,对于使用 MyISAM 等不支持事务的存储引擎的表,–single-transaction 无法保证它们的数据一致性。
接着查看 do_flush_tables_read_lock 函数的源码:
static int do_flush_tables_read_lock(MYSQL *mysql_con) { return (mysql_query_with_error_report( mysql_con, 0, ((opt_master_data != 0) ? FLUSH /*!40101 LOCAL */ TABLES : FLUSH TABLES )) || mysql_query_with_error_report(mysql_con, 0, FLUSH TABLES WITH READ LOCK)); }
可以看到逻辑比较简单,就是向 server 传入执行两个 query,依先后次序分别时 FLUSH TABLES 和 FLUSH TABLES WITH READ LOCK,这里核心的动作在于后面一个 query,之所以需要前面的 FLUSH TABLES 是基于性能的考量,以尽可能减少加锁对其他事务的影响。
3. 设置隔离级别为“可重复读”,开始事务并创建快照
关表操作执行完后,mysqldump 接着开启一个新事务并创建快照,对应 SQL 如下图:
这一部分在 main 函数中对应的源码为:
if (opt_single_transaction start_transaction(mysql)) goto err;
可以看到,只有在指定 –single-transaction 选项时这一步骤才会执行。实际上这一步就是 mysqldump 实现一致性热备的基础,我们接着查看 start_transaction 函数的源码:
static int start_transaction(MYSQL *mysql_con) { // 省略部分非关键代码与注释 return ( mysql_query_with_error_report(mysql_con, 0, SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ) || mysql_query_with_error_report(mysql_con, 0, START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */)); }
可以看到核心动作是传给 server 执行的两个 query,先是 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 确保当前会话的隔离级别是“可重复读”,然后通过 START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */ 来开始一个新事务,产生一个新事务 id,同时创建一个快照,dump 过程中所使用的数据都基于这个快照。这样,所有在该事务之后的事务所进行的数据更新都会被过滤,备份的数据一致性因此得以保证。
但是,这样的热备方法,依赖于像 InnoDB 这样支持事务的存储引擎。相反,如 MyISAM 这种不支持事务的存储引擎在备份过程中的数据一致性则不能被保证。
4. 获取当前 binlog 位置
随后 mysqldump 执行一个 SHOW MASTER STATUS 的 query,以获取当前 binlog 的位置信息:
查看 main 函数中对应部分的源码可以看到,只有在指定 –master-data 选项时才会去获取、记录当前的 binlog 位置:
if (opt_master_data do_show_master_status(mysql)) goto err;
查看 do_show_master_status 函数的实现,可以看到核心动作就是向 server 传入执行一个 SHOW MASTER STATUS 的 query,最后将得到的 binlog 位置信息写入 dump 结果中。
static int do_show_master_status(MYSQL *mysql_con) { MYSQL_ROW row; MYSQL_RES *master; const char *comment_prefix = (opt_master_data == MYSQL_OPT_MASTER_DATA_COMMENTED_SQL) ? -- : if (mysql_query_with_error_report(mysql_con, master, SHOW MASTER STATUS)) { return 1; } else { row = mysql_fetch_row(master); if (row row[0] row[1]) { print_comment(md_result_file, 0, \n--\n-- Position to start replication or point-in-time recovery from\n--\n\n // 写入 dump 结果 fprintf(md_result_file, %sCHANGE MASTER TO MASTER_LOG_FILE= %s , MASTER_LOG_POS=%s;\n , comment_prefix, row[0], row[1]); check_io(md_result_file); } // ... } return 0; }
5. 解锁所有表
在正式开始 dump 操作之前,mysqldump 会把前面操作中可能加了锁的表全部解锁:
查看 main 函数中对应部分代码:
if (opt_single_transaction do_unlock_tables(mysql)) /* unlock but no commit! */ goto err;
可以看到,只有在指定了 –single-transaction 选项时才会解锁所有先前被加锁的表,结合前面的思考可以推断,–single-transaction 下所进行的备份通过事务性质可以保证数据的一致性,没有必要再保留对所有表所加的锁,因此这里执行解锁,以免阻塞其他事务的进行。
6. 对指定的库与表进行 dump
前面的准备操作进行完成后,mysqldump 开始正式进行选定库、表的 dump 操作:
对指定数据库的实际 dump 由 dump_databases 函数执行(当指定了 –all-databases 要求 dump 所有库时,则由 dump_all_databases 函数执行)。
查看 dump_databases 函数的实现:
static int dump_databases(char **db_names) { int result = 0; char **db; DBUG_TRACE; for (db = db_names; *db; db++) { if (is_infoschema_db(*db)) die(EX_USAGE, Dumping \ %s\ DB content is not supported , *db); if (dump_all_tables_in_db(*db)) result = 1; } if (!result seen_views) { for (db = db_names; *db; db++) { if (dump_all_views_in_db(*db)) result = 1; } } return result; } /* dump_databases */
逻辑比较清晰,先 dump 每个指定的数据库中所有的表,之后如果存在视图,则将对应视图也进行 dump。我们的考察重点放在对表的 dump 上。
实际 dump 一个表的操作逻辑也比较清晰,就是先获取表的结构信息,得到表的创建语句,然后获取表中每行的实际数据并生成对应的 insert 语句。
不过,前面的 general log 中有个值得注意的点是 SAVEPOINT 的出现,这一点在 MySQL 5.5 的 mysqldump 中是没有的,查看 dump_all_tables_in_db 函数的实现,可以找到设置 savepoint 的对应代码:
// 创建 savepoint if (opt_single_transaction mysql_get_server_version(mysql) = 50500) { verbose_msg( -- Setting savepoint...\n if (mysql_query_with_error_report(mysql, 0, SAVEPOINT sp)) return 1; } while ((table = getTableName(0))) { char *end = my_stpcpy(afterdot, table); if (include_table(hash_key, end - hash_key)) { dump_table(table, database); // 对表进行 dump // 省略部分代码... // ROLLBACK 操作 /** ROLLBACK TO SAVEPOINT in --single-transaction mode to release metadata lock on table which was already dumped. This allows to avoid blocking concurrent DDL on this table without sacrificing correctness, as we won t access table second time and dumps created by --single-transaction mode have validity point at the start of transaction anyway. Note that this doesn t make --single-transaction mode with concurrent DDL safe in general case. It just improves situation for people for whom it might be working. */ if (opt_single_transaction mysql_get_server_version(mysql) = 50500) { verbose_msg( -- Rolling back to savepoint sp...\n if (mysql_query_with_error_report(mysql, 0, ROLLBACK TO SAVEPOINT sp)) maybe_exit(EX_MYSQLERR); }
可以看到创建 savepoint 是在 dump 表之前,之后遍历库中的每个表,每当 dump 完一个表之后,便执行一次 ROLLBACK TO SAVEPOINT sp 操作,为什么呢?其实上面代码的注释已经解释清楚了:
简单来说,当我们 dump 完一个表后后面都不再需要使用这个表,这时其他事务的 DDL 操作不会影响我们 dump 得到数据的正确性,增加 savepoint 的意义在于,假如我们要 dump 表 A,savepoint 记录了 dump 表 A 之前尚未给表 A 加 MDL 锁的状态,当开始 dump 表 A 时,由于要进行一系列 select 操作,会给表 A 加上 MDL 锁防止其他事务的 DDL 操作改变表结构导致读动作出错;最后当对表 A 的 dump 完成后,后续都不会再访问表 A 了,此时没有释放的 MDL 锁没有意义,反而会阻塞其他并行事务对表 A 的 DDL 操作。
对此,MySQL 的解决方法是在访问表 A 前通过 SAVEPOINT sp 记录一个 savepoint,在 dump 完表 A 之后通过 ROLLBACK TO SAVEPOINT sp 回到当时的状态,即可释放对表 A 加的 MDL 锁,放行其他事务对该表的 DDL 操作。
小结
以上是 mysqldump 基于 MySQL 8.0 的一致性备份原理介绍,相比 MySQL 5.5,现如今 MySQL 8.0 在 mysqldump 的实现存在一定改进,除了上面提到的 savepoint 机制是一个显著区别之外,还有诸如对 GTID 的支持、对 column statistics 的 dump 操作在本文中没有提及,但总体而言,mysqldump 在一致性备份上的实现原理并没有多少改变。
拓展阅读 mdash; mdash;Percona 的实现
MySQL 从出现到普及,中途也出现了其他不少优秀的发行版,MySQL 中一致性备份的实现其实也并不完美,因此如果能够考量其他发行版在这方面上的实现,也是一件有意义的事情。
Backup Lock
在前面我有提到,mysqldump 中 –single-transaction 选项所实现的一致性备份不需要对表加锁,但这一特性基于事务型的存储引擎,因此只对 InnoDB 表或使用其他事务型存储引擎类型的表能够保证备份时过滤掉其他并行事务的更新操作;但对使用了 MyISAM 这种不支持事务的存储引擎的表,–single-transaction 无法保证其数据的一致性,即若备份过程中出现了来自其他并行事务的更新操作,其很有可能被写入了备份中。
既然如此,若想对 MyISAM 的表进行备份,又想保证其一致性该怎么办?一种方式可以是在执行 mysqldump 时传入 –lock-all-tables 选项,这个选项会使得 dump 操作进行之前执行一个 FLUSH TABLES WITH READ LOCK 语句,并保证在 dump 的全程保持对所有表的读锁。但是无疑这是一种 overkill,仅仅是为了保证一部分非事务型存储引擎的表的一致性,就需要对所有表加锁,进而业务上所有对 server 的写操作被阻塞一段时间(若备份的数据量大,这简直会造成一场灾难)。
这一问题,我尚未在 MySQL 8.0 中找到相应的好的解决方式,不过 Percona 对此给出了一个方案:在 Percona 发行版的 mysqldump 中,执行时可以传入一个 –lock-for-backup 选项,这个选项会使得 mysqldump 在 dump 之前,执行一个 LOCK TABLES FOR BACKUP 语句,这是一个 Percona 独有的 query,其主要做以下几件事情:
阻塞对 MyISAM, MEMORY, CSV, ARCHIVE 表的更新操作;
阻塞对任何表的 DDL 操作;
不阻塞对临时表与 log 表的更新操作。
显然,有了以上的特性,当同时传入 –lock-for-backup 与 –single-transaction 两个选项同时,mysqldump 可以保证所有表的数据一致性,并且尽可能保证造成最少的线上业务干扰。
这一部分逻辑可以在 Percona Server 8.0 中 mysqldump 的代码中找到,在 main 函数中:
if (opt_lock_all_tables || (opt_master_data (!has_consistent_binlog_pos || !has_consistent_gtid_executed)) || (opt_single_transaction flush_logs)) { if (do_flush_tables_read_lock(mysql)) goto err; ftwrl_done = true; } else if (opt_lock_for_backup do_lock_tables_for_backup(mysql)) goto err;
细心的朋友会发现,这是对上面的“关表加读锁操作”进行的逻辑改写,其增加了一个 else if 逻辑分支,取代了之前的 FLUSH TABLES; FLUSH TABLES WITH READ LOCK; 操作,主要目的是为了与 –single-transaction 进行的一致性备份更好地兼容,实现对线上业务尽可能少的阻塞。
接着查看 do_lock_tables_for_backup 函数的实现,可以看到就是简单地向 server 传入一个 Percona 独有的 LOCK TABLES FOR BACKUP 语句:
static int do_lock_tables_for_backup(MYSQL *mysql_con) noexcept { return mysql_query_with_error_report(mysql_con, 0, LOCK TABLES FOR BACKUP }
Binlog Snapshot
在 MySQL 8.0 的实现中,有一个常用的选项,仍然会导致“讨人厌”的 FLUSH TABLES WITH READ LOCK 的执行,即 –master-data 选项。
前面提到,–master-data 选项要求在 dump 之后的结果中存有当前备份开始时的 binlog 位置,为了满足所获得 binlog 位置的一致性,需要在执行 SHOW MASTER STATUS 前,获取对所有表的读锁以阻塞所有 binlog 的提交事件,因此要求执行一次 FLUSH TABLES WITH READ LOCK。但是有没有更好的方式?Percona 同样给出了自己的解决方法。
在 Percona Server 中,新增了两个全局 status:Binlog_snapshot_file 和 Binlog_snapshot_pos,分别用来记录当前的 binlog 文件与 binlog 位置,通过 SHOW STATUS LIKE binlog_snapshot_% 即可获取两个 status 的值。那么使用这个方式,跟 SHOW MASTER STATUS 有什么区别?
二者的区别在于,Binlog_snapshot_file 和 Binlog_snapshot_pos 这两个 status 具有事务性,只要在执行 SHOW STATUS LIKE binlog_snapshot_% 这个语句之前通过 START TRANSACTION WITH CONSISTENT SNAPSHOT 创建了新事务与一致性快照,Binlog_snapshot_file 和 Binlog_snapshot_pos 所记录的则正是该事务开始时的 binlog 文件与位置信息,进而 binlog 信息的一致性得到保证,而这一过程的全程都不需要 FLUSH TABLES WITH READ LOCK 的执行。
相对的,SHOW MASTER STATUS 是不具备事务性的,每次执行该语句返回的都是当前最新的 binlog 位置信息,这也是为什么执行它之前需要对所有表上读锁。
看完上述内容,你们掌握 mysqldump 一致性热备的原理是什么的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注丸趣 TV 行业资讯频道,感谢各位的阅读!