当前位置: 首页 > 科技观察

mysqldump一致性热备份原理分析

时间:2023-03-12 04:49:31 科技观察

简介在日常的数据库运维中,经常需要对数据库进行热备份。双机热备的一个关键点是保证数据的一致性,即备份过程中发生的数据变化不会出现在备份结果中。mysqldump是实际场景中最常用的备份工具之一。通过选择合适的选项进行备份,mysqldump可以保证数据的一致性,同时尽可能保证正在进行的业务不受影响。那么mysqldump是如何实现一致性备份的呢?下面我将结合mysqldump过程中mysqld产生的通用日志和mysqldump的源码来讲解mysqldump一致性备份的原理。注:以下示例均基于MySQL8.0.18,不同版本mysqldump的部分实现会有所不同。首先用mysqldump进行一致备份:$mysqldump-uroot-p--skip-opt--default-character-set=utf8--single-transaction--master-data=2--no-autocommit-Bd1>backup.sql关键参数说明:--single-transaction:执行一致备份。--master-data=2:要求dump结果以注释的形式保存备份时的binlog位置信息。-B:指定要转储的数据库,其中d1是一个使用InnoDB作为存储引擎的库,只有一张表t1。执行完成后,可以得到mysqld生成的通用日志,记录了备份过程中mysqldump向服务器发送的指令。关键步骤我都用方框标出来了,具体解释请看下面。mysqldump一致性备份主要执行过程连接服务器,两次关闭所有表,第二次关闭表同时加读锁,设置隔离级别为“可重复读”,启动一个事务并创建一个snapshot,获取当前binlog位置,解锁指定pair的所有表Dumpthelibraryandtables下面将结合SQL内容和源码依次介绍以上主要步骤。流程分析1、连接服务器Mysqldump首先与服务器建立连接,初始化session,设置一些session级别的变量,对应SQL如下图。main函数中对应的源码是connect_to_db函数的调用:if(connect_to_db(current_host,current_user,opt_password)){free_resources();exit(EX_MYSQLERR);2。关闭所有表两次,第二次关闭表并同时加读锁关闭表并同时对所有表加读锁。对应的SQL如下:main函数中这部分对应的源码为:可以看到实际操作是由do_flush_tables_read_lock函数执行的,但是这里需要注意操作执行的前提条件。观察代码我们可以知道,这种关表操作只会在三种情况下执行:--lock-all-tables选项明确要求所有表都被锁定。二进制日志位置需要通过--master-data选项包含在转储结果中。单个事务的一致性备份通过--single-transaction指定,日志文件需要通过--flush-logs刷新。这里不难看出,除了第一种情况明确需要加锁外,情况3不需要其他事务在刷新日志前进行写操作,所以自然而然的给所有表加读锁。Case2要求dump结果准确记录dump执行瞬间的binlog位置。为了准确获取当前的binlog位置,自然需要对所有表加共享锁,防止其他并行事务写入导致binlog更新,所以这里是一个关表加读的动作锁。这是一个细节。我们知道--single-transaction选项可以进行一致性备份,那为什么只有--single-transaction选项时不需要关表加读锁呢?这是因为--single-transaction保证的一致性备份依赖于支持事务的存储引擎(如InnoDB)。后面会提到,mysqldump会通过执行STARTTRANSACTIONWITHCONSISTENTSNAPSHOTid创建一个数据库的当前快照和一个事务,所有在这个事务之后的事务执行的数据更新都会被过滤,以保证备份的一致性。这种方式的优点是在一致性备份时不会干扰其他事务的正常进行,实现了所谓的“热备份”,缺点是依赖于事务性存储引擎。对于MyISAM表等不支持事务的存储引擎,--single-transaction不能保证它们的数据一致性。然后查看do_flush_tables_read_lock函数源码:staticintdo_flush_tables_read_lock(MYSQL*mysql_con){return(mysql_query_with_error_report(mysql_con,0,((opt_master_data!=0)?"FLUSH/*!40101LOCAL*/TABLES":"FLUSHTABLES"))||mysql_query_wmysql_con,0,"FLUSHTABLESWITHREADLOCK"));}可以看到逻辑比较简单,就是依次向服务器传入并执行两个查询,分别是FLUSHTABLES和FLUSHTABLESWITHREADLOCK,这里的核心动作在于后面的查询,之所以需要前面的FLUSHTABLES,是基于性能的考虑,尽量减少加锁对其他事务的影响。3.将隔离级别设置为“可重复读”,启动事务并创建快照。关闭表后,mysqldump会启动一个新事务并创建一个快照。对应的SQL如下图所示:main函数中这部分对应的源码为:if(opt_single_transaction&&start_transaction(mysql))gotoerr;可以看到只有在指定了--single-transaction选项时才会执行这一步。其实这一步是mysqldump实现一致性热备的基础。我们接着看start_transaction函数的源码:staticintstart_transaction(MYSQL*mysql_con){//省略一些非关键代码和注释0,"STARTTRANSACTION""/*!40100WITHCONSISTENTSNAPSHOT*/"));}可以看到核心动作是传递给服务器执行的两个查询。首先,SETSESSIONTRANSACTIONISOLATIONLEVELREPEATABLEREAD确保当前会话的隔离级别为“可重复读”,然后通过STARTTRANSACTION/*!40100WITHCONSISTENTSNAPSHOT*/开始新的事务,生成新的事务id,并创建同时快照。dump过程中使用的数据都是基于这个快照的。这样,所有在本次事务之后的事务进行的数据更新都会被过滤掉,从而保证备份数据的一致性。但是,这样的双机热备方式依赖于像InnoDB这样支持事务的存储引擎。相反,不支持事务的存储引擎,如MyISAM,在备份过程中无法保证数据的一致性。4.获取当前binlog位置然后mysqldump执行一个SHOWMASTERSTATUS查询获取当前binlog位置信息:在main函数中可以看到对应部分的源码,只有当--master时才会获取指定-data选项,记录当前binlog位置:if(opt_master_data&&do_show_master_status(mysql))gotoerr;查看do_show_master_status函数的实现,可以看到核心动作是向服务器传递一个SHOWMASTERSTATUS查询,最后将获取到的binlog位置信息写入dump结果。staticintdo_show_master_status(MYSQL*mysql_con){MYSQL_ROWrow;MYSQL_RES*master;constchar*comment_prefix=(opt_master_data==MYSQL_OPT_MASTER_DATA_COMMENTED_SQL)?"--":"";if(mysql_query_with_error_report(mysql_con,&master,"USTERnel)reSTAT{row=mysql_fetch_row(master);if(row&&row[0]&&row[1]){print_comment(md_result_file,0,"\n--\n--开始复制的位置或时间点""recoveryfrom\n--\n\n");//写入dump结果fprintf(md_result_file,"%sCHANGEMASTERTOMASTER_LOG_FILE='%s',MASTER_LOG_POS=%s;\n",comment_prefix,row[0],row[1]);check_io(md_result_file);}//...}return0;}5.解锁所有表在正式开始dump操作之前,mysqldump会解锁所有可能在之前操作中被锁住的表:查看main函数中对应部分代码:如果(opt_single_transaction&&do_unlock_tables(mysql))/*unlockbutnocommit!*/gotoerr;可以看出只有指定了--single-transaction选项才会解锁所有之前锁定的表。co结合前面的思路,可以推断--single-transaction备份可以通过事务的性质来保证数据的一致性。没有必要对所有的表都加锁,所以在这里进行解锁,避免阻塞其他事务的进行。6、完成指定库表转储前的准备操作后,mysqldump开始正式执行所选库表的转储操作:指定数据库的实际转储由dump_databases函数执行(当--all-databases在指定需要转储所有库时,由dump_all_databases函数执行)。查看dump_databases函数的实现:staticintdump_databases(char**db_names){intresult=0;char**db;DBUG_TRACE;for(db=db_names;*db;db++){if(is_infoschema_db(*db))die(EX_USAGE,"转储\'%s\'DBcontentisnotsupported",*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;}}returnresult;}/*dump_databases*/逻辑比较清晰,先dump每个指定数据库中的所有表,然后如果有视图,也会dump对应的view.我们的调查集中在表的转储上。实际转储一张表的操作逻辑比较清晰,即先获取表的结构信息,获取建表语句,然后获取表中每一行的实际数据并生成对应的insert语句。不过在之前的通用日志中有一个值得注意的地方就是SAVEPOINT的出现,这在MySQL5.5的mysqldump中是没有的。查看dump_all_tables_in_db函数的实现,可以找到对应的设置保存点的代码:(mysql,0,"SAVEPOINTsp"))return1;}while((table=getTableName(0))){char*e??nd=my_stpcpy(afterdot,table);if(include_table(hash_key,end-hash_key)){dump_table(表,数据库);//转储表//省略部分代码...//ROLLBACK操作/**ROLLBACKTOSAVEPOINTin--single-transactionmodetoreleasemetadatalockonontablewhichwasalreadydumped.ThisallowstoavoidblockingconcurrentDDLonthistablewithoutsacrificingcorrectness,aswewon'ttaccesstablesecondtimeanddumpscreatedby--single-transactionmode'havevalidity-note-notesthistransactionpointatthestartof.在一般情况下具有并发DDL安全的单事务模式。它只是改善可能正在工作的人的情况。*/if(opt_single_transaction&&mysql_get_server_version(mysql)>=50500){verbose_msg("--Rollingbacktosavepointsp...\n");if(mysql_query_with_error_report(mysql,0,"ROLLBACKTOSAVEPOINTsp"))maybe_exit(EX_MYSQLERR);}可以看到创建了保存点在转储库中的每一张表之前和之后遍历。每当转储表时,都会执行ROLLBACKTOSAVEPOINTsp操作。为什么?其实上面代码的注释已经说明的很清楚了:简单来说就是我们dump了一张表之后,以后就不需要再用到这张表了。此时其他事务的DDL操作不会影响我们dump数据的正确性,增加savepoint的意义在于如果我们要dump表A,savepoint记录的是没有加MDL锁的状态在转储A表之前先到A表,当开始转储A表时,由于一系列的select操作,会在A表上加MDL锁,防止其他事务的DDL操作改变表结构而导致读取错误手术;最后,A表的dump完成后,就不会再有对A表的后续访问了,这时候还没有释放的MDL锁就没有意义了,会阻塞其他的并行操作。对于事务对A表的DDL操作,MySQL的解决方案是在访问A表之前通过SAVEPOINTsp记录一个savepoint,在dumpA表之后通过ROLLBACKTOSAVEPOINTsp返回到当前状态,然后释放A表的添加MDL锁允许其他事务对表执行DDL操作。总结以上就是对基于MySQL8.0的mysqldump一致性备份原理的介绍。与MySQL5.5相比,MySQL8.0现在在mysqldump的实现上有了一些改进。除了上面提到的savepoint机制,这是一个显着的区别,还有其他的比如GTID列统计的支持和dump操作本文没有提到,但总的来说,mysqldump在一致性备份上的实现原理有变化不大。延伸阅读——Percona对MySQL的实现从出现到普及,中间还出现了很多其他优秀的版本。MySQL中一致性备份的实现其实并不完美,所以如果能考虑其他版本在这方面的实现也是一件有意义的事情。备份锁前面提到过,mysqldump中的--single-transaction选项实现的一致性备份不需要锁表,但是这个特性是基于事务存储引擎的,所以它只使用InnoDB表或者其他事务存储引擎类型的表可以保证在备份时过滤掉其他并行事务的更新操作;但对于使用不支持事务的存储引擎的表,如MyISAM,--single-transaction不能保证其数据的一致性,即如果在备份过程中发生了来自其他并行事务的Update操作,则很有可能写入备份。既然如此,那我要备份MyISAM表,又想保证其一致性怎么办呢?一种方法是在执行mysqldump时传入--lock-all-tables选项。该选项将导致在转储操作之前执行FLUSHTABLESWITHREADLOCK语句,并确保在整个转储过程中保持所有表上的读锁。.但毫无疑问,这是一种大材小用。只是为了保证一些非事务性的存储引擎表的一致性,需要对所有表进行加锁,所有对服务器的业务写操作都会被阻塞一段时间(如果备份数据量很大,那就是灾难)。这个问题在MySQL8.0中我还没有找到很好的解决方案,不过Percona给出了解决方案:在Percona发行版的mysqldump中,执行时可以传入一个--lock-for-backup选项,这个选项会让mysqldump在转储之前执行一条LOCKTABLESFORBACKUP语句,这是Percona特有的查询,主要做了以下几件事:阻止对MyISAM、MEMORY、CSV、ARCHIVE表的更新;阻止任何表上的DDL操作;临时表和日志表的更新操作不会被阻塞。显然,有了以上特点,当同时传入--lock-for-backup和--single-transaction选项时,mysqldump可以保证所有表的数据一致性,保证对线上业务的干扰尽可能小。这部分逻辑可以参考PerconaServer8.0中mysqldump的代码,在main函数中:_lock(mysql)gotoerr;ftwrl_done=true;}elseif(opt_lock_for_backup&&do_lock_tables_for_backup(mysql))gotoerr;细心的朋友会发现,这是对上面“closetableplusreadlockoperation”的逻辑改写,增加了一个elseif逻辑分支来替代之前的FLUSHTABLES;带读锁的刷新表;operation,主要目的是为了更好的兼容--single-transaction进行的一致性备份,实现线上业务尽可能少的阻塞。再看do_lock_tables_for_backup函数的实现,可以看到它只是简单的传递了一条Percona独有的LOCKTABLESFORBACKUP语句给服务端:staticintdo_lock_tables_for_backup(MYSQL*mysql_con)noexcept{returnmysql_query_with_error_report(mysql_con,0,"LOCKTABLESFORBACKUP");}BinlogSnapshot在MySQL8.0的实现中,有一个常用的选项仍然会导致执行“讨厌的”FLUSHTABLESWITHREADLOCK,那就是--master-data选项。前面提到--master-data选项要求在dump后的结果中存放当前备份开始的binlog位置。为了满足获取到的binlog位置的一致性,在执行SHOWMASTERSTATUSReadlock之前需要获取所有表的信息来阻塞所有的binlogcommit事件,所以需要执行一次FLUSHTABLESWITHREADLOCK。但是有更好的方法吗?Percona也给出了自己的解决方案。在PerconaServer中,新增了两个全局状态:Binlog_snapshot_file和Binlog_snapshot_pos,分别用于记录当前的binlog文件和binlog位置,两个状态的值可以通过SHOWSTATUSLIKE'binlog_snapshot_%'获取。那么使用这个方法和SHOWMASTERSTATUS有什么区别呢?两者的区别在于Binlog_snapshot_file和Binlog_snapshot_pos这两个状态都是事务性的,只要在执行SHOWSTATUSLIKE'binlog_snapshot_%'语句之前通过STARTTRANSACTIONWITHCONSISTENTSNAPSHOT创建新的事务和一致性快照即可,如Binlog_snapshot_file所记录而Binlog_snapshot_pos是事务开始时的binlog文件和位置信息,保证了binlog信息的一致性,整个过程不需要执行FLUSHTABLESWITHREADLOCK。相反,SHOWMASTERSTATUS不是事务性的。语句每次执行都会返回最新的binlog位置信息,这就是为什么在执行之前需要对所有表进行读锁。