mysqldump一致性熱備原理剖析
引言
在日常數(shù)據(jù)庫運(yùn)維中,經(jīng)常要對(duì)數(shù)據(jù)庫進(jìn)行熱備。熱備的一個(gè)關(guān)鍵點(diǎn)是保證數(shù)據(jù)的一致性,即在備份進(jìn)行時(shí)發(fā)生的數(shù)據(jù)更改,不會(huì)在備份結(jié)果中出現(xiàn)。mysqldump是實(shí)際場(chǎng)景中最常使用的備份工具之一,通過選擇合適的選項(xiàng)做備份,mysqldump可以保證數(shù)據(jù)的一致性,同時(shí)盡可能保證進(jìn)行中的業(yè)務(wù)不受影響。
那么mysqldump是如何實(shí)現(xiàn)一致性備份的?以下我將結(jié)合mysqldump過程中mysqld生成的general log與mysqldump的源碼來解釋mysqldump一致性備份的原理。
注:以下的實(shí)例基于MySQL 8.0.18,在不同版本上mysqldump的部分實(shí)現(xiàn)會(huì)有不同
首先用mysqldump執(zhí)行一次一致性備份:
- $ mysqldump -uroot -p --skip-opt --default-character-set=utf8 --single-transaction --master-data=2 --no-autocommit -B d1> backup.sql
關(guān)鍵參數(shù)解釋:
- --single-transaction:執(zhí)行一致性備份。
- --master-data=2:要求dump結(jié)果中以注釋形式保存?zhèn)浞輹r(shí)的binlog位置信息。
- -B:指定要dump的數(shù)據(jù)庫,在這里d1是一個(gè)使用InnoDB作為存儲(chǔ)引擎的庫,其中只有一個(gè)表t1。
執(zhí)行完成后可以得到mysqld生成的general log,里面記錄了mysqldump在備份過程中傳給server的指令。
其中關(guān)鍵的步驟我用框框作了標(biāo)記,具體的解釋請(qǐng)看下文。
mysqldump一致性備份的主要執(zhí)行流程
- 連接server
- 兩次關(guān)閉所有表,第二次關(guān)表同時(shí)加讀鎖
- 設(shè)置隔離級(jí)別為“可重復(fù)讀”,開始事務(wù)并創(chuàng)建快照
- 獲取當(dāng)前binlog位置
- 解鎖所有表
- 對(duì)指定的庫與表進(jìn)行dump
下面結(jié)合SQL內(nèi)容與源碼對(duì)以上主要步驟進(jìn)行依次介紹。
流程剖析
1. 連接server
mysqldump首先與server建立連接,并初始化session,set一些session級(jí)的變量,對(duì)應(yīng)SQL如下圖
其在main函數(shù)中對(duì)應(yīng)的源碼就是一個(gè)對(duì)connect_to_db函數(shù)的調(diào)用:
- if (connect_to_db(current_host, current_user, opt_password)) {
- free_resources();
- exit(EX_MYSQLERR);
2. 兩次關(guān)閉所有表,第二次關(guān)表同時(shí)加讀鎖
連接建立后,mysqldump緊接著執(zhí)行兩次關(guān)表操作,并在第二次關(guān)表同時(shí)給所有表加上讀鎖,對(duì)應(yīng)SQL如下圖:
這一部分在main函數(shù)中對(duì)應(yīng)的源碼為:
- if ((opt_lock_all_tables || opt_master_data ||
- (opt_single_transaction && flush_logs)) &&
- do_flush_tables_read_lock(mysql))
- goto err;
可以看到實(shí)際操作由do_flush_tables_read_lock函數(shù)進(jìn)行,但是這里需要注意操作執(zhí)行的前提條件,觀察代碼我們可以知道,這個(gè)關(guān)表操作只會(huì)在三種情況下進(jìn)行:
- 通過--lock-all-tables選項(xiàng)顯式要求給所有表加鎖。
- 通過--master-data選項(xiàng)要求dump出來的結(jié)果中包含binlog位置。
- 通過--single-transaction指定了進(jìn)行單事務(wù)的一致性備份,同時(shí)通過--flush-logs要求刷新log文件。
看到這里不難知道,除了第一種情況顯式要求加鎖之外,情況3要求刷新log前沒有其他事務(wù)在進(jìn)行寫操作,自然要對(duì)所有表加上讀鎖。情況2要求dump結(jié)果中準(zhǔn)確記錄dump進(jìn)行時(shí)刻的binlog位置,為了準(zhǔn)確地得到當(dāng)前binlog的位置,自然就需要給所有的表加共享鎖,防止其他并行事務(wù)進(jìn)行寫操作導(dǎo)致binlog更新,因此這里才有一個(gè)關(guān)表、加讀鎖的動(dòng)作。
這里有一個(gè)細(xì)節(jié),我們知道--single-transaction選項(xiàng)可以執(zhí)行一致性備份,那么在只有--single-transaction選項(xiàng)時(shí)為什么不需要進(jìn)行關(guān)表與加讀鎖的動(dòng)作呢?這是因?yàn)?-single-transaction所保證的一致性備份依賴于支持事務(wù)的存儲(chǔ)引擎(如InnoDB),在后面會(huì)提到,mysqldump通過執(zhí)行START TRANSACTION WITH CONSISTENT SNAPSHOT會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫當(dāng)前的快照與一個(gè)事務(wù)id,所有在該事務(wù)之后的事務(wù)所進(jìn)行的數(shù)據(jù)更新都會(huì)被過濾,以此來保證備份的一致性。這種方式的優(yōu)勢(shì)在于不會(huì)在進(jìn)行一致性備份時(shí)干擾其他事務(wù)的正常進(jìn)行,實(shí)現(xiàn)了所謂的“熱備”,但是缺點(diǎn)在于其依賴事務(wù)型存儲(chǔ)引擎,對(duì)于使用MyISAM等不支持事務(wù)的存儲(chǔ)引擎的表,--single-transaction無法保證它們的數(shù)據(jù)一致性。
接著查看do_flush_tables_read_lock函數(shù)的源碼:
- 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"));
- }
可以看到邏輯比較簡(jiǎn)單,就是向server傳入執(zhí)行兩個(gè)query,依先后次序分別時(shí)FLUSH TABLES和FLUSH TABLES WITH READ LOCK,這里核心的動(dòng)作在于后面一個(gè)query,之所以需要前面的FLUSH TABLES是基于性能的考量,以盡可能減少加鎖對(duì)其他事務(wù)的影響。
3. 設(shè)置隔離級(jí)別為“可重復(fù)讀”,開始事務(wù)并創(chuàng)建快照
關(guān)表操作執(zhí)行完后,mysqldump接著開啟一個(gè)新事務(wù)并創(chuàng)建快照,對(duì)應(yīng)SQL如下圖:
這一部分在main函數(shù)中對(duì)應(yīng)的源碼為:
- if (opt_single_transaction && start_transaction(mysql)) goto err;
可以看到,只有在指定--single-transaction選項(xiàng)時(shí)這一步驟才會(huì)執(zhí)行。實(shí)際上這一步就是mysqldump實(shí)現(xiàn)一致性熱備的基礎(chǔ),我們接著查看start_transaction函數(shù)的源碼:
- static int start_transaction(MYSQL *mysql_con) {
- // 省略部分非關(guān)鍵代碼與注釋
- 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 */"));
- }
可以看到核心動(dòng)作是傳給server執(zhí)行的兩個(gè)query,先是SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ確保當(dāng)前會(huì)話的隔離級(jí)別是“可重復(fù)讀”,然后通過START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */來開始一個(gè)新事務(wù),產(chǎn)生一個(gè)新事務(wù)id,同時(shí)創(chuàng)建一個(gè)快照,dump過程中所使用的數(shù)據(jù)都基于這個(gè)快照。這樣,所有在該事務(wù)之后的事務(wù)所進(jìn)行的數(shù)據(jù)更新都會(huì)被過濾,備份的數(shù)據(jù)一致性因此得以保證。
但是,這樣的熱備方法,依賴于像InnoDB這樣支持事務(wù)的存儲(chǔ)引擎。相反,如MyISAM這種不支持事務(wù)的存儲(chǔ)引擎在備份過程中的數(shù)據(jù)一致性則不能被保證。
4. 獲取當(dāng)前binlog位置
隨后mysqldump執(zhí)行一個(gè)SHOW MASTER STATUS的query,以獲取當(dāng)前binlog的位置信息:
查看main函數(shù)中對(duì)應(yīng)部分的源碼可以看到,只有在指定--master-data選項(xiàng)時(shí)才會(huì)去獲取、記錄當(dāng)前的binlog位置:
- if (opt_master_data && do_show_master_status(mysql)) goto err;
查看do_show_master_status函數(shù)的實(shí)現(xiàn),可以看到核心動(dòng)作就是向server傳入執(zhí)行一個(gè)SHOW MASTER STATUS的query,最后將得到的binlog位置信息寫入dump結(jié)果中。
- 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結(jié)果
- 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會(huì)把前面操作中可能加了鎖的表全部解鎖:
查看main函數(shù)中對(duì)應(yīng)部分代碼:
- if (opt_single_transaction &&
- do_unlock_tables(mysql)) /* unlock but no commit! */
- goto err;
可以看到,只有在指定了--single-transaction選項(xiàng)時(shí)才會(huì)解鎖所有先前被加鎖的表,結(jié)合前面的思考可以推斷,--single-transaction下所進(jìn)行的備份通過事務(wù)性質(zhì)可以保證數(shù)據(jù)的一致性,沒有必要再保留對(duì)所有表所加的鎖,因此這里執(zhí)行解鎖,以免阻塞其他事務(wù)的進(jìn)行。
6. 對(duì)指定的庫與表進(jìn)行dump
前面的準(zhǔn)備操作進(jìn)行完成后,mysqldump開始正式進(jìn)行選定庫、表的dump操作:
對(duì)指定數(shù)據(jù)庫的實(shí)際dump由dump_databases函數(shù)執(zhí)行(當(dāng)指定了--all-databases要求dump所有庫時(shí),則由dump_all_databases函數(shù)執(zhí)行)。
查看dump_databases函數(shù)的實(shí)現(xiàn):
- 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每個(gè)指定的數(shù)據(jù)庫中所有的表,之后如果存在視圖,則將對(duì)應(yīng)視圖也進(jìn)行dump。我們的考察重點(diǎn)放在對(duì)表的dump上。
實(shí)際dump一個(gè)表的操作邏輯也比較清晰,就是先獲取表的結(jié)構(gòu)信息,得到表的創(chuàng)建語句,然后獲取表中每行的實(shí)際數(shù)據(jù)并生成對(duì)應(yīng)的insert語句。
不過,前面的general log中有個(gè)值得注意的點(diǎn)是SAVEPOINT的出現(xiàn),這一點(diǎn)在MySQL 5.5的mysqldump中是沒有的,查看dump_all_tables_in_db函數(shù)的實(shí)現(xiàn),可以找到設(shè)置savepoint的對(duì)應(yīng)代碼:
- // 創(chuàng)建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); // 對(duì)表進(jìn)行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);
- }
可以看到創(chuàng)建savepoint是在dump表之前,之后遍歷庫中的每個(gè)表,每當(dāng)dump完一個(gè)表之后,便執(zhí)行一次ROLLBACK TO SAVEPOINT sp操作,為什么呢?其實(shí)上面代碼的注釋已經(jīng)解釋清楚了:
簡(jiǎn)單來說,當(dāng)我們dump完一個(gè)表后后面都不再需要使用這個(gè)表,這時(shí)其他事務(wù)的DDL操作不會(huì)影響我們dump得到數(shù)據(jù)的正確性,增加savepoint的意義在于,假如我們要dump表A,savepoint記錄了dump表A之前尚未給表A加MDL鎖的狀態(tài),當(dāng)開始dump表A時(shí),由于要進(jìn)行一系列select操作,會(huì)給表A加上MDL鎖防止其他事務(wù)的DDL操作改變表結(jié)構(gòu)導(dǎo)致讀動(dòng)作出錯(cuò);最后當(dāng)對(duì)表A的dump完成后,后續(xù)都不會(huì)再訪問表A了,此時(shí)沒有釋放的MDL鎖沒有意義,反而會(huì)阻塞其他并行事務(wù)對(duì)表A的DDL操作。
對(duì)此,MySQL的解決方法是在訪問表A前通過SAVEPOINT sp記錄一個(gè)savepoint,在dump完表A之后通過ROLLBACK TO SAVEPOINT sp回到當(dāng)時(shí)的狀態(tài),即可釋放對(duì)表A加的MDL鎖,放行其他事務(wù)對(duì)該表的DDL操作。
小結(jié)
以上是mysqldump基于MySQL 8.0的一致性備份原理介紹,相比MySQL 5.5,現(xiàn)如今MySQL 8.0在mysqldump的實(shí)現(xiàn)存在一定改進(jìn),除了上面提到的savepoint機(jī)制是一個(gè)顯著區(qū)別之外,還有諸如對(duì)GTID的支持、對(duì)column statistics的dump操作在本文中沒有提及,但總體而言,mysqldump在一致性備份上的實(shí)現(xiàn)原理并沒有多少改變。
拓展閱讀——Percona的實(shí)現(xiàn)
MySQL從出現(xiàn)到普及,中途也出現(xiàn)了其他不少優(yōu)秀的發(fā)行版,MySQL中一致性備份的實(shí)現(xiàn)其實(shí)也并不完美,因此如果能夠考量其他發(fā)行版在這方面上的實(shí)現(xiàn),也是一件有意義的事情。
Backup Lock
在前面我有提到,mysqldump中--single-transaction選項(xiàng)所實(shí)現(xiàn)的一致性備份不需要對(duì)表加鎖,但這一特性基于事務(wù)型的存儲(chǔ)引擎,因此只對(duì)InnoDB表或使用其他事務(wù)型存儲(chǔ)引擎類型的表能夠保證備份時(shí)過濾掉其他并行事務(wù)的更新操作;但對(duì)使用了MyISAM這種不支持事務(wù)的存儲(chǔ)引擎的表,--single-transaction無法保證其數(shù)據(jù)的一致性,即若備份過程中出現(xiàn)了來自其他并行事務(wù)的更新操作,其很有可能被寫入了備份中。
既然如此,若想對(duì)MyISAM的表進(jìn)行備份,又想保證其一致性該怎么辦?一種方式可以是在執(zhí)行mysqldump時(shí)傳入--lock-all-tables選項(xiàng),這個(gè)選項(xiàng)會(huì)使得dump操作進(jìn)行之前執(zhí)行一個(gè)FLUSH TABLES WITH READ LOCK語句,并保證在dump的全程保持對(duì)所有表的讀鎖。但是無疑這是一種overkill,僅僅是為了保證一部分非事務(wù)型存儲(chǔ)引擎的表的一致性,就需要對(duì)所有表加鎖,進(jìn)而業(yè)務(wù)上所有對(duì)server的寫操作被阻塞一段時(shí)間(若備份的數(shù)據(jù)量大,這簡(jiǎn)直會(huì)造成一場(chǎng)災(zāi)難)。
這一問題,我尚未在MySQL 8.0中找到相應(yīng)的好的解決方式,不過Percona對(duì)此給出了一個(gè)方案:在Percona發(fā)行版的mysqldump中,執(zhí)行時(shí)可以傳入一個(gè)--lock-for-backup選項(xiàng),這個(gè)選項(xiàng)會(huì)使得mysqldump在dump之前,執(zhí)行一個(gè)LOCK TABLES FOR BACKUP語句,這是一個(gè)Percona獨(dú)有的query,其主要做以下幾件事情:
- 阻塞對(duì)MyISAM, MEMORY, CSV, ARCHIVE表的更新操作;
- 阻塞對(duì)任何表的DDL操作;
- 不阻塞對(duì)臨時(shí)表與log表的更新操作。
顯然,有了以上的特性,當(dāng)同時(shí)傳入--lock-for-backup與--single-transaction兩個(gè)選項(xiàng)同時(shí),mysqldump可以保證所有表的數(shù)據(jù)一致性,并且盡可能保證造成最少的線上業(yè)務(wù)干擾。
這一部分邏輯可以在Percona Server 8.0中mysqldump的代碼中找到,在main函數(shù)中:
- 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;
細(xì)心的朋友會(huì)發(fā)現(xiàn),這是對(duì)上面的“關(guān)表加讀鎖操作”進(jìn)行的邏輯改寫,其增加了一個(gè)else if邏輯分支,取代了之前的FLUSH TABLES; FLUSH TABLES WITH READ LOCK;操作,主要目的是為了與--single-transaction進(jìn)行的一致性備份更好地兼容,實(shí)現(xiàn)對(duì)線上業(yè)務(wù)盡可能少的阻塞。
接著查看do_lock_tables_for_backup函數(shù)的實(shí)現(xiàn),可以看到就是簡(jiǎn)單地向server傳入一個(gè)Percona獨(dú)有的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的實(shí)現(xiàn)中,有一個(gè)常用的選項(xiàng),仍然會(huì)導(dǎo)致“討人厭”的FLUSH TABLES WITH READ LOCK的執(zhí)行,即--master-data選項(xiàng)。
前面提到,--master-data選項(xiàng)要求在dump之后的結(jié)果中存有當(dāng)前備份開始時(shí)的binlog位置,為了滿足所獲得binlog位置的一致性,需要在執(zhí)行SHOW MASTER STATUS前,獲取對(duì)所有表的讀鎖以阻塞所有binlog的提交事件,因此要求執(zhí)行一次FLUSH TABLES WITH READ LOCK。但是有沒有更好的方式?Percona同樣給出了自己的解決方法。
在Percona Server中,新增了兩個(gè)全局status:Binlog_snapshot_file和Binlog_snapshot_pos,分別用來記錄當(dāng)前的binlog文件與binlog位置,通過SHOW STATUS LIKE 'binlog_snapshot_%'即可獲取兩個(gè)status的值。那么使用這個(gè)方式,跟SHOW MASTER STATUS有什么區(qū)別?
二者的區(qū)別在于,Binlog_snapshot_file和Binlog_snapshot_pos這兩個(gè)status具有事務(wù)性,只要在執(zhí)行SHOW STATUS LIKE 'binlog_snapshot_%'這個(gè)語句之前通過START TRANSACTION WITH CONSISTENT SNAPSHOT創(chuàng)建了新事務(wù)與一致性快照,Binlog_snapshot_file和Binlog_snapshot_pos所記錄的則正是該事務(wù)開始時(shí)的binlog文件與位置信息,進(jìn)而binlog信息的一致性得到保證,而這一過程的全程都不需要FLUSH TABLES WITH READ LOCK的執(zhí)行。
相對(duì)的,SHOW MASTER STATUS是不具備事務(wù)性的,每次執(zhí)行該語句返回的都是當(dāng)前最新的binlog位置信息,這也是為什么執(zhí)行它之前需要對(duì)所有表上讀鎖。


































