mysqldump一致性熱備原理剖析

引言mysql

在平常數據庫運維中,常常要對數據庫進行熱備。熱備的一個關鍵點是保證數據的一致性,即在備份進行時發生的數據更改,不會在備份結果中出現。mysqldump是實際場景中最常使用的備份工具之一,經過選擇合適的選項作備份,mysqldump能夠保證數據的一致性,同時儘量保證進行中的業務不受影響。程序員

那麼mysqldump是如何實現一致性備份的?如下我將結合mysqldump過程當中mysqld生成的general log與mysqldump的源碼來解釋mysqldump一致性備份的原理。sql

注:如下的實例基於MySQL 8.0.18,在不一樣版本上mysqldump的部分實現會有不一樣數據庫

首先用mysqldump執行一次一致性備份:session

$ mysqldump -uroot -p --skip-opt --default-character-set=utf8  --single-transaction --master-data=2 --no-autocommit -B d1> backup.sql

關鍵參數解釋:運維

  •  --single-transaction:執行一致性備份。ide

  •  --master-data=2:要求dump結果中以註釋形式保存備份時的binlog位置信息。函數

  •  -B:指定要dump的數據庫,在這裏d1是一個使用InnoDB做爲存儲引擎的庫,其中只有一個表t1。工具

執行完成後能夠獲得mysqld生成的general log,裏面記錄了mysqldump在備份過程當中傳給server的指令。性能

其中關鍵的步驟我用框框做了標記,具體的解釋請看下文。

mysqldump一致性備份的主要執行流程

  1.  鏈接server

  2.  兩次關閉全部表,第二次關表同時加讀鎖

  3.  設置隔離級別爲「可重複讀」,開始事務並建立快照

  4.  獲取當前binlog位置

  5.  解鎖全部表

  6.  對指定的庫與表進行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函數進行,可是這裏須要注意操做執行的前提條件,觀察代碼咱們能夠知道,這個關表操做只會在三種狀況下進行:

  1.  經過--lock-all-tables選項顯式要求給全部表加鎖。

  2.  經過--master-data選項要求dump出來的結果中包含binlog位置。

  3.  經過--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在一致性備份上的實現原理並無多少改變。

拓展閱讀——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位置信息,這也是爲何執行它以前須要對全部表上讀鎖。

【編輯推薦】

  1. 程序員修神之路--略懂數據庫集羣讀寫分離而已

  2. 手把手教你如何進行業務系統數據庫技術選型

  3. 「雲+數據庫」鋪就數智轉型之路

  4. 開源Graviton數據庫,號稱「用於鍵值存儲的ZFS」

  5. 數十億的工業物聯網設備可能存在缺陷

【責任編輯:龐桂玉 TEL:(010)68476606】

相關文章
相關標籤/搜索