GTID复制的工作原理

参考自:https://dev.mysql.com/doc/refman/5.7/en/replication-gtids-lifecycle.html

笔记说明:

本文翻译自官网,当然会根据语义做一些解释或总结简化,有些地方为了理解顺畅也有删减,有些地方直接翻为中文略显生硬,如有疑问请直接参考上述链接中的原文。

本文主要介绍GTID的生成方式、基于GTID的主从同步时的工作机制,对于如何搭建GTID主从复制以及GTID主从复制为何可以实现并行复制的原理未做详细介绍,后者原理可以参考innodb二阶段日志提交机制和组提交解析理解。

关于如何搭建基于GTID的主从复制以及在主从同步失败时进行抢救,参考:How to create/restore a slave using GTID replication in MySQL 5.6

基于GTID的主从同步在出现问题时,除了手动的重新开启下同步进程你能做的操作很少,相比之下原始的基于binlog pos的复制比较灵活,为了避免这种情况发生有必要探究一下基于GTID复制的工作机制,以便在主从同步异常时有效的进行修复。

本文的主要目的就是搞清GTID的生成和使用机制,搞清基于GTID复制的主要流程和核心参数,保证在GTID复制出现问题时可以通过灵活的处理相关参数来拯救主从复制。

一、GTID的生命周期如下:

1. 当事务于主库执行时,系统会为事务分配一个由server uuid加序列号组成的GTID(当然读事务或者被主动过滤掉的事务不会被分配GTID),写binlog日志时此GTID标志着一个事务的开始。GTID的格式如下所示:

GTID = source_id:transaction_id
# source_id为server的uuid
# transaction_id是一个表示事务执行顺序的序列号,例如第一个执行的事务transaction_id为1,第10个为10
# GTID = <server uuid>:1-10表示从1-10的10个事务的集合,称作gtid set

2. binlog中写GTID的event被称作Gtid_log_event,当binlog切换或者mysql服务关闭时,之前binlog中的所有gtid都会被加入mysql.gtid_executed表中。此表内容如下(slave中此表记录数会有多条,取决于主从个数):

mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 71cf4b9d-8343-11e8-97f1-a0d3c1f25190 |              1 |    653948549 |
+--------------------------------------+----------------+--------------+

3. 当GTID被分配且事务被提交后,他会被迅速的以一种外部的、非原子性的方式加入@@GLOBAL.gtid_executed参数中,这个参数包含了所有被提交的GTID事务(其实他是一个GTID范围值,例如71cf4b9d-8343-11e8-97f1-a0d3c1f25190:1-10),@@GLOBAL.gtid_executed也被用于主从复制,表示数据库当前已经执行到了哪个事务。相比之下mysql.gtid_executed不能用于标识主库当前事务进度,毕竟他只有在binlog切换时才会将日志中的GTID加入(mysql服务关闭也相当于binlog切换)。

4. 在主从首次同步时(master_auto_position=1),slave会通过gtid协议将自己已经执行的gtid set(@@global.gtid_executed)发给master,master比较后从首个未被执行的GTID事务开始主从同步。

5. 当事务随binlog被传输至slave后,slave每次读到Gtid_log_event就把自己的gtid_next参数设为此GTID,需要注意的是这里的gtid_next是在复制进程的session context中自动设置的(由binlog提供的语句),不同于show variables like 'gtid_next';这里看到的结果默认为AUTOMATIC,是当前会话本身的gtid_next,这是个session级别的参数。

6. 当开启并行复制时,slave会读取并检查事务的GTID确保当前GTID事务未被在slave执行过,且没有并行进程在读取并执行此事务,如果有并行复制进程正在应用此事务那么slave server只会允许一个进程继续执行,@@GLOBAL.gtid_owned参数展示了当前哪个并行复制进程在执行什么事务。

7. 同样的,在slave上如果开启了binlog,GTID也会以Gtid_log_event事件写入binlog,同时binlog切换或者mysql服务关闭时,当前binlog中的所有gtid都会被加入mysql.gtid_executed表中。

8. 在备库上如果未开启binlog,那么GTID会被直接持久化到mysql.gtid_executed表中,在这种情况下slave的mysql.gtid_executed表包含了所有已经被执行的事务。需要注意的是在mysql5.7中,向mysql.gtid_executed表插入GTID的操作与DML操作是原子性的,对于DDL操作则不是,因此如果slave在执行DDL操作的过程中异常中断那么GTID机制可能会失效。在mysql8.0中这个问题已经得到解决,DDL操作的GTID插入也是原子性的。

