今天简单介绍一下GTID,并有部分相关实验。
GTID相信大家都不陌生,GTID的英文全称为Global Transaction Identifier,在MySQL主从架构中应用广泛。
GTID是由“UUID:事务号“组成的,GTID是基于事务的,在主从架构中,在主库每提交一个事务都会对应生成一个GTID号,GTID支持语句和行格式的复制,而且在主库提交的事务只会在从库应用一次,保证了一致性。
一、下面说一下GTID的优缺点:
1、GTID优点:
①复制安全性高,搭建主从相比与传统的(基于binlog和position号)复制要简单;
②GTID在主库是连续的,保证了数据的一致性;
③故障切换时间更短,降低服务故障时间等。
2、GTID缺点:
①主从表存储引擎必须一致,不能一个innodb一个myisam,这样会导致主备数据不一致,因为GTID是和事务之间一一对应的;
②不允许一个sql同时更新事务引擎表和非事务引擎表;
③在主从模式下,需要主库从库同时开启和关闭gtid;
④不支持create table .... as select 语句,在主库会直接报错的;
⑤不支持create/drop temporary table语句;
⑥不支持传统的复制模式,跳过错误的语句(sql_slave_skip_counter)。
二、GTID部分相关参数介绍(参考官方文档,以及自己总结):
①gtid_mode:控制是否启用gtid,on/on_permissiv/off_perissiv/off;
②enforce_gtid_consistency:用于保证GTID一致性的,有on/off/warn三个值
on:不允许任何事务违反gtid一致性;
off:允许所有事务违反gtid一致性;
warn:允许所有事务违反gtid一致性,但是这种情况下会生成告警,mysql5.7.6新增。
③gtid_executed:它是一个gtid的范围,表示的是已经执行过的所有的gtid事务集或者是由于语句人为设置的gtid。
④gtid_purged:表示的是已经执行过但是已经被purge掉的gtid,也是一个范围,它是executed的一个子集。在以下几种情况下,gtid_purged会有值:
禁用二进制日志的情况下,提交事务的GTID;
写入二进制日志的GTID已经被删除了;
使用语句显示的指定gtid purged:set @@global.gtid_purged=...。
⑤gtid_next:是会话级别的变量,对于提交的事务,会自动分配新的gtid,默认值为automatic,也可以显示指定gtid_next,来指定下一个事务的GTID号。
⑥gtid_owned:该变量主要提供内部使用,保存的是服务器上当前正在使用的所有GTID列表,以及拥有它的线程的ID。
三、进行几个GTID的测试:
测试环境,已经开启GTID复制模式,搭建过程省略。
1、1236报错的情况:
情况1:
主库真正的purge了正在使用的binlog,或者执行了reset master;,这种情况不用过多解释,从库肯定不能找到主库的binlog而报错了。
情况2:
从库gtid不连续,出现了空洞。
1) 如何模拟空洞的出现:
①在从库参数文件中指定slave_skip_errors=1050;
②在从库test库中创建一张表;
③在主库test库中创建同样的一张表,此时如果没有skip参数的话,从库肯定会报错;
④此时在主库向该表中插入一条数据,此时从库gtid就出现了空洞;
⑤查看show slave status\G;
Executed_Gtid_Set: 9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1737:1739-1740,
b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14
对于gtid空洞的情况,假如在需要进行维护的时候,需要重新进行执行change master to语句,可能会出现1236报错,模拟如下:
2)前期准备:
①切换和删除binlog:
由于测试库没有什么业务,binlog没有切换,为了模拟生产上binlog的情况,我们手动进行binlog切换和删除。
mysql> flush logs; //首先刷新一个日志,这样从库也就指向了新的binlog
Query OK, 0 rows affected (0.04 sec)
mysql> show binary logs; //查看存在的binlog
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000022 | 2216 |
| mysql-bin.000023 | 194 |
+------------------+-----------+
2 rows in set (0.01 sec)
mysql> purge binary logs to 'mysql-bin.000023'; //将该binlog之前的binlog全部purge掉,这样也就 模拟了主库binlog超过了expire log days时间后对binlog进行的清理,这样binlog里面之前的gtid对应的操作都不存在了。
mysql> show binary logs; //再次查看只留下了最新的binlog
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000023 | 194 |
+------------------+-----------+
1 row in set (0.00 sec)
3) 复现1236报错:
此时主库老的gtid对应的操作,在binlog中已经被purge了,而从库由于skip参数已经出现了gtid空洞,此时简单的stop slave;start slave;是不会有什么问题的。
查看gtid相关信息:
mysql> show global variables like '%gtid%';
gtid_executed:9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1737:1739-1740,
b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14 //这个值是和show slave status对应的。
gtid_purged :9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1732,
b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-11 //由于从库的binlog没有purge,所以它的值没有变化。
如果此时由于某种原因,我们需要重新执行change master to...操作,此时就会报错1236:
mysql> stop slave;
mysql> change master to .....
mysql> start slave;
mysql> show slave status\G;
Last_IO_Errno:1236
Last_IO_Error:Got fatal error 1236 from master when reading data from binary log: 'The slave is connecting using CHANGE MASTER TO
MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires.'
这是因为:当MASTER_AUTO_POSITION = 1时,在初始连接握手时也就是执行change的时候,从库发送一个GTID集,其中包含它已经接收或提交的事务,或同时包含这两种事务(也就是executed的值)。主库的响应方式是发送所有记录在二进制日志中的事务,这些事务的GTID不在副本发送的GTID集合中。这种交换确保主库只发送从库还没有记录或提交的GTID的事务。
有了上面的理解就不难明白:从库发送的gtid集为:9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1737:1739-1740,b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14,是不连续的,缺少9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1738,而主库会发送到从库的是不包含在从库发送出来的gtid的,所以从库还会去主库找9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1738这个gtid,而该gtid所在的binlog已经被purge掉了,所以会报1236的错误。
3) 修复:
我们需要在从库执行purge,将gtid空洞补齐:
mysql> stop slave;
mysql> reset master;
mysql> set global gtid_purged='9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1740,b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14';
mysql> reset slave all;
mysql> change master to ....;
mysql> start slave;
mysql> show slave status\G;
Slave_IO_Running:Yes
Slave_SQL_Running:Yes
注意:在执行set global gtid_purged的时候,gtid_purged需要为空,我们需要执行reset master将从库的gtid_purged清空,这样gtid就会连续了,从库发送gtid集的时候就包含了空洞这部分gtid,所以主库也就不会向从库发送该gtid,从库也就不会去主库找该gtid,也就会从最新的gtid去拉取,这样就不会出现1236报错了。
当然还有其他情况会出现1236的报错,比如主库中包含其他实例的gtid(可能是该主库是从其他实例上摘下来的从库),也就是说主库的gtid集包含没在该架构中的实例的gtid,而从库不包含该实例的gtid,当从库执行change的时候大概率也会有1236报错。大概就是这一个意思,这里就不做测试了,想要说明的就是从库在执行change的时候它包含的gtid集要和主库的一致,也就是说主库gtid集中有的uuid,从库中也得有,不管该gtid有没有变化有没有用到。
2、从库gtid_purged值什么时候会变化:
情况1:
从库手动执行set global gtid_purged=....;这个上面已经执行过了。
情况2:
禁用二进制日志的情况下或者关闭log_slave_updates参数,提交事务的GTID;这种情况说的是当从库在禁用binlog的情况下,gtid purged该值是都在变化的,因为从库没有binlog了,也就没有purge binlog这一说了。
情况3:
当从库binlog被purge掉的时候。
此时的gtid executed和gtid purged分别为:
gtid_executed :9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1748,b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14
gtid_purged:9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1740,b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14
需要注意的是这两个值平时是不一样的,executed表示已经接收执行到的gtid,而purged表示已经被purge掉的gtid。
我们先flush一个binlog,然后再purge:
mysql> flush logs;
Query OK, 0 rows affected (0.09 sec)
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 2217 |
| mysql-bin.000002 | 194 |
+------------------+-----------+
2 rows in set (0.00 sec)
mysql> purge binary logs to 'mysql-bin.000002';
Query OK, 0 rows affected (0.11 sec)
mysql> show global variables like '%gtid%';
gtid_executed:9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1748,b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14
gtid_purged :9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1-1748,b30eac1b-7398-11ec-befa-fa7f4a6c5000:1-14
可以看到gtid_purged的值发生了变化,包含的是我们已经purge掉的gtid,因为测试库没有业务,所以executed和purged在这里显示出来是一样的。
3、跳过gtid:
当从库出现主键冲突,或者其他报错的时候,我们根据实际情况来进行手动跳过gtid,步骤如下:
mysql> stop slave;
mysql> set gtid_next=...; //该gtid号可以根据报错提示去找。
mysql> begin;commit;
mysql> set gtid_next='automatic';
mysql> start slave;
这样也就解释了gtid_next可以显示的指定,默认情况下是automatic的。
需要注意的是,在生产上从库出现gtid问题的时候,不要一昧的去跳过,需要找到问题的原因,是因为从库没有开启read only还是什么原因,要搞清楚,否则可能造成主从数据不一致。
4、gtid_owned什么时候显示:
在平时使用过程中该值是不容易被发现,被捕捉的。这里我们模拟一个大事务来看看:
在主库执行,模拟大事务:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update d set id=sleep(20) + 111 where id=1; //这里d是一个测试表,可以自行创建。
Query OK, 1 row affected, 1 warning (20.09 sec)
Rows matched: 1 Changed: 1 Warnings: 1
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
在从库查看:
mysql> show global variables like '%gtid%';
gtid_owned :9c94ddb7-7397-11ec-a69f-fa37ddbb3200:1752#5
它是由gtid和线程组成,我们可以使用show processlist,查看线程5:
mysql> show processlist;
| 5| system user | | test | Connect | 34 | User sleep | update d set id=sleep(20) + 111 where id=1 |
正式主库同步过来的操作。
5、gtid_executed表简介:
仅当gtid_mode设置为ON或ON_PERMISSIVE时,GTID才存储在gtid_executed表中。如果从库禁用了binlog或者log_slave_updates=0的时候,该表中存储所有的gtid。
当启用二进制日志的时候,该表并不保存所有的gtid,而是通过show global variables查出来的gtid_executed保存,刷新二进制日志或者重启时,将会把当前binlog中所有的gtid写入到该表中。因此该表有时候可能并不是完整的gtid,所以通过show查看出来的才是完整的。
好了,就先说这么多,说的比较浅显,gtid还是很奇妙很有意思的,里面其实还有好多东西需要学习,要带着兴趣带着思考去学习去测试,这样会事半功倍。