GTID 简介
GTID (global transaction identifier)在MySQL5.6时引入,GTID是事务的全局唯一标识。GTID结构如下
GTID = source_id:transaction_id
source_id:执行事务的原始实例的sever_uuid, 此事务GTID在备库apply时也不变。
transaction_id:事务的执行编号,binlog_order_commits=1时,此编号根据事务的提交顺序严格递增。GTID是在binlog flush时生成的,因此同一个serverid的GTID在binglog中是严格有序的。binlog_order_commits=0时,GTID在binlog中也是序的,但并不一定与提交的顺序一致。
binlog_order_commits=0会影响XtraBackup工具的备份,但不会影响innobackup工具的备份
XtraBackup会从innodb事务page获取最后提交事务的binlog位点信息,binlog_order_commits=0时事务提交顺序和binlog顺序不一定一致,这样此位点前可能存在部分prepare状态的事务,这些事务在备份恢复后会丢失。
而innobackup的位点信息是在加备份锁前提下从show master status/show slave status中获取的,位点前的事务都是已提交的。
支持GTID后,备库启动时不再需要通过位点信息从主库来拉取binlog,而是根据备库本身已执行和拉取的gtid去主库查找第一个未执行的GTID,从此GTID位置开始拉取binlog。
新增了COM_BINLOG_DUMP_GTID命令
-
备库
备库封装COM_BINLOG_DUMP_GTID命令,包含备库的gtid_executed(已执行的GTID和当前已拉取的GTID的并集)request_dump(): gtid_executed.add_gtid_set(mi->rli->get_gtid_set()) gtid_executed.add_gtid_set(gtid_state->get_executed_gtids())
- 主库
主库接收COM_BINLOG_DUMP_GTID命令,从最新的binlog开始反向遍历查找的binlog, 依次读取PREVIOUS_GTIDS_LOG_EVENT, 直到PREVIOUS_GTIDS_LOG_EVENT记录的gtid_set是备库发过来的gtid子集为止。
com_binlog_dump_gtid():
Binlog_sender::init
Binlog_sender::check_start_file(
mysql_bin_log.find_first_log_not_in_gtid_set
Binlog_sender::run
mysql5.7 gtid相对5.6主要有以下变化
- gtid_mode可以动态设置,支持gtid模式和非gtid模式之间的复制
- 增加了gtid_executed表
gtid_mode
MySQL5.7(>= 5.7.6) gtid_mode支持动态修改,gtid_mode取值可选择如下
OFF: Both new and replicated transactions must be anonymous.
OFF_PERMISSIVE: New transactions are anonymous. Replicated transactions can be either anonymous or GTID transactions.
ON_PERMISSIVE: New transactions are GTID transactions. Replicated transactions can be either anonymous or GTID transactions.
ON: Both new and replicated transactions must be GTID transactions.
OFF_PERMISSIVE时支持GTID模式的实例向非GTID模式的实例的复制。
ON_PERMISSIVE:时支持非GTID模式的实例向GTID模式的实例的复制。此模式下,可支持低版本实例(<=5.5)向5.7高版本GTID实例的复制,从而为低版本实例(不支持GTID)平滑升级为5.7GTID实例提供了便利。
需要吐槽的是MySQL5.6目前还不支持低版本实例(<=5.5)向5.6高版本GTID实例的复制, 需要修改代码打开此限制才可以。
另外,gtid_mode动态修改不支持跳跃修改。例如,如果当前值为OFF_PERMISSIV,只支持修改为OFF或ON_PERMISSIVE,不支持修改为ON。
MySQL 5.7 gtid_mode=on时需要设置enforce_gtid_consistency=1. MySQL5.6还需要另外设置 --log-bin, --log-slave-updates,而5.7是不需要的,这得益于5.7的gtid_executed表。
gtid_executed表
gtid_executed表存储的是已执行的GTID集合信息,此信息不一定是实时的。gtid_executed表结构如下
CREATE TABLE gtid_executed (
source_uuid CHAR(36) NOT NULL,
interval_start BIGINT(20) NOT NULL,
interval_end BIGINT(20) NOT NULL,
PRIMARY KEY (source_uuid, interval_start)
)
gtid_executed表的益处
有了gtid_executed表后,GTID模式下允许关闭binlog,允许设置log-slave-updates=0。这样带来的以下好处
- 开启GTID模式下,可以关闭备库的binlog或设置log-slave-updates=0,GTID信息仍然会保存在gtid_executed表中。这样备库依然可以正常复制,同时省去了记录binlog的开销。
AliSQL 5.6在这块也做了优化,备库SQL线程的产生的binlog只记录GTID EVENT信息,不记录实际操作的event, 因此减少了binglog的量, 并且能够保证正常的复制。
- 开启GTID模式下,由于gtid_executed表是持久化的,即使人为删除了备库的binlog,复制依然可以通过gtid_executed表恢复。
gtid_executed表的更新
gtid_executed在binlog开启和关闭的情况下都会更新
- binlog开启
每次rotate或shutdown时存储PREVIOUS_GTIDS_LOG_EVENT,只记录最后一个binlog的gtid信息。参考save_gtids_of_last_binlog_into_table - binlog关闭或log_slave_updates=0
每次事务提交时都存储GTID
MYSQL_BIN_LOG::gtid_end_transaction():
if (!opt_bin_log || (thd->slave_thread && !opt_log_slave_updates))
gtid_state->save(thd)
ha_commit_trans():
if (!opt_bin_log || (thd->slave_thread && !opt_log_slave_updates))
gtid_state->save(thd)
- reset master
reset master 会重置表,以delete方式删除所有数据(非truncate)
Gtid_table_persistor::reset
delete_all(table)
gtid_executed表的compress
更新gtid_executed表信息时,每次都是insert一条数据,而不是update方式,update容易产生行冲突,insert可以提高并发。而insert的副作用是导致gtid_executed表行记录数不断增加。因此,专门提供了一个compress线程用来压缩gtid_executed表。
以源码中的注释来说明compress过程,具体可参考Gtid_table_persistor::compress_in_single_transaction
Read each row by the PK(sid, gno_start) in increasing order,
compress the first consecutive range of gtids.
For example,
1 1
2 2
3 3
6 6
7 7
8 8
After the compression, the gtids in the table is compressed as following:
1 3
6 6
7 7
8 8
全表扫描,依次找到连续一行删一行,删除(2,2),(3,3),最后更新第一行的结束值(1,1)更新为(1,3)
这里有个有趣的bug, 设置super_read_only导致compress事务在提交时检查read_only失败,然后回滚事务。随着gtid_executed表数据的增加,compress线程的事务越来越大,更新失败然后回滚的代价越来越大。
compress线程是被动触发的
mysql_cond_wait(&COND_compress_gtid_table, &LOCK_compress_gtid_table);
以下两种情况会唤醒compress线程
mysql_cond_signal(&COND_compress_gtid_table);
- 插入单个GTID时通过参数gtid_executed_compression_period来控制唤醒compress,此种情况发生在binlog关闭或log_slave_updates=0事务提交时。
- 插入GTID集合每次都会唤醒compress,这种情况发生在binlog开启时, binlog rotate或实例关闭时。
启动时gtid_executed表的处理
实例启动时,会读取gtid_execute表信息来构建以下信息
executed_gtids:已执行的gtid信息,是gtid_executed表和binlog中gtid的并集。即gtid_executed。
lost_gtids:已经purged的gtid。即gtid_purged。
- 构建executed_gtids
1 读gtid_executed表的GTID信息赋值给exeucted_gtids, 参考read_gtid_executed_from_table
2 将binlog中比gtid_executed表中多的GTID补进来
gitds_in_binlog_not_in_table.add_gtid_set(>ids_in_binlog);
gtids_in_binlog_not_in_table.remove_gtid_set(executed_gtids);
gtid_state->save(>ids_in_binlog_not_in_table) //将binlog比表中多的补进来
executed_gtids->add_gtid_set(>ids_in_binlog_not_in_table);
gtids_in_binlog是逆向查找binlog,直到找到第一个包含PREVIOUS_GTIDS_LOG_EVENT的binlog为止, 读取此binlog文件的PREVIOUS_GTIDS_LOG_EVENT和GTID_LOG_EVENT构成gtids_in_binlog
-
构建 lost_gtids
lost_gtids = executed_gtids - (gtids_in_binlog - purged_gtids_from_binlog) = gtids_only_in_table + purged_gtids_from_binlog;
purged_gtids_from_binlog是正向查找binlog,可以从第一个包含GTID_LOG_EVENT的binlog的PREVIOUS_GTIDS_LOG_EVENT中获取。
有一种情况比较特殊,5.6 升级5.7时,有一种情况会导致binlog中有PREVIOUS_GTIDS_LOG_EVENT但没有GTID_LOG_EVENT。如下面的注释所示,真正的purged_gtids_from_binlog应该从master-bin.N+2的PREVIOUS_GTIDS_LOG_EVENT中获取
/*
This branch is only reacheable by a binary log. The relay log
don't need to get lost_gtids information.
A 5.6 server sets GTID_PURGED by rotating the binary log.
A 5.6 server that had recently enabled GTIDs and set GTID_PURGED
would have a sequence of binary logs like:
master-bin.N : No PREVIOUS_GTIDS (GTID wasn't enabled)
master-bin.N+1: Has an empty PREVIOUS_GTIDS and a ROTATE
(GTID was enabled on startup)
master-bin.N+2: Has a PREVIOUS_GTIDS with the content set by a
SET @@GLOBAL.GTID_PURGED + has GTIDs of some
transactions.
If this 5.6 server be upgraded to 5.7 keeping its binary log files,
this routine will have to find the first binary log that contains a
PREVIOUS_GTIDS + a GTID event to ensure that the content of the
GTID_PURGED will be correctly set (assuming binlog_gtid_simple_recovery
is not enabled).
*/
原因在于MySQL5.6在set gtid_purged时是通过切换文件(rotate_and_purge )将gtid_purged存储在PREVIOUS_GTIDS_LOG_EVENT中.
而MySQL5.7在set gtid_purged时并不切换文件,gtid_purged直接存储到gtid_executed表中。
参数 binlog_gtid_simple_recovery
官网对binlog_gtid_simple_recovery=false进行了详细的解释。主要说明了binlog_gtid_simple_recovery=false时正向查找binlog获取gtid_purged(对应上节的lost_gtids)和逆向查找binlog获取gtid_executed(对应上节的executed_gtids)可能需要遍历较多的binlog文件,上节也介绍了遍历查找的方法。
但官网只是简单的介绍了binlog_gtid_simple_recovery=true时只需要查找最新或最老的binlog文件即可,至于为什么可以这样做没有明确说明。
以下是我的个人理解,
对于gtid_executed只需要读最新的binlog文件,即使最新的binlog文件没有PREVIOUS_GTIDS_LOG_EVENT也没有关系,因为最老的PREVIOUS_GTIDS_LOG_EVENT在binlog roate时已经写入gtid_executed表,根据上节的gtid_executed获取逻辑会读取gtid_executed表,最后获取的gtid_executed是完整的。
对于gtid_purged只需要读取老的binlog文件, 如果最老的binlog文件没有PREVIOUS_GTIDS_LOG_EVENT,同时最新的binlog文件也没有PREVIOUS_GTIDS_LOG_EVENT的情况下,根据上节的lost_gtids恢复逻辑
lost_gtids = executed_gtids -
(gtids_in_binlog - purged_gtids_from_binlog)
gtids_in_binlog和purged_gtids_from_binlog都为空,最后lost_gtids=executed_gtids,这显然是不正确的。这里我认为lost_gtids并不是一个重要的值,只在set gtid_purge时会修改,即使不正确也不影响正常复制。
GTID三个限制
enforce-gtid-consistency=ON时,以下三类语句时不支持的
- CREATE TABLE ... SELECT statements
- CREATE TEMPORARY TABLE or DROP TEMPORARY TABLE statements inside transactions
- Transactions or statements that update both transactional and nontransactional tables. There is an exception that nontransactional DML is allowed in the same transaction or in the same statement as transactional DML, if all nontransactional tables are temporary.
而实际上这个限制没有必要这么严格,
- CREATE TABLE ... SELECT statements
对于binlog_format=row, gtid_next='automatic'时可以放开限制。
生成的binlog包含两个GTID, 一个是建表语句,一个是包含多个insert的事务。
- 事务中包含事务表和非事务表
对于gtid_next='automatic'时可以放开限制。
生成的binlog包含两个GTID, 一个是所有非事务表的,一个是所有事务表的。
对update多表(包含事务表和非事务表)此时需额外要求binlog_format=row。
总结
MySQL 5.7 在GTID上有了较大改进,但GTID的三个使用限制仍然存在,期待后期有所改进。