9. 同第3条中所说的一样,slave上的事务被执行后GTID也会被迅速的以一种外部的、非原子性的方式加入@@GLOBAL.gtid_executed参数中,在slave的binlog未开启时mysql.gtid_executed中记载的已提交事务事实上与@@GLOBAL.gtid_executed记载的是一致的,如果slave的binlog已开启那么mysql.gtid_executed的GTID事务集就没有@@GLOBAL.gtid_executed全了。

主从同步补充说明:

slave会完全继承master的GTID,因此如果slave的binlog开启那么即便事务在slave上什么也没做,还是会产生一个Gtid_log_event,只不过之后会跟一个空事务,即begin;commit;。

这种slave空事务的可能产生场景是在master上手动设置了gtid_next并且什么都没做,这样就会在binlog里产生一个空事务,虽然这个空事务什么都没做,slave依然要把他写入自己的binlog中。

这样做的好处是可以使mysql.gtid_executed和@@GLOBAL.gtid_executed记载的gtid set保持连贯。另一个好处是在主从同步中断后重新开启同步时可以防止再次同步那些过滤掉的GTID事务。

这可以印证一种slave跳过错误事务的方法,即stop slave;set gtid_next='要跳过的事务GTID';begin;commit;set gtid_next=AUTOMATIC;start slave;但是在跳过错误事务之前,请使用show binlog events in 'log_name' from pos limit ...语句和mysqlbinlog工具确保你要跳过的事务不包含重要的数据更改。 

并行复制的情境下,slave的GTID事务的提交顺序可能与主库不一样,因为binlog的组提交机制允许同一组内的日志记载的事务并行执行,其原理这里不详细描述,这会导致@@global.gtid_executed参数的值可能包含gtid gap,即@@global.gtid_executed中包含的事务序列号可能是不连贯的,如果使用stop slave来停止主从同步那么复制进行会先把这些gap填上再停止,但如果主库或从库是异常关机的那么这些gap可能会依然存在,这会导致你需要重新搭建主从复制,除非你自己确认这些gap事务是无影响可以跳过的。

二、一些GTID分配的其他情况:

GTID并非只会被分配给事务,一个事务也可能会被分配多个GTID。

首先解释第一句:

除了正常的DML,DDL事务外,创建、修改、删除一个database也会被分配一个GTID,此外procedure, function, trigger, event, view, user, role等对象的增删改也会被分配一个GTID,此外grant操作也会被分配一个GTID。

另外对于类似myisam类型的表,虽然不涉及事务也还是会被分配GTID的,而且一旦此类不支持事务的存储引擎的表的更改发生binlog落盘的错误时,binlog就会记载一次gap,对于这个binlog gap也会分配一个GTID给这个log event。

如之前所说的,master上rollback的事务不会被分配GTID,此外通过SET @@SESSION.sql_log_bin = 0;主动关闭会话binlog当然也不会为事务分配GTID了,毕竟连binlog都不会产生。

然后解释第二句:

对于XA事务(分布式事务),一个事务会有多个GTID,而且就算其中一段事务被回滚也会被分配一个GTID。

此外在以下几种情况下一条语句会产生多个事务,因此会被分配多个GTID:

  • 一个存储过程中包含多个事务。
  • 使用一条drop table语句drop多个不同类型的表。
  • CREATE TABLE ... SELECT语句,create table产生一个GTID,插入数据产生一个GTID。

三、系统参数gtid_next和gtid_purged以及gtid_executed:

gtid_next:

  • 当gtid_next设为AUTOMATIC(默认)时,每个事务被提交时都会分配一个自增的GTID(这里主要是说master),如果事务被回滚那么GTID不会被分配。
  • 如果将gtid_next设为一个合法的GTID值,那么mysql server就会将此GTID设为你当前事务的GTID,即便你不作任何操作甚至设置sql_log_bin=0,此GTID也会被记录入binlog。

需要注意的是如果你手动的将@@session.gtid_next设为一个GTID值,那么在执行完事务后请务必重新将其设置为AUTOMATIC。

当slave的SQL thread进程应用事务时,他们会根据binlog日志的记载将自己的@@SESSION.gtid_next设为即将要重放的事务的GTID,等到重放完毕后,还会把这个GTID加入@@global.gtid_executed。

