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】

相关文章
相关标签/搜索