共計 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 行業資訊頻道,感謝各位的閱讀!