总结下就是:此参数在事实上提供了手动跳过事务的方法,在主从同步需要跳过错误事务时很有用。

gtid_purged和gtid_executed:

此参数表示所有已经被提交但是在所有binlog中都找不到相关GTID的事务们,gtid_purged是gtid_executed的一个子集,其涉及到的场景主要是:

  • slave上禁用了binlog,那么所有重放的GTID事务都会被加入gtid_purged。
  • 包含相应GTID事务的binlog已经被删除,这些已提交事务会被加入gtid_purged。
  • 通过SET @@GLOBAL.gtid_purged语句手动的将某些gtid加入gtid_purged的gtid set。

你可以通过修改@@GLOBAL.gtid_purged的值告诉slave:虽然已经无法在binlog中找到相关的GTID记录了,但放心这些gtid set内的事务已经被应用过了,本人亲自作保的!

此参数一个经典的应用场景是:你在搭建主从时使用mysqldump在slave server上恢复了备份,但是因为备份前未开启GTID导致恢复后的数据库并没有gtid_executed和gtid_purged信息,因此指定gtid_mode=ON以及master_auto_position=1开启GTID同步时slave尝试同步master从uuid:1开始的所有GTID事务,这当然不是我们想要的也肯定会遇到错误。在mysql 5.7之后你可以通过只修改@@GLOBAL.gtid_purged的值来为slave同步的master_auto_position=1指明起始GTID。

gtid_executed和gtid_purged的值是在数据库服务启动时初始化的,每个binlog的初始event(其实是第2个啦,第一个是pos=4的Format_desc)都是Previous_gtids_log_event(通过SHOW BINLOG EVENTS [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count]查看),这个event包含了之前所有binlog files的GTID set(一般是uuid:1-<最新的事务序列号>),gtid_executed只需要看最新一个binlog的Previous_gtids_log_event的值即可,gtid_purged的值则是最新的binlog文件的Previous_gtids_log_event的值减去最老binlog文件的Previous_gtids_log_event的值。

gtid_executed的值会随着事务的生成不断更新,但不包含@@GLOBAL.gtid_owned的GTID,@@GLOBAL.gtid_owned表示当前数据库正在执行的GTID事务。

在MySQL5.7.7版本之前,gtid_executed和gtid_purged的值可能会错误的生成,这姑且一个BUG,你可能需要将 binlog_gtid_simple_recovery 设为FALSE重新启动DB服务器来处理这个BUG,将此参数设为FALSE后,DB server在启动时会遍历所有binlog文件以便正确计算gtid_executed和gtid_purged的值,如果你有很多未开启GTID模式时就存在的binlog,可能会导致重启花费很长时间。

因此还是推荐在mysql5.7.8之后的版本上启用GTID复制,以前的版本能用传统复制就用传统复制吧。

四、通过reset master重置GTID的自增序列号

如果你想要重置GTID的事务序列号,那么需要执行下reset master,这会清除@@global.gtid_executed和@@global.gtid_purged的值,并且会清除以前的binlog和序列号,重新开启一个类似于binlog.0001的binlog,如果未开启binlog,那么reset master至少也会清除掉@@global.gtid_executed和@@global.gtid_purged的值。

请谨慎的使用reset master以防止主从同步的事务丢失,为很好的把握此语句的使用情景需要非常了解他的作用和影响,以下为使用reset master时的一些注意事项:

在reset master之前,请确保你已经备份了当前数据库的binlog文件和binlog index file,同时确保记下当前的@@global.gtid_executed和@@global.gtid_purged值。

reset master实际上做了以下操作:

  • 将gtid_purged参数设为空字符
  • 将gtid_executed设为空字符
  • 清空mysql.gtid_executed表
  • 如果DB server开启了binlog,那么reset master还会清除所有binlog文件和binlog index file,然后以初始的自增序列号1开启一个新的binlog

此外要说明的是,无论是reset slave还是reset slave all都不会清除@@global.gtid_executed和@@global.gtid_purged的值。

想建一个数据库技术和编程技术的交流群,用于磨炼提升技术能力,目前主要专注于Golang和Python以及TiDB,MySQL数据库,群号:231338927,建群日期:2019.04.26,截止2021.02.01人数:300人 ... 如发现博客错误,可直接留言指正,感谢。
posted @ 2019-09-17 15:22  realcp1018  阅读(1605)  评论(0编辑  收藏  举报