MySQL复制(四)异步复制之GTID

node-current node-current

本文 8.8万 字,按照目录,酌情阅读。

1. GTID理论篇

本篇部分理论内容来引用自网络。

1.1 背景与概念

背景

MySQL 任何复制方式都是基于 binlog 方式进行的。从 MySQL5.6 开始增加了GTID(Global Transaction ID,全局事务ID)特性,GTID作为position方式的延伸,用来强化数据库的主备一致性,故障恢复,以及容错能力。以取代过去传统的主从复制 (即:基于binlogposition的异步复制)。借助GTID,在发生主备切换的情况下,MySQL 的其他 slave 可以自动在新主上找到正确的复制位置,这大大简化了复杂复制拓扑下集群的维护,也减少了人为设置复制position发生误操作的风险。另外,基于GTID的复制可以忽略已经执行过的事务,减少了数据发生不一致的风险。

MySQL复制中使用的事务类型有以下两种:

  • GTID事务:在二进制日志中每个 GTID 事务始终都以Gtid_log_event开头。可以使用 GTID 或使用文件名和位置来定位 GTID 事务。
  • 匿名事务:MySQL 8 的二进制日志中的每个匿名事务都以Anonymous_gtid_log_event开头,不分配 GTID。匿名事务只能使用文件名和位置来定位。

GTID 出现之前,在一主多从的复制拓扑中,如果主库宕机,需要从多个从库选择之一作为新主库,这个过程比较复杂。没有一种直接了当的方法找到其它从库对应的新主库二进制日志坐标。通常的做法是先要寻找每个从库复制原主库的最后语句,然后找到新主库中包含该语句的二进制日志文件,其中该语句后的第一个事件位置即为连接新主库的二进制坐标。主要难点在于不存在一个唯一标识指出“复制原主库的最后语句”,于是后来的 MySQL 中就出现了 GTID 的概念。

概念

GTID(Global Transaction ID,全局事务ID),实际上是由UUID+TID组成的。其中UUID是一个 MySQL 实例的唯一标识。TID 代表了该实例上已经提交的事务数量,并且随着事务提交单调递增,所以GTID能够保证每个 MySQL 实例事务的执行(不会重复执行同一个事务,并且会补全没有执行的事务)。下面是一个 GTID 的具体形式:

GTID=UUID+TID
GTID=server_uuid:transaction_id
4e659069-3cd8-11e5-9a49-001c4270714e:1-77
  • transaction_id,是一个从1开始的自增计数,表示在这个主库上执行的第n个事务。
  • MySQL 会保证事务与GTID之间的 1:1 映射,如:6ba9a76d-606b-11ea-b3ce-000c29cb3421:1
    表示在以6ba9a76d-606b-11ea-b3ce-000c29cb3421为唯一标识的 MySQL 实例上执行的第1个数据库事务。
  • 一组连续的事务可以用-连接的事务序号范围表示,称作gtid set。例如:6ba9a76d-606b-11ea-b3ce-000c29cb3421:1-15

1.2 GTID复制原理(生命周期)

mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 71cf4b9d-8343-11e8-97f1-a0d3c1f25190 |              1 |    653948549 |
+--------------------------------------+----------------+--------------+
  • 1). 当主库 Master 执行事务时,系统会为事务分配一个由server uuid和此服务器上尚未使用的最小非零事务序列号组成的GTID(当然读事务或者被主动过滤掉的事务不会被分配 GTID,例如因为事务已被过滤掉,或者事务是只读的),写binlog日志时此GTID标志着一个事务的开始。

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

  • 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 上如果开启了binlogGTID也会以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全了。

  • 简单过程如下

    • 1). Master 更新数据时,会在事务前产生 GTID,一同记录到 binlog 日志中;
    • 2). Slave 端的 I/O 线程将变更的 binlog,写入到 Slave 的 relay log 中;
    • 3). Slave 的 SQL 线程从 relay log 中读取 GTID 值设置 gtid_next 变量,然后对比 Slave 端的 binlog 是否有记录 (所以 MySQL5.6 slave 端必须开启 binlog);
    • 4). 如果有记录,说明该 GTID 的事务已经执行,slave 会忽略;
    • 5). 如果没有记录,slave 就会从 relay log 中执行该 GTID 的事务,并记录到 binlog;
    • 6). 在解析过程中会判断是否有主键,如果没有就用二级索引,如果没有就用全部扫描。

1.3 同步补充说明

  • 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事务是无影响可以跳过的。

1.4 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。当二进制日志中的生成语句自动删除表时,会为该语句分配 GTID。例如,当具有打开临时表的用户会话断开连接时,将自动删除临时表,或者使用MEMORY存储引擎的表在服务器启动后第一次访问时会自动删除。未写入二进制日志事务不会分配 GTID。如之前所说的,master 上rollback的事务不会被分配 GTID,或在禁用二进制日志时执行的事务,或通过SET @@SESSION.sql_log_bin = 0;,或空事务 (begin;commit;) 等。主动关闭会话 binlog 当然也不会为事务分配 GTID 了,毕竟连 binlog 都不会产生。

然后解释第二句:

对于XA事务(分布式事务),一个事务会有多个 GTID,而且就算其中一段事务被回滚也会被分配一个 GTID。此外在以下几种情况下一条语句会产生多个事务,因此会被分配多个 GTID:

  • 一个存储过程中包含多个事务。
  • 多表DROP TABLE语句中包含任何不支持原子 DDL 存储引擎的表 (如 myisam ) 或临时表,会生成多个 GTID。
  • CREATE TABLE ... SELECT语句,create table产生一个 GTID,插入数据产生一个 GTID。

1.5 GTID参数

1.5.1 相关参数

参数 描述
Auto_Position 自动获取 position 位置,显示为 1。
binlog_gtid_simple_recovery 布尔类型全局变量,该参数是控制当 MySQL 服务重启或启动时候自动寻找 GTIDs 的值,缺省值为 ON。缺省值时,初始化 gtid_executed 和 gtid_purged 值时只读取最老和最新的 binlog 文件。否则需要遍历所有 binlog 文件。
enforce_gtid_consistency 枚举类型全局变量,指示是否强制 GTID 数据一致性,有效值为 OFF、ON、WARN,缺省值为 OFF。
executed_gtid_set 当前实例执行过的 GTID 集合。
retrieved_gtid_Set Slave 节点已经接收到的 Master 节点的 GTIDs。
gtid_executed 全局和会话级别都可以用,用来保存已经执行过的GTID集合。
gtid_executed_compression_period 启用 GTID 时,服务器会定期在 mysql.gtid_executed 表上执行压缩。通过设置该变量,可以控制压缩表之前允许的事务数,从而控制压缩率。设置为 0 时,则不进行压缩。缺省值 1000。
gtid_mode 枚举类型全局变量,控制是否开启 GTID 功能,有效值为 OFF、OFF_PERMISSIVE、ON_PERMISSIVE、ON,缺省值为 OFF。联机设置时,只能按顺序一步步修改。
gtid_next session 级别的变量,产生下一个 gtid,有 AUTOMATIC、ANONYMOUS、UUID:NUMBER 三种取值方式。
gtid_owned 该参数包含全局和 session,全局表示所有服务器拥有 GTIDs,session 级别表示当前 client 拥有的所有 GTIDs。
gtid_purged 全局变量,设置已经执行但在 binlog 中被清除的 GTID 集合,是 gtid_executed 的子集。
retrieved_gtid_set slave会扫描最后一个 relay log 文件,retrieved_gtid_set 显示的是当前扫描所得的 GTID。
session_track_gtids 该参数是控制用于捕获的 GTIDs 和在 OK PACKE 返回的跟踪器。

1.5.2 gtid_next

gtid_next是会话系统变量。默认情况下,对于在用户会话中提交的新事务,服务器会自动生成并分配新的 GTID。在从库上应用事务时,将保留来自原始服务器的 GTID。可以通过设置 gtid_next 系统变量的会话值来更改此行为:

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

在将 gtid_next 设置为特定 GTID 并且已提交或回滚事务之后,必须在任何其它语句之前发出显式SET @@SESSION.gtid_next语句。如果不想分配更多 GTID,可以将此选项值的值设置回 AUTOMATIC。

# 搭建完GTID环境后实验以下内容
mysql> show variables like 'gtid%';
+----------------------------------+-------------------------------------------------------------------------------------+
| Variable_name                    | Value                                                                               |
+----------------------------------+-------------------------------------------------------------------------------------+
| gtid_executed                    | dd746660-528a-11ed-9c86-000c293b9f86:1-48,e189b1a5-529d-11ed-992e-000c29c1da06:1    |
| gtid_executed_compression_period | 0                                                                                   |
| gtid_mode                        | ON                                                                                  |
| gtid_next                        | AUTOMATIC                                                                           |
| gtid_owned                       |                                                                                     |
| gtid_purged                      |                                                                                     |
+----------------------------------+-------------------------------------------------------------------------------------+
6 rows in set (0.02 sec)
 
mysql> set gtid_next='e189b1a5-529d-11ed-992e-000c29c1da06:2';
Query OK, 0 rows affected (0.00 sec)
 
mysql> show variables like 'gtid%';
+----------------------------------+-------------------------------------------------------------------------------------+
| Variable_name                    | Value                                                                               |
+----------------------------------+-------------------------------------------------------------------------------------+
| gtid_executed                    | dd746660-528a-11ed-9c86-000c293b9f86:1-48,e189b1a5-529d-11ed-992e-000c29c1da06:1    |
| gtid_executed_compression_period | 0                                                                                   |
| gtid_mode                        | ON                                                                                  |
| gtid_next                        | e189b1a5-529d-11ed-992e-000c29c1da06:2                                              |
| gtid_owned                       | e189b1a5-529d-11ed-992e-000c29c1da06:2                                              |
| gtid_purged                      |                                                                                     |
+----------------------------------+-------------------------------------------------------------------------------------+
6 rows in set (0.00 sec)
 
mysql> begin;commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'gtid%';
+----------------------------------+-------------------------------------------------------------------------------------+
| Variable_name                    | Value                                                                               |
+----------------------------------+-------------------------------------------------------------------------------------+
| gtid_executed                    | dd746660-528a-11ed-9c86-000c293b9f86:1-48,e189b1a5-529d-11ed-992e-000c29c1da06:1-2  |
| gtid_executed_compression_period | 0                                                                                   |
| gtid_mode                        | ON                                                                                  |
| gtid_next                        | e189b1a5-529d-11ed-992e-000c29c1da06:2                                              |
| gtid_owned                       |                                                                                     |
| gtid_purged                      |                                                                                     |
+----------------------------------+-------------------------------------------------------------------------------------+
6 rows in set (0.00 sec)

mysql> create table t1(a int);
ERROR 1837 (HY000): When @@SESSION.GTID_NEXT is set to a GTID, you must explicitly set it to a different value after a COMMIT or ROLLBACK. Please check GTID_NEXT variable manual page for detailed explanation. Current @@SESSION.GTID_NEXT is 'e189b1a5-529d-11ed-992e-000c29c1da06:2'.
mysql> set gtid_next=automatic;
Query OK, 0 rows affected (0.00 sec)
 
mysql> create table t1(a int);
Query OK, 0 rows affected (0.01 sec)

前面已经提到,从库的 SQL 线程应用复制事务时使用此技术,将@@SESSION.gtid_next显式设置为在源服务器上分配给事务的 GTID。这意味着保留来自原始服务器的 GTID,而不是由从库生成和分配的新 GTID。即使从库禁用log_binlog_slave_updates,或者事务是空操作或在从库上过滤掉时,GTID 也会添加到从库上的 gtid_executed。客户端可以通过在执行事务之前将@@SESSION.gtid_next设置为特定GTID来模拟复制的事务。mysqlbinlog使用此技术生成二进制日志的转储,客户端可以重放该转储以保留 GTID。通过客户端提交的模拟复制事务完全等同于通过复制应用程序线程提交的复制事务,并且事后无法区分它们。

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

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

[root@mysql80-master mysql]# mysqlbinlog  --base64-output=decode-rows mysql-bin.000001 | tail -15
/*!*/;
# at 2633
#221023 16:03:32 server id 80  end_log_pos 2681 CRC32 0x0f350d86        Table_map: `test`.`t1` mapped to number 95
# at 2681
#221023 16:03:32 server id 80  end_log_pos 2726 CRC32 0x6551a0da        Write_rows: table id 95 flags: STMT_END_F
# at 2726
#221023 16:03:32 server id 80  end_log_pos 2757 CRC32 0xcff96fe5        Xid = 42
COMMIT/*!*/;
# at 2757
#221023 21:28:42 server id 80  end_log_pos 2780 CRC32 0x1aab1a3a        Stop
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

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

1.5.3 gtid_purged和gtid_executed

1.5.3.1 gtid_purged场景

gtid_purged是全局系统变量。@@GLOBAL.gtid_purged中的 GTID 集包含已在服务器上提交但在服务器上的任何二进制日志文件中不存在的所有事务的 GTID。gtid_purgedgtid_executed的子集。以下类别的 GTID 位于gtid_purged中:

  • 在从库上禁用二进制日志记录时提交的复制事务的 GTID。
  • 已清除的二进制日志文件中事务的 GTID。
  • 通过语句SET @@GLOBAL.gtid_purged明确添加到gtid_purged的集合gtid set中的 GTID。
# 以下三个情况阅读即可,实验失败...
=== 第一种情况:
# 从库
[root@mysql80-slave01 ~]# mysqladmin -uroot -proot shutdown
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
[root@mysql80-slave01 ~]# mysqld --defaults-file=/etc/my.cnf --skip-log-bin --user=root &
[root@mysql80-slave01 ~]# mysql -uroot -proot -e "show variables like 'gtid_purged'"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------+-------------------------------------------------------------------------------------+
| Variable_name | Value                                                                               |
+---------------+-------------------------------------------------------------------------------------+
| gtid_purged   | dd746660-528a-11ed-9c86-000c293b9f86:47-49,e189b1a5-529d-11ed-992e-000c29c1da06:6   |
+---------------+-------------------------------------------------------------------------------------+
# 主库执行更新 ...
# 从库
[root@mysql80-slave01 ~]#mysql -uroot -proot -e "show variables like 'gtid_purged'"
=== 第二种情况:
mysql> show binary logs;
+---------------+-----------+-----------+
| Log_name      | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000001 |  13683049 | No        |
+---------------+-----------+-----------+
1 row in set (0.00 sec)
 
mysql> show variables like 'gtid_purged';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_purged   |       |
+---------------+-------+
1 row in set (0.00 sec)
 
mysql> flush logs;
Query OK, 0 rows affected (0.01 sec)
 
mysql> purge master logs to 'binlog.000002';
Query OK, 0 rows affected (0.01 sec)
 
mysql> show variables like 'gtid_purged';
+---------------+----------------------------------------------+
| Variable_name | Value                                        |
+---------------+----------------------------------------------+
| gtid_purged   | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50060 |
+---------------+----------------------------------------------+
1 row in set (0.01 sec)
=== 第三种情况:
mysql> show variables like 'gtid%';
+----------------------------------+--------------------------------------------------+
| Variable_name                    | Value                                            |
+----------------------------------+--------------------------------------------------+
| gtid_executed                    | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060-50061 |
| gtid_executed_compression_period | 1000                                             |
| gtid_mode                        | ON                                               |
| gtid_next                        | AUTOMATIC                                        |
| gtid_owned                       |                                                  |
| gtid_purged                      | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060       |
+----------------------------------+--------------------------------------------------+
6 rows in set (0.01 sec)
 
mysql> set gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060-50061';
ERROR 1229 (HY000): Variable 'gtid_purged' is a GLOBAL variable and should be set with SET GLOBAL
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060-50061';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the added gtid set must not overlap with @@GLOBAL.GTID_EXECUTED
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50061';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50059';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50059:50060';
Query OK, 0 rows affected (0.00 sec)
 
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name                    | Value                                        |
+----------------------------------+----------------------------------------------+
| gtid_executed                    | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50061 |
| gtid_executed_compression_period | 1000                                         |
| gtid_mode                        | ON                                           |
| gtid_next                        | AUTOMATIC                                    |
| gtid_owned                       |                                              |
| gtid_purged                      | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50060 |
+----------------------------------+----------------------------------------------+
6 rows in set (0.01 sec)

可以更改@@GLOBAL.gtid_purged的值,以便在服务器上记录已应用某个 GTID 集中的事务,尽管它们不存在于服务器上的任何二进制日志中,告诉 slave:虽然已经无法在 binlog 中找到相关的 GTID 记录了,但放心这些gtid set内的事务已经被应用过了。此参数一个经典的应用场景是:在搭建主从时使用mysqldumpslave server上恢复了备份,但是因为备份前未开启 GTID 导致恢复后的数据库并没有gtid_executedgtid_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 添加到 gtid_purged 时,它们也会添加到gtid_executed中。看一个相对极端的例子。

1.5.3.2 gtid_purged实验
#1\从库清除二进制日志和gtid_executed信息
reset master;
stop slave;
reset slave all;
show variables like 'gtid%';
mysql> show variables like 'gtid%';
+----------------------------------+-----------+
| Variable_name                    | Value     |
+----------------------------------+-----------+
| gtid_executed                    |           |
| gtid_executed_compression_period | 1000      |
| gtid_mode                        | ON        |
| gtid_next                        | AUTOMATIC |
| gtid_owned                       |           |
| gtid_purged                      |           |
+----------------------------------+-----------+
6 rows in set (0.00 sec)
#2\重置复制
change master to
       master_host = '192.168.2.80',
       master_port = 3306,
       master_user = 'repl',
       master_password = 'repl',
       master_auto_position = 1;
start slave;
mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 192.168.2.80
                  Master_User: rep
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000002
          Read_Master_Log_Pos: 197
               Relay_Log_File: mysql80-slave01-relay-bin.000002
                Relay_Log_Pos: 373
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 1396
                   Last_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'dd746660-528a-11ed-9c86-000c293b9f86:1' at master log mysql-bin.000001, end_log_pos 471. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 157
              Relay_Log_Space: 3669
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 1396
               Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'dd746660-528a-11ed-9c86-000c293b9f86:1' at master log mysql-bin.000001, end_log_pos 471. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 80
                  Master_UUID: dd746660-528a-11ed-9c86-000c293b9f86
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: 
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 221108 22:07:44
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:1-11
            Executed_Gtid_Set: 
                Auto_Position: 1
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
       Master_public_key_path: 
        Get_master_public_key: 0
            Network_Namespace: 
1 row in set, 1 warning (0.00 sec)

可以看到,从主库读到的 GTID 已经到了 11,但没有已经执行的 GTID。实际上这些事务都已经在从库应用了,只是由于 reset master 而没有留下执行的痕迹,所以要从 1 开始执行,而重复执行事务造成了错误。

#3\将所有已读的GTID都标记为已执行,然后重启复制
set global gtid_purged='dd746660-528a-11ed-9c86-000c293b9f86:1-11';
stop slave;
start slave;
show slave status\G
show variables like 'gtid%';
#从show slave status的输出中可以看到复制已恢复正常,最后的输出为:
mysql> show variables like 'gtid%';
+----------------------------------+-------------------------------------------+
| Variable_name                    | Value                                     |
+----------------------------------+-------------------------------------------+
| gtid_executed                    | dd746660-528a-11ed-9c86-000c293b9f86:1-11 |
| gtid_executed_compression_period | 0                                         |
| gtid_mode                        | ON                                        |
| gtid_next                        | AUTOMATIC                                 |
| gtid_owned                       |                                           |
| gtid_purged                      | dd746660-528a-11ed-9c86-000c293b9f86:1-11 |
+----------------------------------+-------------------------------------------+
6 rows in set (0.01 sec)
1.5.3.3 gtid_executed和gtid_purged计算

gtid_executedgtid_purged的值是在数据库服务启动时初始化的,每个binlog的初始event(其实是第2个,第一个是pos=4Format_desc)都是Previous_gtids_log_event(通过SHOW BINLOG EVENTS [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count]查看),这个event包含了之前所有binlog filesGTID set(一般是uuid:1-<最新的事务序列号>),gtid_executed只需要看最新一个binlogPrevious_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_executedgtid_purged的值可能会错误的生成,这姑且一个 BUG,你可能需要将 binlog_gtid_simple_recovery 设为FALSE重新启动 DB 服务器来处理这个 BUG,将此参数设为 FALSE 后,DB server 在启动时会遍历所有binlog文件以便正确计算gtid_executedgtid_purged的值,如果你有很多未开启 GTID 模式时就存在的binlog,可能会导致重启花费很长时间。

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

最旧和最新的二进制日志文件中的Previous_gtids_log_event的内容用于计算服务器启动时的gtid_executedgtid_purged的GTID集:

gtid_executed是最新二进制日志文件中Previous_gtids_log_event中的 GTID、该二进制日志文件中的事务的 GTID、存储在mysql.gtid_executed表中的 GTID,三者的并集。此 GTID 集包含服务器上已使用(或显式添加到gtid_purged)的所有 GTID,无论它们当前是否位于服务器上的二进制日志文件中。它不包括当前正在服务器上正在处理事务的 GTID (@@GLOBAL.gtid_owned)。
gtid_purged的计算方法是首先添加最新二进制日志文件Previous_gtids_log_event中的GTID,再添加该二进制日志文件中事务的 GTID。此步骤提供当前或曾经记录在服务器上的二进制日志中的 GTID 集(gtids_in_binlog)。然后从gtids_in_binlog中减去最旧的二进制日志文件中的Previous_gtids_log_event中的 GTID。此步骤提供当前记录在服务器上的二进制日志中的 GTID 集(gtids_in_binlog_not_purged)。最后,从gtid_executed中减去gtids_in_binlog_not_purged。结果是服务器上已经执行,但当前未记录在服务器上的二进制日志文件中的 GTID 集,此结果用于初始化 gtid_purged。

参考 https://wxy0327.blog.csdn.net/article/details/91047395

1.5.4 GTID自动定位

GTID 是用来代替传统复制的方法,GTID 复制与普通复制模式的最大不同在于,启动和恢复复制时能够自动定位,而不需要指定二进制日志文件名和位置。配置非 GTID 复制时,需要在CHANGE MASTER TO语句中包含MASTER_LOG_FILEMASTER_LOG_POS选项,用于指示从主库复制的开始点。但对于 GTID,从库不需要此非本地数据,其与主库同步的所有信息都直接从复制数据流中获取,因此不需要指定这些选项。要使用基于 GTID 的复制启动从库,推荐启用MASTER_AUTO_POSITION选项。

默认情况下禁用MASTER_AUTO_POSITION选项。如果在从库上启用了多源复制,则需要为每个适用的复制通道设置该选项。设置MASTER_AUTO_POSITION=0会使从库恢复为基于文件的复制,这时必须指定MASTER_LOG_FILE或MASTER_LOG_POS选项。当从库启用 GTID (GTID_MODE = ON、ON_PERMISSIVE或OFF_PERMISSIVE)并使用MASTER_AUTO_POSITION选项时,将激活自动定位以连接到主库。主库必须设置GTID_MODE = ON才能使连接成功。

在初始握手中,从库向主库发送一个 GTID 集,其中包含已经收到、已提交或两者都已完成的事务。此 GTID 集等于@@GLOBAL.gtid_executed系统变量与select received_transaction_set from performance_schema.replication_connection_status查询结果的并集。主库会比较其二进制日志中记录的所有事务和从库发来的 GTID 集合,并将不包括在从库发送的 GTID 集中的事务全部发送给从库。自动跳过功能可确保同一事务不会应用两次。如果从库缺失的GTID 已经被主库清除 (purge),则复制中断,主库将错误ER_MASTER_HAS_PURGED_REQUIRED_GTIDS发送给从库。主库错误日志的ER_FOUND_MISSING_GTIDS警告消息中将列出丢失事务的 GTID。从库无法自动解决此问题,尝试在不启用MASTER_AUTO_POSITION选项的情况下重新连接主库只会导致已清除事务在从库上的丢失。可以考虑修改主库上的binlog_expire_logs_seconds系统参数值(缺省为2592000秒,即30天),以确保不再发生二进制日志还需要时已经被提前清除的情况。下面模拟一下这个场景。

1.5.4.1 模拟从库未开启同步时主库丢失日志
-- 从库停止复制
stop slave;
-- 主库做更新
truncate table t1;
-- 主库修改binlog文件名,模拟事务丢失
mysql -uroot -proot -e "show master status;"
mv binlog.000001 binlog.000001.bak
-- 从库启动复制
start slave;
show slave status\G
#会看到1236错误:
Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Cannot replicate because the master purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new slave from backup. Consider increasing the master's binary log expiration period. To find the missing transactions, see the master's error log or the manual for GTID_SUBTRACT.'
# 主库的错误日志中会显示如下信息:
2019-06-11T00:18:14.500248Z 207 [ERROR] [MY-010958] [Server] Could not open log file.
2019-06-11T00:18:14.500299Z 207 [Warning] [MY-011809] [Server] Cannot replicate to server with server_uuid='565a6b0a-6f05-11e9-b95c-005056a5497f' because the present server has purged required binary logs. The connecting server needs to replicate the missing transactions from elsewhere, or be replaced by a new server created from a more recent backup. To prevent this error in the future, consider increasing the binary log expiration period on the present server. The missing transactions are '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-11007'.
# 主库二进制日志修改成正确的文件名,重启从库复制后恢复正常:
-- 主库
mv binlog.000001.bak binlog.000001
-- 从库
stop slave;
start slave;
show slave status\G
1.5.4.2 主库未配置sync时遇到故障

如果在事务交换期间发现从库已经在 GTID 中接收或提交了事务,但主库本身没有它们的记录,则复制停止,主库将错误ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER发送给从库。当没有配置sync_binlog=1的主库遇到电源故障或操作系统崩溃,导致尚未同步到二进制日志文件的已提交事务已被从库接收,则会发生这种情况。如果主库重新提交事务,可能导致主库和从库对不同的事务使用相同的 GTID,这时只能根据需要对各个事务手动解决冲突(例如手工设置 gtid_next )。如果问题仅在于主库缺少事务,则可以主从切换,允许它跟上复制拓扑中的其它服务器,然后在需要时再次将其设置为主库。可见sync_binlog=1对于主从数据一致至关重要,这也是MySQL 8 的缺省配置值。下面模拟一下这个场景。

-- 主库reset master
reset master;
-- 从库重启复制
stop slave;
start slave;
show slave status\G
# 会看到以下错误:
Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Slave has more GTIDs than the master has, using the master's SERVER_UUID. This may indicate that the end of the binary log was truncated or that the last binary log file was lost, e.g., after a power or disk failure when sync_binlog != 1. The master may or may not have rolled back transactions that were already replica''

# 重新配置从库以恢复复制:
reset master;
stop slave;
reset slave all;
change master to
       master_host = '192.168.2.80',
       master_port = 3306,
       master_user = 'repl',
       master_password = 'repl',
       master_auto_position = 1;
start slave;
show slave status\G

1.6 GTID的格式与存储

此小节可在搭建完GTID复制环境后实验。

1.6.1 单个GTID

GTID 与主库上提交的每个事务相关联。此标识符不仅对发起事务的库是唯一的,而且在给定复制拓扑中的所有库中都是唯一的。GTID 用冒号分隔的一对坐标表示,例如:

8eed0f5b-6f9b-11e9-94a9-005056a57a4e:23

前一部分是主库的server_uuid,后面一部分是主库上按提交事务的顺序确定的序列号,提交的事务序号从 1 开始。上面显式的 GTID 表示:具有 8eed0f5b-6f9b-11e9-94a9-005056a57a4e 的服务器上提交的第23个事务具有此 GTID。MySQL 5.6 以后用 128 位的 server_uuid 代替了原本的 32 位 server_id 的大部分功能。原因很简单,server_id依赖于my.cnf 的手工配置,很可能产生冲突。而自动产生128位UUID的算法可以保证所有的MySQL UUID都不会冲突。数据目录下的auto.cnf文件用来保存server_uuidMySQL启动的时候会读取auto.cnf文件,如果没有读取到则会生成一个server_id,并保存到auto.cnf文件中。

在主库上提交客户端事务时,如果事务已写入二进制日志,则会为其分配新的 GTID,保证为客户事务生成单调递增且没有间隙的 GTID。如果未将客户端事务写入二进制日志(例如,因为事务已被过滤掉,或者事务是只读的),则不会在源服务器上为其分配 GTID。从库上复制的事务保留与主库上事务相同的 GTID。即使从库上未开启二进制日志,GTID 也会被保存。MySQL系统表mysql.gtid_executed用于保存 MySQL 服务器上应用的所有事务的 GTID,但存储在当前活动二进制日志文件中的事务除外。

GTID的自动跳过功能意味着一旦在给定服务器上提交了具有给定 GTID 的事务,则该服务器将忽略使用相同 GTID 执行的任何后续事务(这种情况是可能发生的,如手工设置了gtid_next时)。这有助于保证主从一致性,因为在主库上提交的事务在从库上应用不超过一次。如果具有给定 GTID 的事务已开始在服务器上执行但尚未提交或回滚,则任何在该服务器上启动具有相同 GTID 的并发事务都将被阻止。服务器既不执行并发事务也不将控制权返回给客户端。一旦先前的事务提交或回滚,就可以继续执行同一 GTID 上被阻塞的并发会话。如果是回滚,则一个并发会话继续执行事务,并且在同一 GTID 上阻塞的任何其它并发会话仍然被阻止。如果是提交,则所有并发会话都将被阻止,并自动跳过事务的所有语句。mysqlbinlog 的输出中的GTID_NEXT包含事务的 GTID,用于标识复制中的单个事务。

下面做三个简单实验验证 GTID 的自动跳过功能。

1.6.1.1 实验1:验证自动跳过
# 1、准备初始数据
drop database test;
create database test;
use test;
create table t1(a int);
create table t2(a int);
insert into t1 values(1),(2);
insert into t2 values(1),(2);
commit;
# 2、查看当前GTID
mysql> show master status;
+------------------+----------+--------------+------------------+-----------------------------------------------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                                                 |
+------------------+----------+--------------+------------------+-----------------------------------------------------------------------------------+
| mysql-bin.000002 |     2437 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-20,
e189b1a5-529d-11ed-992e-000c29c1da06:1 |
+------------------+----------+--------------+------------------+-----------------------------------------------------------------------------------+
1 row in set (0.00 sec)
# 3、将GTID设置为已经执行过的值,再执行事务
mysql> set gtid_next = 'e189b1a5-529d-11ed-992e-000c29c1da06:1';
Query OK, 0 rows affected (0.00 sec)
mysql> truncate table test.t1;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|    2 |
+------+
# 4、设置自动
mysql> set gtid_next = automatic;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|    2 |
+------+
mysql> truncate table test.t1;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test.t1;
Empty set (0.00 sec)

可以看到,服务器已经执行了 GTID 为 e189b1a5-529d-11ed-992e-000c29c1da06:1 的事务,后续相同GTID的事务都被自动跳过,虽然truncate 语句没有报错,但并未执行,数据无变化。

1.6.1.2 实验2:验证两个相同GTID事务,事务1提交,事务2被跳过
#1、实验数据
drop database test;
create database test;
use test;
create table t1(a int);
create table t2(a int);
insert into t1 values(1),(2);
insert into t2 values(1),(2);
commit;
# 2、查看当前GTID
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                                                   |
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| mysql-bin.000002 |     5613 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-34,
e189b1a5-529d-11ed-992e-000c29c1da06:1-2 |
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
# 3、准备两个SQL脚本s1.sql、s1.sql,gtid_next是一个没用过的新值
	# s1.sql内容如下:
[root@mysql80-master ~]# vi s1.sql
set gtid_next='e189b1a5-529d-11ed-992e-000c29c1da06:3';
begin;
delete from test.t1 where a=1;
select sleep(10);
commit;
set gtid_next=automatic;
	# s2.sql内容如下:
[root@mysql80-master ~]# vi s2.sql
set gtid_next='e189b1a5-529d-11ed-992e-000c29c1da06:3';
begin;
delete from test.t2 where a=1;
commit;
set gtid_next=automatic;
# 4、在会话1执行s1.sql,并且在其sleep期间,在会话2执行s2.sql
-- 会话1
mysql -uroot -proot test < s1.sql
-- 会话2
mysql -uroot -proot test < s2.sql
# 5、查询数据
mysql> select * from test.t1;
+------+
| a    |
+------+
|    2 |
+------+
1 row in set (0.00 sec)
mysql> select * from test.t2;
+------+
| a    |
+------+
|    1 |
|    2 |
+------+
2 rows in set (0.00 sec)
mysql>

可以看到,事务 1 提交前,事务 2 被阻塞。事务 1 提交后,具有相同 GTID 的事务 2 被跳过。

1.6.1.3 实验3:验证两个相同GTID事务,事务1回滚,事务2提交。
#1、实验数据
drop database test;
create database test;
use test;
create table t1(a int);
create table t2(a int);
insert into t1 values(1),(2);
insert into t2 values(1),(2);
commit;
# 2、查看当前GTID
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                                                   |
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| mysql-bin.000002 |     7182 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-40,
e189b1a5-529d-11ed-992e-000c29c1da06:1-3 |
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
# 3、准备两个SQL脚本s1.sql、s1.sql,gtid_next是一个没用过的新值
	#s1.sql内容如下:
[root@mysql80-master ~]# vi s1.sql
set gtid_next='e189b1a5-529d-11ed-992e-000c29c1da06:5';
begin;
delete from test.t1 where a=2;
select sleep(10);
rollback;
set gtid_next=automatic;
	#s2.sql内容如下:
[root@mysql80-master ~]# vi s2.sql
set gtid_next='e189b1a5-529d-11ed-992e-000c29c1da06:5';
begin;
delete from test.t2 where a=1;
commit;
set gtid_next=automatic;
# 4、在会话1执行s1.sql,并且在其sleep期间,在会话2执行s2.sql
-- 会话1
mysql -uroot -proot test < s1.sql
-- 会话2
mysql -uroot -proot test < s2.sql
# 5、查询数据
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|    2 |
+------+
2 rows in set (0.00 sec)
mysql> select * from test.t2;
+------+
| a    |
+------+
|    2 |
+------+
1 row in set (0.00 sec)

可以看到,事务 1 回滚前,事务 2 被阻塞。事务 1 回滚后,具有相同 GTID 的事务 2 被提交。

1.6.2 GTID集

GTID 集是包括一个或多个单个 GTID 或 GTID 范围的集合。源自同一服务器的一系列 GTID 可以折叠为单个表达式,例如:

8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-321

上面的示例表示源自 server_uuid 为 8eed0f5b-6f9b-11e9-94a9-005056a57a4e 服务器的第 1 到第 321 个事务。源自同一服务器的多个单GTID 或 GTID 范围可以同时包含在由冒号分隔的单个表达式中,例如:

8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-3:11:47-49

GTID 集可以包括单个 GTID 和 GTID 范围的任意组合,甚至它可以包括源自不同服务器的 GTID。例如一个存储在从库 gtid_executed 系统变量中的 GTID 集可能如下:

565a6b0a-6f05-11e9-b95c-005056a5497f:1-20, 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-321

表示该从库已从两个主库应用了事务 (也有可能是在从库执行的写操作)。当从库变量返回 GTID 集时,UUID 按字母顺序排列,并且数值间隔按升序合并。MySQL 服务器中很多地方都用到 GTID 集,例如:gtid_executed 和 gtid_purged 系统变量存储的值是 GTID 集;START SLAVEUNTIL SQL_BEFORE_GTIDSUNTIL SQL_AFTER_GTIDS子句的值是 GTID 集;内置函数GTID_SUBSET()GTID_SUBTRACT()需要 GTID 集作为输入等。

1.6.3 mysql.gtid_executed表

1.6.3.1 mysql.gtid_executed表解析
mysql> desc mysql.gtid_executed;
+----------------+------------+------+-----+---------+-------+
| Field          | Type       | Null | Key | Default | Extra |
+----------------+------------+------+-----+---------+-------+
| source_uuid    | char(36)   | NO   | PRI | NULL    |       |
| interval_start | bigint(20) | NO   | PRI | NULL    |       |
| interval_end   | bigint(20) | NO   |     | NULL    |       |
+----------------+------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

mysql.gtid_executed表记录的是服务器上已经执行事务的 GTID。三个字段分别表示发起事务的服务器 UUID、UUID 集的起始和结束事务 ID。对于单个 GTID,后两个字段的值相同。mysql.gtid_executed表供MySQL服务器内部使用。当从库禁用二进制日志时用该表记录 GTID,或者当二进制日志丢失时,可从该表查询 GTID 状态。RESET MASTER命令将重置mysql.gtid_executed表(清空表数据)。和所有系统表一样,用户不要修改该表。仅当gtid_mode设置为 ON 或 ON_PERMISSIVE 时,GTID 才存储在mysql.gtid_executed表中。存储的 GTID 值取决于是是否开启二进制日志:

  • 对于从库,如果禁用了二进制日志记录(skip-log-bin)或log_slave_updates,则服务器将在该表中存储每个事务的 GTID。
  • 如果启用了二进制日志记录,当刷新二进制日志或重启服务器时,服务器都会将当前二进制日志中所有事务的 GTID 写入mysql.gtid_executed表。这种情况适用于主库或启用了二进制日志记录的从库。

启用二进制日志记录时,mysql.gtid_executed表并不保存所有已执行事务的 GTID 的完整记录,该信息由gtid_executed全局系统变量的值提供。如果服务器意外停止,则当前二进制日志文件中的 GTID 集不会保存在mysql.gtid_executed表中。在 MySQL 实例恢复期间,这些 GTID 将从二进制日志文件添加到表中。

即使服务器处于只读模式,MySQL 服务器也可以写入mysql.gtid_executed表,这样二进制日志文件仍然可以在只读模式下轮转。如果无法访问mysql.gtid_executed表时进行二进制日志文件轮转,则继续使用二进制日志文件存储 GTID,同时在服务器上记录警告信息:

2019-06-03T09:37:07.777423Z 287633 [Warning] [MY-010015] [Repl] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.

前面已经提到,mysql.gtid_executed表的记录可能并不是完整的已执行 GTID,而且有不可访问的可能性 (例如误删除此表),因此建议始终通过查询@@global.gtid_executed(每次提交后更新)来确认 MySQL 服务器的 GTID 状态,而不是查询mysql.gtid_executed表。

mysql.gtid_executed表可能随着事务量的增多而快速膨胀,存储了源自同一服务器的大量不同的单个 GTID,这些 GTID 构成一个范围,例如:

+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |              1 |          329 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |            330 |          330 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |            331 |          331 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |            332 |          332 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |            333 |          333 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |            334 |          334 |
+--------------------------------------+----------------+--------------+

为了节省空间,MySQL 服务器定期压缩mysql.gtid_executed表,方法是将每个这样的行集替换为跨越整个事务标识符间隔的单行,如下所示:

+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
|--------------------------------------+----------------+--------------|
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e |              1 |          334 |
...

通过设置gtid_executed_compression_period系统变量,可以控制压缩表之前允许的事务数,从而控制压缩率。此变量的默认值为 1000,指的是在每 1000 次事务之后执行表的压缩。将gtid_executed_compression_period设置为 0 将不执行压缩。注意,启用二进制日志时不使用gtid_executed_compression_period的值,并在每个二进制日志轮转时压缩mysql.gtid_executed表。mysql.gtid_executed表的压缩由名为thread/sql/compress_gtid_table的专用前台线程执行。此线程未在SHOW PROCESSLIST的输出中列出,但可以从performance_schema.threads中查询到。

mysql> select * from performance_schema.threads where name like '%gtid%'\G
*************************** 1. row ***************************
          THREAD_ID: 44
               NAME: thread/sql/compress_gtid_table
               TYPE: FOREGROUND
     PROCESSLIST_ID: 6
   PROCESSLIST_USER: NULL
   PROCESSLIST_HOST: NULL
     PROCESSLIST_DB: NULL
PROCESSLIST_COMMAND: Daemon
   PROCESSLIST_TIME: 438302
  PROCESSLIST_STATE: Suspending
   PROCESSLIST_INFO: NULL
   PARENT_THREAD_ID: 1
               ROLE: NULL
       INSTRUMENTED: YES
            HISTORY: YES
    CONNECTION_TYPE: NULL
       THREAD_OS_ID: 73199
     RESOURCE_GROUP: SYS_default
1 row in set (0.00 sec)
mysql>

通常该线程都处于暂停状态,只有当满足条件时被唤醒,如达到gtid_executed_compression_period或发生了二进制日志轮转 (如 flush logs 等) 时。

1.6.3.2 reset master实验

下面做个简单实验展示一下reset master的作用和影响。

#1、查看从库当前已经执行的GTID和二进制日志
show master status;
show variables like 'gtid%';
select * from mysql.gtid_executed;
show slave status\G
# 查询结果如下:
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                                                                   |
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
| mysql-bin.000002 |     9226 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-46,e189b1a5-529d-11ed-992e-000c29c1da06:1-5  |
+------------------+----------+--------------+------------------+-------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

mysql> show variables like 'gtid%';
+----------------------------------+-------------------------------------------------------------------------------------+
| Variable_name                    | Value                                                                               |
+----------------------------------+-------------------------------------------------------------------------------------+
| gtid_executed                    | dd746660-528a-11ed-9c86-000c293b9f86:1-46,e189b1a5-529d-11ed-992e-000c29c1da06:1-5  |
| gtid_executed_compression_period | 0                                                                                   |
| gtid_mode                        | ON                                                                                  |
| gtid_next                        | AUTOMATIC                                                                           |
| gtid_owned                       |                                                                                     |
| gtid_purged                      |                                                                                     |
+----------------------------------+-------------------------------------------------------------------------------------+
6 rows in set (0.01 sec)

mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| dd746660-528a-11ed-9c86-000c293b9f86 |              1 |           11 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             12 |           14 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             15 |           15 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             16 |           16 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             17 |           17 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             18 |           20 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             21 |           21 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             22 |           22 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             23 |           24 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             25 |           27 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             28 |           28 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             29 |           31 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             32 |           34 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             35 |           37 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             38 |           40 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             41 |           41 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             42 |           44 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             45 |           46 |
| e189b1a5-529d-11ed-992e-000c29c1da06 |              1 |            1 |
| e189b1a5-529d-11ed-992e-000c29c1da06 |              3 |            3 |
| e189b1a5-529d-11ed-992e-000c29c1da06 |              4 |            4 |
| e189b1a5-529d-11ed-992e-000c29c1da06 |              5 |            5 |
+--------------------------------------+----------------+--------------+
22 rows in set (0.00 sec)

mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 192.168.2.80
                  Master_User: rep
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000002
          Read_Master_Log_Pos: 9024
               Relay_Log_File: mysql80-slave01-relay-bin.000002
                Relay_Log_Pos: 8972
        Relay_Master_Log_File: mysql-bin.000002
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 9024
              Relay_Log_Space: 9192
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 80
                  Master_UUID: dd746660-528a-11ed-9c86-000c293b9f86
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:12-46,
e189b1a5-529d-11ed-992e-000c29c1da06:2-5
            Executed_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:1-46,
e189b1a5-529d-11ed-992e-000c29c1da06:1-5
                Auto_Position: 1
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
       Master_public_key_path: 
        Get_master_public_key: 0
            Network_Namespace: 
1 row in set, 1 warning (0.00 sec)
#所有查询显示的已经执行的GTID均为 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6。
#查看当前的binlog结果如下:
[root@mysql80-slave01 ~]# cd /var/lib/mysql
[root@mysql80-slave01 mysql]# more binlog.index
./binlog.000001
./binlog.000002
[root@mysql80-slave01 mysql]# ls -lt binlog.*
-rw-r-----. 1 mysql mysql  180 Oct 23 15:31 binlog.000002
-rw-r-----. 1 mysql mysql   32 Oct 23 14:53 binlog.index
-rw-r-----. 1 mysql mysql 1456 Oct 23 14:51 binlog.000001
[root@mysql80-slave01 mysql]# 
#当前从库有2个binlog文件。
#2、在从库执行reset master
mysql> reset master;
Query OK, 0 rows affected (0.09 sec)
#3、再次执行(1)的查询,可以看到所有查询的gtid_executed都置空,binlog文件只有binlog.000001一个。
# 说明reset master命令会清空gtid_executed变量和mysql.gtid_executed表,并会只保留一个初始的binlog文件。
#4、在主库上执行一些更新
use test;
create table t3(a int);
insert into t3 select 3;
#5、再次执行(1)的查询,可以看到mysql.gtid_executed表中没有记录,其它查询都已显示出新执行GTID的值,复制正常。
mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| dd746660-528a-11ed-9c86-000c293b9f86 |             47 |           47 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             48 |           48 |
+--------------------------------------+----------------+--------------+
#说明mysql.gtid_executed不记录当前binlog中的GTID。
#6、在从库上执行flush logs后,mysql.gtid_executed表中存储了从reset master到flush logs之间binlog中的GTID。
mysql> flush logs;
Query OK, 0 rows affected (0.10 sec)
mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| dd746660-528a-11ed-9c86-000c293b9f86 |             47 |           48 |
+--------------------------------------+----------------+--------------+
1 row in set (0.00 sec)
#7、在主库上执行以下语句
use test;
delimiter //
create procedure p1(a int)
begin
   declare i int default 1;
   while i<=a do
      insert into t1 values (i);
      set i=i+1;
   end while;
end;
//
delimiter ;
 
call p1(10000);
#8、在上一步执行期间,开一个新会话在主库上执行reset master
[root@mysql80-master ~]# mysql -uroot -proot
mysql> reset master;
#9、查看从库的复制状态,从show slave status的输出中可以看到复制的IO线程已停止,并报以下错误:
Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'I/O error reading log event; the first event '' at 4, the last event read from './binlog.000001' at 201303, the last byte read from './binlog.000001' at 201303.'
#由于主库正在执行事务中间进行了reset master,从库无法读取主库的二进制日志而报错。
#更甚之,这些二进制日志的丢失是永久性的,结果很可能需要从头重建复制。

由此实验得出的结论是,作为一条基本原则,不要随意在主库上执行 reset master,这样做极有可能导致复制停止或造成主从数据不一致等严重后果,而且不易恢复。

1.7 GTID和Position的区别

1.7.1 binlog直观对比

Pos列和End_log_pos

mysql> show binlog events in 'mysql-bin.000001';
+------------------+------+----------------+-----------+-------------+------------------------------------------+
| Log_name         | Pos  | Event_type     | Server_id | End_log_pos | Info                                     |
+------------------+------+----------------+-----------+-------------+------------------------------------------+
| ...																											|
| mysql-bin.000001 | 1390 | Anonymous_Gtid |        57 |        1455 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'     |
| mysql-bin.000001 | 1455 | Query          |        57 |        1540 | BEGIN									|
| mysql-bin.000001 | 1540 | Table_map      |        57 |        1604 | table_id: 109 (test_master_slave.mytbl)	|
| mysql-bin.000001 | 1604 | Write_rows     |        57 |        1652 | table_id: 109 flags: STMT_END_F			|
| mysql-bin.000001 | 1652 | Xid            |        57 |        1683 | COMMIT /* xid=24 */						|
| mysql-bin.000001 | 1683 | Anonymous_Gtid |        57 |        1748 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'		|
| mysql-bin.000001 | 1748 | Query          |        57 |        1833 | BEGIN									|
| mysql-bin.000001 | 1833 | Table_map      |        57 |        1897 | table_id: 109 (test_master_slave.mytbl)	|
| mysql-bin.000001 | 1897 | Write_rows     |        57 |        1945 | table_id: 109 flags: STMT_END_F			|
| mysql-bin.000001 | 1945 | Xid            |        57 |        1976 | COMMIT /* xid=27 */						|
| mysql-bin.000001 | 1976 | Anonymous_Gtid |        57 |        2041 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'		|
| mysql-bin.000001 | 2041 | Query          |        57 |        2126 | BEGIN									|
| mysql-bin.000001 | 2126 | Table_map      |        57 |        2190 | table_id: 109 (test_master_slave.mytbl)	|
| mysql-bin.000001 | 2190 | Write_rows     |        57 |        2237 | table_id: 109 flags: STMT_END_F			|
| mysql-bin.000001 | 2237 | Xid            |        57 |        2268 | COMMIT /* xid=30 */						|
+------------------+------+----------------+-----------+-------------+------------------------------------------+
27 rows in set (0.00 sec)

主从复制,默认是通过pos复制(postion)方式,将用户进行的每一项操作都进行编号(pos),每一个event都有一个起始编号,一个终止编号。GTID就是类似于pos的一个作用,全局通用并且日志文件里事件的GTID值是一致的。posGTID在日志里是一个标识符,在slave里已不同的方式展现。

GTID的生成受gtid_next控制。
Master上,gtid_next是默认的AUTOMATIC,即GTID在每次事务提交时自动生成。它从当前已执行的GTID集合(即gtid_executed)中,找一个大于0的未使用的最小值作为下个事务GTID。同时将GTID写入到binlog(set gtid_next记录),在实际的更新事务记录之前。
Slave上,从binlog先读取到主库的GTID(即set gtid_next记录),而后执行的事务采用该GTID

1.7.2 建立同步语句不同

# 传统复制
change master to 
master_host="192.168.2.80",
master_port=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_LOG_FILE='log-bin.000005', 
MASTER_LOG_POS=4111;

# GTID复制
change master to 
master_host="192.168.2.80",
master_port=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;

比起binlog+position的传统方式,在恢复的时候,不需要指定二进制日志文件和位置号了,将传统复制由人为指定 binlog 的 pos 位点改为了MASTER_AUTO_POSITION=1自动获取 binlog 的 pos 位点。降低了故障切换的难度,提高了效率。

2. 空库配置GTID复制

2.1 服务器环境部署

参考【第02章_MySQL复制环境搭建】部署,本节实验环境部署的是 mysql 8.0.31 一主两从环境

2.2 主库配置

2.2.1 参数设置

[root@mysql80-master ~]# vi /etc/my.cnf
[mysqld]
#[必须]主服务器唯一ID,比如可以取master的IP(192.168.2.80)最后80作为ID
server-id=80
#[必须]启用二进制日志,指名路径。比如:自己本地的路径/log/mysqlbin
log-bin=mysql-bin
#[必须]开启GTID的相关参数
gtid-mode=on
#[必须]强制gtid一致性
enforce-gtid-consistency=1
#[必须]从库binlog才会记录主库同步的操作日志
log-slave-updates=1
== ============
server-id=80
log-bin=mysql-bin
gtid-mode=on
enforce-gtid-consistency=1

2.2.2 授权账号

# 如果使用的是MySQL v8,需要如下的方式建立账户,并授权slave:
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

#此语句必须执行。否则报错,见下面注意部分。
ALTER USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'repl';
flush privileges;

2.2.3 重启服务

systemctl restart mysqld

2.3 从库配置

2.3.1 参数设置

[root@mysql80-slave01 ~]# vi /etc/my.cnf
[mysqld]
server-id=81
log-bin=mysql-bin
gtid-mode=on
enforce-gtid-consistency=1
log-slave-updates=1
read_only=on

[root@mysql80-slave02 ~]# vi /etc/my.cnf
[mysqld]
server-id=82
log-bin=mysql-bin
gtid-mode=on
enforce-gtid-consistency=1
log-slave-updates=1
read_only=on	# 从库只读

关于log-slave-updates=1 (※ 来源于网络收集)

1). 从库只开启log-bin功能,不添加log-slave-updates参数,从库从主库复制的数据不会写入log-bin日志。

2). 从库开启log-slave-updates参数后,从库从主库复制的数据会写入log-bin日志文件里。

3). 直接向从库写入数据时,是会写入log-bin日志的。

4). 一主一从时,不需要这个参数,而“主—从—从”级联复制时,中间的从库需要启动这个参数。

5). 双主时,也不需要这个参数,但如果增加从库,则连接从库的主库同时是个中间库,要加这个参数。

6). MySQL5.7 可以不启用此参数,5.7 版本使用了 gtid_executed 表记录同步复制的信息,避免两次写入 relay-log 和 binlog,降低了从库磁盘 I/O。

2.3.2 重启服务

systemctl restart mysqld

2.4 主从关系配置

当使用 MASTER_AUTO_POSITION 参数的时候,MASTER_LOG_FILE,MASTER_LOG_POS参数不能使用。

=== 节点1 ==========
[root@mysql80-slave01 ~]# mysql -uroot -proot
# 节点一配置同步主库
mysql>
change master to 
master_host='192.168.2.80',
master_user='repl',
master_password='repl',
master_port=3306,
master_auto_position=1;
# 其中master_auto_position=1;#自动 position 号(偏移值)【不用填写binlog & position】
# 开启从库1同步
mysql>
start slave;
# 查看状态
mysql>
show slave status\G
# Slave_IO_Running: YES		-- 负责与主机的io通信
# Slave_SQL_Running: Yes	-- 负责自己的slave mysql进程
# 以上两个状态都是YES表表明主从搭建成功
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 192.168.2.80
                  Master_User: rep
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 2106
               Relay_Log_File: mysql80-slave01-relay-bin.000002
                Relay_Log_Pos: 2322
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
......
=== 节点2 ==========
mysql> 
change master to 
master_host='192.168.2.80',
master_user='repl',
master_password='repl',
master_port=3306,
master_auto_position=1;
mysql>
start slave;
mysql>
show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 192.168.2.80
                  Master_User: rep
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 2106
               Relay_Log_File: mysql-slave02-relay-bin.000002
                Relay_Log_Pos: 2322
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
......

2.5 同步测试

2.5.1 状态show processlist

GTID 的主从完成之后可以通过 show processlist 查看。

#主库上查询
mysql> show processlist\G;
...
*************************** 2. row ***************************
     Id: 9
   User: rep
   Host: 192.168.2.81:49924
     db: NULL
Command: Binlog Dump GTID
   Time: 1425
  State: Source has sent all binlog to replica; waiting for more updates
   Info: NULL
*************************** 3. row ***************************
     Id: 10
   User: rep
   Host: 192.168.2.82:48502
     db: NULL
Command: Binlog Dump GTID
   Time: 1330
  State: Source has sent all binlog to replica; waiting for more updates
   Info: NULL
...

2.5.2 状态show slave status

mysql> show slave status\G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 192.168.2.80
                  Master_User: rep
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 2757
               Relay_Log_File: mysql80-slave01-relay-bin.000002
                Relay_Log_Pos: 2973
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 2757
              Relay_Log_Space: 3193
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 80
                  Master_UUID: dd746660-528a-11ed-9c86-000c293b9f86
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:1-11
            Executed_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:1-11
                Auto_Position: 1
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
       Master_public_key_path: 
        Get_master_public_key: 0
            Network_Namespace: 
1 row in set, 1 warning (0.00 sec)

2.5.3 数据测试

# 主库创建数据库
[root@mysql80-master ~]# mysql -uroot -proot
mysql>
create database test;
use test;
create table t1(id int);
insert into t1 values(1),(2);
# 从1、从2查询
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
+--------------------+
5 rows in set (0.01 sec)
mysql> use test;
mysql> select * from t1;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+
2 rows in set (0.00 sec)

2.5.4 binlog列表

# 物理文件
[root@mysql80-master ~]# cd /var/lib/mysql
[root@mysql80-master mysql]# ll mysql-bin*
-rw-r-----. 1 mysql mysql 2757 Oct 23 16:03 mysql-bin.000001
-rw-r-----. 1 mysql mysql   19 Oct 23 15:31 mysql-bin.index
[root@mysql80-master mysql]# cat mysql-bin.index 
./mysql-bin.000001
# 登陆mysql
[root@mysql80-master mysql]# mysql -uroot -proot
mysql> show binary logs;
+------------------+-----------+-----------+
| Log_name         | File_size | Encrypted |
+------------------+-----------+-----------+
| mysql-bin.000001 |      2757 | No        |
+------------------+-----------+-----------+
1 row in set (0.00 sec)

2.5.5 查看mysql.gtid_executed

#主
mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| dd746660-528a-11ed-9c86-000c293b9f86 |              1 |            1 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              2 |            2 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              3 |            3 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              5 |            5 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              6 |            6 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              7 |            7 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              9 |            9 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             10 |           10 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             11 |           11 |
+--------------------------------------+----------------+--------------+
9 rows in set (0.00 sec)
#从
+--------------------------------------+----------------+--------------+
| source_uuid                          | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| dd746660-528a-11ed-9c86-000c293b9f86 |              1 |            3 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              5 |            7 |
| dd746660-528a-11ed-9c86-000c293b9f86 |              9 |            9 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             10 |           10 |
| dd746660-528a-11ed-9c86-000c293b9f86 |             11 |           11 |
+--------------------------------------+----------------+--------------+
5 rows in set (0.00 sec)

2.5.6 查看当前GTID

可以看出三台服务器的GTID是相同的

#主
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000001 |     2757 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-11 |
+------------------+----------+--------------+------------------+-------------------------------------------+
1 row in set (0.00 sec)
#从1
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000001 |     2829 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-11 |
+------------------+----------+--------------+------------------+-------------------------------------------+
1 row in set (0.00 sec)
#从2
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000001 |     2829 |              |                  | dd746660-528a-11ed-9c86-000c293b9f86:1-11 |
+------------------+----------+--------------+------------------+-------------------------------------------+
1 row in set (0.00 sec)
# 或者参数查看
mysql> show global variables like 'gtid_%';
+----------------------------------+-------------------------------------------+
| Variable_name                    | Value                                     |
+----------------------------------+-------------------------------------------+
| gtid_executed                    | dd746660-528a-11ed-9c86-000c293b9f86:1-11 |
| gtid_executed_compression_period | 0                                         |
| gtid_mode                        | ON                                        |
| gtid_owned                       |                                           |
| gtid_purged                      |                                           |
+----------------------------------+-------------------------------------------+
5 rows in set (0.00 sec)

2.5.7 查看gtid_purged

#查看清理过log
show variables like 'gtid_purged';

3. 联机配置GTID复制XtraBackup方式

3.1 服务器环境部署

参考【第02章_MySQL复制环境搭建】部署,本节实验环境部署的是 mysql 5.7.40 一主一从环境。

3.2 准备联机测试数据

# 主库
[root@MySQL57 ~]# mysql -uroot -proot
mysql>
create database test;
use test;
create table t1(id int);
insert into test.t1 values(1),(2);

3.3 主库配置

参考上一节: 空库配置GTID复制的主库配置。

#检查不支持GTID的操作
mysql>
set global enforce_gtid_consistency=warn;
# 如果是正在运行到生产中,让服务器在正常工作负载下运行一段时间并监控错误日志,最好包含一天负载最高的时间段,有条件建议观察2-3天。
# 如果此步骤导致错误日志中出现任何警告,需要调整应用程序,使其仅使用与GTID兼容的功能,并且不能生成与GTID相关的任何警告。
# 这是一个重要步骤,在进行下一步之前,必须确保错误日志中未生成警告。

#参数设置,按顺序执行,解释参考后文
mysql>
set global enforce_gtid_consistency=true;
set global gtid_mode=off_permissive;
set global gtid_mode=on_permissive;
set global gtid_mode=on;
#参数持久化
[root@MySQL57 ~]# vi /etc/my.cnf
server-id=57
log-bin=mysql-bin
gtid-mode=on
enforce-gtid-consistency=1
log-slave-updates=1
#授权账号
[root@MySQL57 ~]# mysql -uroot -proot
# mysql v5
#在主机MySQL里执行授权主从复制的命令
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

# 如果使用的是MySQL v8,需要如下的方式建立账户,并授权slave:
CREATE USER 'repl'@'localhost' IDENTIFIED WITH mysql_native_password BY 'repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
flush privileges;
#重启服务
systemctl restart mysqld
  • enforce-gtid-consistency启用后,MySQL 服务器通过仅允许执行使 GTID 安全的语句来强制 GTID 一致性。在启用基于 GTID 的复制之前,必须将此选项设置为 true。enforce_gtid_consistency的可配置值为:
    • false:允许事务违反 GTID 一致性。
    • true:不允许事务违反 GTID 一致性。
    • warn:允许事务违反 GTID 一致性,但在这种情况下会生成警告。
  • enforce_gtid_consistency设置为 true 时,只能使用 GTID 安全的语句,例如如下操作不能与此选项一起使用:
    • CREATE TABLE ... SELECT 语句。
    • 在事务内创建 TEMPORARY TABLE或DROP TEMPORARY TABLE 语句
    • 更新事务和非事务表的事务或语句。

enforce_gtid_consistency仅在语句进行二进制日志记录时生效。如果在服务器上禁用了二进制日志记录,或者由于过滤器删除了语句而未将语句写入二进制日志,则不会对未记录的语句检查或强制执行 GTID 一致性。

  • 在包含gtid_mode系统变量的所有 MySQL 版本中,它都可以设置成 on 或 off。MySQL 5.7.6 之后 gtid_mode 提供了两个新的选项分别为 n_permissive 和 off_permissive。

    • gtid_mode = on时,无法复制匿名事务,而当 gtid_mode = off 时,只能复制匿名事务。
    • gtid_mode = off_permissive时,新事务是匿名的,同时允许复制的事务是 GTID 或匿名事务。
    • gtid_mode = on_permissive时,新事务使用 GTID,同时允许复制事务为 GTID 或匿名事务。这意味着可以拥有一个复制拓扑,其中包含使用匿名和 GTID 事务的服务器。例如,具有gtid_mode = on的主库可以有使用gtid_mode = on_permissive从库。
  • gtid_mode在主从库上的兼容性以及能否使用自动定位如下表所示,每个条目的含义如下:

    • Y: 主库和从库的 gtid_mode 兼容。
    • N: 主库和从库的 gtid_mode 不兼容。
    • *: 自动定位可与此组合一起使用。
GTID_mode Master Off Master Off_permissive Master On_permissive Master On
Slave Off Y Y N N
Slave Off_permissive Y Y Y Y*
Slave On_permissive Y Y Y Y*
Slave On N N Y Y*

联机设置 gtid_mode 时,只能基于 OFF、OFF_PERMISSIVE、ON_PERMISSIVE、ON 顺序一次改变一步。例如,如果 gtid_mode 当前设置为 OFF_PERMISSIVE,则可以更改为 OFF 或 ON_PERMISSIVE,但不能直接更改为 ON,否则会报以下错误:

ERROR 1788 (HY000): The value of @@GLOBAL.GTID_MODE can only be changed one step at a time: OFF <-> OFF_PERMISSIVE <-> ON_PERMISSIVE <-> ON. Also note that this value must be stepped up or down simultaneously on all servers. See the Manual for instructions.

这样实现是为了确保服务器能够正确处理从匿名事务更改为 GTID 事务的过程,此过程中可能同时包含这两种模式的事务。在gtid_mode = ongtid_mode = off之间切换时,GTID 状态(也就是gtid_executed的值)是持久化的,因此不管gtid_mode的类型如何更改,都可确保始终保留服务器应用的 GTID 集。无论当前选择的gtid_mode如何,与 GTID 相关的字段都会显示正确的信息。显示 GTID 集的字段(例如replication_connection_status性能架构表中的gtid_executedgtid_purgedRECEIVED_TRANSACTION_SET以及SHOW SLAVE STATUS的 GTID 相关结果)在没有 GTID 时返回空字符串。显示单个 GTID 的字段 (如 Performance Schema replication_applier_status_by_worker表中的CURRENT_TRANSACTION)在未使用 GTID 事务时显示ANONYMOUS。从库使用 gtid_mode = on 复制提供了自动定位的功能。

  • 当前选择的 gtid_mode 也会影响 gtid_next 变量。下表显示了服务器对 gtid_mode 和 gtid_next 不同值的行为。每个条目的含义如下:
    • ANONYMOUS:生成匿名事务。
    • Error:生成错误并且无法执行 SET GTID_NEXT。
    • UUID:NUMBER:使用指定的 UUID:NUMBER 生成 GTID。
    • New GTID:使用自动生成的数字生成 GTID。
GTID_next Automatic binary log on GTID_next Automatic binary log off GTID_next Anonymous GTID_next UUID: Number
GTID_mode Off Anonymous Anonymous Anonymous Error
GTID_mode Off_permissive Anonymous Anonymous Anonymous UUID: Number
GTID_mode On_permissive New GTID Anonymous Anonymous UUID: Number
GTID_mode On New GTID Anonymous Error UUID: Number

当二进制日志关闭且gtid_next设置为AUTOMATIC时,不会生成 GTID,这与先前版本的行为一致。注意,为了保证主从数据一致性和实例恢复的性能,在 MySQL 8 中作为一项基本原则,除非有特殊需求,与复制相关的其它系统变量最好保持缺省值,包括但不限于下面所列出的系统变量:

autocommit = ON
log_bin = ON
log_slave_updates = ON
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
master_info_repository = TABLE
relay_log_info_repository = TABLE
relay_log_recovery = OFF
binlog_gtid_simple_recovery = ON
innodb_replication_delay = 0

3.4 从库配置

参考上一节: 空库配置 GTID 复制的从库配置。

# 参数配置
[root@Mysql57-Slave ~]# vi /etc/my.cnf
server-id=58
log-bin=mysql-bin
gtid-mode=on
enforce-gtid-consistency=1
log-slave-updates=1
read_only=on
# 重启服务
systemctl restart mysqld
# 或者开启服务
mysqld_safe --defaults-file=/etc/my.cnf &

3.5 使用XtraBackup备份传输数据

使用XtraBackup的一个好处是,不必考虑从gtid_mode=offgtid_mode=on的过程中是否存在正在进行的匿名事务。XtraBackup 执行的是物理备份,主库上无论是匿名事务还是 GTID 事务,最终数据文件和二进制日志文件都会被拷贝到从库,并且在恢复备份、启动实例和复制后,可以使用 GTID 的自动定位功能找到初始复制点。

3.5.1 XtraBackup安装

根据Mysql版本参考XtraBackup官网文档:

# 这里使用yum在线安装
# 主从都要装
# 下载yum包
yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm
percona-release enable-only tools release
yum -y install qpress

# MySQL 8.0 
yum -y install percona-xtrabackup-80

# MySQL 5.7
yum -y install percona-xtrabackup-24

# 检查安装结果
rpm -qa |grep xtraback

# 卸载xtrabackup
yum -y remove percona-xtrabackup-80
yum -y remove percona-xtrabackup-24

3.5.2 主从的SSH连接

# 主
[root@MySQL57 ~]#
ssh-keygen
# ... 一路回车 ... 输入从库密码等
ssh-copy-id 192.168.2.58

3.5.3 清除从库数据

# 停库从库
[root@Mysql57-Slave ~]# mysqladmin -u root -proot shutdown
# 备份并清空数据目录
[root@Mysql57-Slave ~]# cd /var/lib/
[root@Mysql57-Slave lib]# mkdir -p mysql_backup
[root@Mysql57-Slave lib]# mv mysql/* mysql_backup/

3.5.4 数据传输

# 主库执行
# 这条命令连接主库,进行并行压缩流式备份,同时将备份通过管道操作符传输到从库。
xtrabackup -uroot -proot --socket=/var/lib/mysql/mysql.sock --no-lock --backup --compress --stream=xbstream --parallel=4 --target-dir=./ | ssh root@192.168.2.58 "xbstream -x -C /var/lib/mysql/ "

# 如果是Mysql 8.0,遇到登录报错解决
use mysql;
ALTER USER root@localhost IDENTIFIED WITH mysql_native_password BY 'root';
#或者
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';

# 从库查看传输过来的数据
[root@Mysql57-Slave ~]# cd  /var/lib/mysql
[root@Mysql57-Slave mysql]# ll
total 308
drwxr-x--- 2 root root   4096 Nov  5 17:28 atguigudb
-rw-r----- 1 root root    477 Nov  5 17:28 backup-my.cnf.qp
-rw-r----- 1 root root    371 Nov  5 17:28 ib_buffer_pool.qp
-rw-r----- 1 root root 257194 Nov  5 17:27 ibdata1.qp
drwxr-x--- 2 root root   4096 Nov  5 17:28 mysql
drwxr-x--- 2 root root   8192 Nov  5 17:28 performance_schema
drwxr-x--- 2 root root   8192 Nov  5 17:28 sys
drwxr-x--- 2 root root     57 Nov  5 17:28 test
-rw-r----- 1 root root    152 Nov  5 17:28 xtrabackup_binlog_info.qp
-rw-r----- 1 root root    135 Nov  5 17:28 xtrabackup_checkpoints
-rw-r----- 1 root root    600 Nov  5 17:28 xtrabackup_info.qp
-rw-r----- 1 root root    523 Nov  5 17:28 xtrabackup_logfile.qp

3.6 从库恢复并同步数据

3.6.1 从库解压数据恢复

# 解压数据
[root@Mysql57-Slave mysql]#
xtrabackup --decompress --parallel=4 --target-dir=/var/lib/mysql/
# 应用日志
xtrabackup --prepare --target-dir=/var/lib/mysql/

3.6.2 启动从库服务

# 复制过来的文件授权
[root@Mysql57-Slave mysql]# chmod -R 777 /var/lib/mysql
# 开启mysql服务
[root@Mysql57-Slave mysql]# systemctl start mysqld

3.6.3 启动从库复制

[root@Mysql57-Slave mysql]# mysql -uroot -proot
mysql>
change master to
master_host='192.168.2.57',
master_port=3306,
master_user='repl',
master_password='repl',
master_auto_position = 1;
-- 启动复制
start slave;
show slave status\G
# 可以看到Retrieved_Gtid_Set和Executed_Gtid_Set不断增加,Seconds_Behind_Master逐渐缩小至0。
# 查看数据
mysql> select * from test.t1;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+
2 rows in set (0.00 sec)

3.7 遇到过的错误Last_Errno: 1396

# 从库遇到如下错误的解决
mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.2.57
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 4967
               Relay_Log_File: Mysql57-Slave-relay-bin.000002
                Relay_Log_Pos: 367
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 1396
                   Last_Error: Error 'Operation CREATE USER failed for 'repl'@'%'' on query. Default database: ''. Query: 'CREATE USER 'repl'@'%' IDENTIFIED WITH 'mysql_native_password' AS '*A424E797037BF97C19A2E88CF7891C5C2038C039''

# 从库执行,解决方法如下
mysql>  show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |      154 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
# 1、查询报错编号1396的详细报错
# LAST_SEEN_TRANSACTION值为:fc9cc74b-1bbf-11ed-8416-000c29428a3f
mysql> select * from performance_schema.replication_applier_status_by_worker where LAST_ERROR_NUMBER=1396;
+--------------+-----------+-----------+---------------+----------------------------------------+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+
| CHANNEL_NAME | WORKER_ID | THREAD_ID | SERVICE_STATE | LAST_SEEN_TRANSACTION                  | LAST_ERROR_NUMBER | LAST_ERROR_MESSAGE                                                                                                                                                                                         | LAST_ERROR_TIMESTAMP |
+--------------+-----------+-----------+---------------+----------------------------------------+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+
|              |         0 |      NULL | OFF           | fc9cc74b-1bbf-11ed-8416-000c29428a3f:1 |              1396 | Error 'Operation CREATE USER failed for 'repl'@'%'' on query. Default database: ''. Query: 'CREATE USER 'repl'@'%' IDENTIFIED WITH 'mysql_native_password' AS '*A424E797037BF97C19A2E88CF7891C5C2038C039'' | 2022-11-05 18:46:50  |
+--------------+-----------+-----------+---------------+----------------------------------------+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+
1 row in set (0.00 sec)
#2、停止复制,设置gtid_next
stop slave;
set @@session.gtid_next='fc9cc74b-1bbf-11ed-8416-000c29428a3f:1';
begin;
commit;
#3、设置自动
set @@session.gtid_next=automatic;
#4、启动再次查看
start slave;
show slave status\G;
# 如果还有其他报错:
	#1、检查主库创建复制账号的语句是否正确(mysql5.7和8.0有区别)
	#2、从库重新传输文件,创建同步复制语句

3.8 小结

XtraBackup 是物理复制,性能比 mysqldump 高的多,而且对主库的影响极小,非常适用于从头联机创建高负载、大数据量、全实例从库的场景。

4. GTID部署多主复制

5. 主从切换

参考【第02章_MySQL复制环境搭建】部署,本节实验环境部署的是 mysql 8.0.31一主 一从环境。

这里分三种情况进行讨论:

  • 从库只读
  • 从库读写并且有全部写操作的二进制日志
  • 从库读写但写操作的二进制日志不全。

5.1 从库只读

这种情况从库 (新主库) 没有做过本地的事务,只需执行正常切换。

-- 原从库(新主库)192.168.2.81
stop slave;
reset slave all;
-- 原主库(新从库)192.168.2.80
change master to
       master_host = '192.168.2.81',
       master_port = 3306,
       master_user = 'repl',
       master_password = 'repl',
       master_auto_position = 1;
start slave;
mysql> show slave status\G;
# 数据测试(略)

新主库会生成自己的 GTID 事务,此时会出有两个server_uuid对应的 GTID:

mysql> select @@global.gtid_executed;
+-----------------------------------------------------------------------------------+
| @@global.gtid_executed                                                            |
+-----------------------------------------------------------------------------------+
| dd746660-528a-11ed-9c86-000c293b9f86:1-11,
e189b1a5-529d-11ed-992e-000c29c1da06:1 |
+-----------------------------------------------------------------------------------+

5.2 从库读写并且有全部写操作的二进制日志

# 继上一小节
-- 原从库(新主库)192.168.2.80
-- 模拟从库读写数据
drop table test.t1;
create table test.t1(a int);
insert into test.t1 select 100;
select * from test.t1;

stop slave;
reset slave all;

-- 原主库(新从库)192.168.2.81
change master to
       master_host = '192.168.2.80',
       master_port = 3306,
       master_user = 'repl',
       master_password = 'repl',
       master_auto_position = 1;
start slave;
mysql> show slave status\G;
# 测试
mysql> select * from test.t1;
+------+
| a    |
+------+
|  100 |
+------+
1 row in set (0.00 sec)

此时在show slave status\G的输出中可以看到:

Retrieved_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:12-14
Executed_Gtid_Set: dd746660-528a-11ed-9c86-000c293b9f86:1-14,e189b1a5-529d-11ed-992e-000c29c1da06:1

刚才从库执行的三个本地事务,在新从库上正常复制。因为本地事务与复制事务 GTID 的server_uuid部分不同,只要 binlog 保留完整,从库上的写操作在主从切换后可以自动复制到新的从库上,与匿名复制相比明显方便许多。

5.3 从库读写但写操作的二进制日志不全

# 继上一小节
-- 从库(新主库)192.168.2.81
drop table test.t1;
create table test.t1(a int);
insert into test.t1 select 200;

stop slave;
reset slave all;
flush logs;

# 模拟binlog文件丢失
[root@mysql80-slave01 ~]# cd /var/lib/mysql
[root@mysql80-slave01 mysql]# mv mysql-bin.000002 mysql-bin.000002.bak

-- 主库(新从库)192.168.2.80
change master to
       master_host = '192.168.2.81',
       master_port = 3306,
       master_user = 'repl',
       master_password = 'repl',
       master_auto_position = 1;
start slave;

此时在show slave status\G的输出中报错如下:

Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Could not open log file'

真实环境要是遇到这种情况还是重建从库吧。二进制日志文件缺省的保留时间是30天(binlog_expire_logs_seconds = 2592000)。一般来说从库的写操作通常是为保留一些报表结果或临时数据,这些操作的最早时间很大可能已超过三十天,在这之后进行主从切换就会出现问题。这也是建议从库 read only 的原因之一。如果确实要做比如加索引等不影响数据的操作可以在执行前设置 sql_log_bin 变量:

set sql_log_bin=0;
create index idx1 on test.t1(a);

这样不会增加本地GTID。但还是要强调,从库最好始终read only。

6. GTID运维

每个 GTID 唯一标识构成事务的一组二进制日志事件,在二进制日志中跟踪 GTID 事务与其事件集之间的映射。应用连接到数据库时,MySQL 服务器自动跳过之前已处理的 GTID 事务,此行为对于自动复制定位和正确的故障转移至关重要。启用 GTID 也给运维带来了一些改变。

6.1 跳过一个事务

传统基于二进制坐标 (Position) 的复制中,从库由于某些错误导致复制中断时,一个可能的解决方案是设置sql_slave_skip_counter全局系统变量,跳过导致错误的事件,然后重启复制。但启用 GTID 后,执行的单位由事件变为事务,因此该方法不再有效(slave_skip_errors仍然可用),并会报以下错误。

mysql> set global sql_slave_skip_counter=1;
ERROR 1858 (HY000): sql_slave_skip_counter can not be set when the server is running with @@GLOBAL.GTID_MODE = ON. Instead, for each transaction that you want to skip, generate an empty transaction with the same GTID as the transaction
mysql>

从错误消息可以看到,GTID 跳过事务的方法是注入一个空事务,具体步骤为:

#1、定位出错事务的GTID。
	# 库报错我们需要获得从库执行的最后一个事务,方法有:
show slave status\G;					# 中的 Executed_Gtid_Set
show global variables like '%gtid%';	# 中的 gtid_executed
show master status;						# 中的 Executed_Gtid_Set
#2、将会话级系统变量gtid_next设置为上一步的GTID,如:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:980058'
	#  注意gtid_next的值只能是单个GTID。
#3、注入空事务
begin;
commit;
#4、重启复制
set gtid_next='automatic';
start slave;
	# 重启复制前需要将gtid_next设置为缺省值'automatic'。
#5、
show slave status\G;
# 下面是个跳过多个事务的例子
stop slave;
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:980055';
begin;commit;
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:980056';
begin;commit;
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:980057';
begin;commit;
set gtid_next='automatic';
start slave;

6.2 mysqldump导出

使用mysqldump受set-gtid-purged选项影响,set-gtid-purged选项设置为AUTO(默认值)或 ON 时的输出如下所示。

[mysql@hdp3~]$ mysqldump --single-transaction --all-databases --master-data=2 --host=192.168.2.57 --user=root --password=root
 
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
 SET NAMES utf8mb4 ;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!50606 SET @OLD_INNODB_STATS_AUTO_RECALC=@@INNODB_STATS_AUTO_RECALC */;
/*!50606 SET GLOBAL INNODB_STATS_AUTO_RECALC=OFF */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
SET @MYSQLDUMP_TEMP_LOG_BIN = @@SESSION.SQL_LOG_BIN;
SET @@SESSION.SQL_LOG_BIN= 0; 
--
-- GTID state at the beginning of the backup
--
SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-980059';
--
-- Position to start replication or point-in-time recovery from
--
-- CHANGE MASTER TO MASTER_LOG_FILE='binlog.000022', MASTER_LOG_POS=209837996;
SET @@SESSION.SQL_LOG_BIN = @MYSQLDUMP_TEMP_LOG_BIN;
...
-- Dump completed on 2019-06-13 10:36:35
[mysql@hdp3~]$

开始部分的 SET @@SESSION.SQL_LOG_BIN= 0 防止导入数据时基于本地服务器生成新的 GTID。接着GTID_PURGED被设置为备份时刻已经执行过的 GTID 事务,该操作将会初始化mysql.gtid_executed表、gtid_purge变量及gtid_executed变量。当 mysqldump 命令加入--set-gtid-purged=off选项时,则输出中不会加入SQL_LOG_BIN= 0GTID_PURGED的设置。如果要将数据导入作为从库初始化,不能设置--set-gtid-purged=off。下面是这个选项的含义。

--set-gtid-purged=value

Value Meaning
OFF Add no SET statement to the output.
ON Add a SET statement to the output. An error occurs if GTIDs are not enabled on the server.
AUTO Add a SET statement to the output if GTIDs are enabled on the server.

细心的用户到这里可能心生疑问:为初始化从库数据,命令行使用了--all-databases选项。mysql.gtid_executed表会不会被重建,进而通过GTID_PURGED设置的mysql.gtid_executed表会重新改变,重启数据库后读取mysql.gtid_executed表可能获得错误 GTID 集合导致复制错误?答案也在 mysqldump 的输出中。

...
-- CHANGE MASTER TO MASTER_LOG_FILE='binlog.000022', MASTER_LOG_POS=209837996;
--
-- Current Database: `mysql`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `mysql` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
 
USE `mysql`;
...

首先,如果从库实例的 mysql 库存在则不会删除重建。

...
--
-- Table structure for table `gtid_executed`
-- 
/*!40101 SET @saved_cs_client     = @@character_set_client */;
 SET character_set_client = utf8mb4 ;
CREATE TABLE IF NOT EXISTS `gtid_executed` (
  `source_uuid` char(36) NOT NULL COMMENT 'uuid of the source where the transaction was originally executed.',
  `interval_start` bigint(20) NOT NULL COMMENT 'First number of interval.',
  `interval_end` bigint(20) NOT NULL COMMENT 'Last number of interval.',
  PRIMARY KEY (`source_uuid`,`interval_start`)
) /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
 
--
-- Table structure for table `help_category`
--
...

其次,如果mysql.gtid_executed表存在则不会删除重建。最后,如果该表不存在则创建它,但不会向其装载数据。由此得出结论,除非手工删除了mysql.gtid_executed表,否则不会因它造成复制问题,至少 MySQL 8 是这样。

7. GTID集合运算函数

7.1 GTID内置函数

MySQL 8包含GTID_SUBSET、GTID_SUBTRACT、WAIT_FOR_EXECUTED_GTID_SET、WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS 4个内置函数,用于GTID集合的基本运算。

7.1.1 GTID_SUBSET(set1,set2)

给定两个GTID集set1和set2,set1是set2的子集返回true,否则返回false。

mysql> select gtid_subset('','') c1,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1','53442434-8bfa-11e9-bc15-005056a50f77:1') c2,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1','53442434-8bfa-11e9-bc15-005056a50f77:1-10') c3,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1','53442434-8bfa-11e9-bc15-005056a50f77:2-10') c4,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1-2','53442434-8bfa-11e9-bc15-005056a50f77:1') c5,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1-2','53442434-8bfa-11e9-bc15-005056a50f77:1-10') c6,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1-2','53442434-8bfa-11e9-bc15-005056a50f77:2-10') c7,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1-2','8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-2') c8,
    ->        gtid_subset('53442434-8bfa-11e9-bc15-005056a50f77:1-2','8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-2,53442434-8bfa-11e9-bc15-005056a50f77:1-2') c9;
+----+----+----+----+----+----+----+----+----+
| c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 |
+----+----+----+----+----+----+----+----+----+
|  1 |  1 |  1 |  0 |  0 |  1 |  0 |  0 |  1 |
+----+----+----+----+----+----+----+----+----+
1 row in set (0.00 sec)

7.1.2 GTID_SUBTRACT(set1,set2)

给定两个 GTID 集 set1 和 set2,仅返回 set1 与 set2 的差集。

mysql> select gtid_subtract('','') c1,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1','53442434-8bfa-11e9-bc15-005056a50f77:1') c2,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1','53442434-8bfa-11e9-bc15-005056a50f77:1-10') c3,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1','53442434-8bfa-11e9-bc15-005056a50f77:2-10') c4,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1-2','53442434-8bfa-11e9-bc15-005056a50f77:1') c5,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1-2','53442434-8bfa-11e9-bc15-005056a50f77:1-10') c6,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1-2','53442434-8bfa-11e9-bc15-005056a50f77:2-10') c7,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1-5','53442434-8bfa-11e9-bc15-005056a50f77:3-10') c8,
    ->        gtid_subtract('53442434-8bfa-11e9-bc15-005056a50f77:1-2,8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-2','8eed0f5b-6f9b-11e9-94a9-005056a57a4e:2-3') c9\G
*************************** 1. row ***************************
c1: 
c2: 
c3: 
c4: 53442434-8bfa-11e9-bc15-005056a50f77:1
c5: 53442434-8bfa-11e9-bc15-005056a50f77:2
c6: 
c7: 53442434-8bfa-11e9-bc15-005056a50f77:1
c8: 53442434-8bfa-11e9-bc15-005056a50f77:1-2
c9: 53442434-8bfa-11e9-bc15-005056a50f77:1-2,
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1
1 row in set (0.00 sec)

7.1.3 WAIT_FOR_EXECUTED_GTID_SET(gtid_set[, timeout])

等到服务器应用了包含在 gtid_set 中的所有事务。如果指定可选的 timeout 值 (秒数),超时会使函数停止等待而退出。

mysql> select wait_for_executed_gtid_set('53442434-8bfa-11e9-bc15-005056a50f77:1');
+----------------------------------------------------------------------+
| wait_for_executed_gtid_set('53442434-8bfa-11e9-bc15-005056a50f77:1') |
+----------------------------------------------------------------------+
|                                                                    0 |
+----------------------------------------------------------------------+
1 row in set (0.00 sec)
 
mysql> select wait_for_executed_gtid_set('53442434-8bfa-11e9-bc15-005056a50f77:1-7');
+------------------------------------------------------------------------+
| wait_for_executed_gtid_set('53442434-8bfa-11e9-bc15-005056a50f77:1-7') |
+------------------------------------------------------------------------+
|                                                                      0 |
+------------------------------------------------------------------------+
1 row in set (0.00 sec)
 
mysql> select wait_for_executed_gtid_set('53442434-8bfa-11e9-bc15-005056a50f77:8',5);
+------------------------------------------------------------------------+
| wait_for_executed_gtid_set('53442434-8bfa-11e9-bc15-005056a50f77:8',5) |
+------------------------------------------------------------------------+
|                                                                      1 |
+------------------------------------------------------------------------+
1 row in set (5.00 sec)

7.1.4 WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(gtid_set[, timeout][,channel])

WAIT_FOR_EXECUTED_GTID_SET类似,但针对单个启动的复制通道。

mysql> select wait_until_sql_thread_after_gtids('53442434-8bfa-11e9-bc15-005056a50f77:1');
+-----------------------------------------------------------------------------+
| wait_until_sql_thread_after_gtids('53442434-8bfa-11e9-bc15-005056a50f77:1') |
+-----------------------------------------------------------------------------+
|                                                                           0 |
+-----------------------------------------------------------------------------+
1 row in set (0.00 sec)
 
mysql> select wait_until_sql_thread_after_gtids('53442434-8bfa-11e9-bc15-005056a50f77:1-7');
+-------------------------------------------------------------------------------+
| wait_until_sql_thread_after_gtids('53442434-8bfa-11e9-bc15-005056a50f77:1-7') |
+-------------------------------------------------------------------------------+
|                                                                             0 |
+-------------------------------------------------------------------------------+
1 row in set (0.00 sec)
 
mysql> select wait_until_sql_thread_after_gtids('53442434-8bfa-11e9-bc15-005056a50f77:8',5);
+-------------------------------------------------------------------------------+
| wait_until_sql_thread_after_gtids('53442434-8bfa-11e9-bc15-005056a50f77:8',5) |
+-------------------------------------------------------------------------------+
|                                                                            -1 |
+-------------------------------------------------------------------------------+
1 row in set (5.00 sec)

7.2 用户自定义函数

用户可以在自定义函数中调用这些内置函数,实现一些常用的 GTID 集合运算,下面是 MySQL 8 文档中的几个例子。

# 如果两个GTID集相同,函数返回非零值
create function gtid_is_equal(gtid_set_1 longtext, gtid_set_2 longtext)
returns int deterministic
  return gtid_subset(gtid_set_1, gtid_set_2) and gtid_subset(gtid_set_2, gtid_set_1);
# 如果两个GTID集不相交,函数返回非零值
create function gtid_is_disjoint(gtid_set_1 longtext, gtid_set_2 longtext)
returns int deterministic
  return gtid_subset(gtid_set_1, gtid_subtract(gtid_set_1, gtid_set_2));
# 如果两个GTID集不相交,则函数返回非零,sum是两个集的并集
create function gtid_is_disjoint_union(gtid_set_1 longtext, gtid_set_2 longtext, sum longtext)
returns int deterministic
  return gtid_is_equal(gtid_subtract(sum, gtid_set_1), gtid_set_2) and
         gtid_is_equal(gtid_subtract(sum, gtid_set_2), gtid_set_1);
# 函数返回格式化的GTID集。没有空格且没有重复,UUID按字母顺序排列,间隔按数字顺序排列
create function gtid_normalize(g longtext)
returns longtext deterministic
return gtid_subtract(g, '');
# 函数返回两个GTID集的并集
create function gtid_union(gtid_set_1 longtext, gtid_set_2 longtext)
returns longtext deterministic
  return gtid_normalize(concat(gtid_set_1, ',', gtid_set_2));
# 函数返回两个GTID集的交集
create function gtid_intersection(gtid_set_1 longtext, gtid_set_2 longtext)
returns longtext deterministic
  return gtid_subtract(gtid_set_1, gtid_subtract(gtid_set_1, gtid_set_2));
# 函数返回两个GTID集的对称差集
create function gtid_symmetric_difference(gtid_set_1 longtext, gtid_set_2 longtext)
returns longtext deterministic
  return gtid_subtract(concat(gtid_set_1, ',', gtid_set_2), gtid_intersection(gtid_set_1, gtid_set_2));
# 函数返回除去指定UUID的GTID集
create function gtid_subtract_uuid(gtid_set longtext, uuid text)
returns longtext deterministic
  return gtid_subtract(gtid_set, concat(uuid, ':1-', (1 << 63) - 2));
# 函数返回指定UUID的GTID集
create function gtid_intersection_with_uuid(gtid_set longtext, uuid text)
returns longtext deterministic
  return gtid_subtract(gtid_set, gtid_subtract_uuid(gtid_set, uuid));

7.3 使用示例

7.3.1 验证从库的复制是否最新

内置函数GTID_SUBSETGTID_SUBTRACT可用于检查从库是应用了主库的每个事务。使用GTID_SUBSET执行此检查,在从库上执行以下命令:

[root@mysql80-slave01 ~]# 
master_gtid_executed=`mysql -uroot -proot -h192.168.2.80 -N -e "select replace(@@global.gtid_executed,char(10),'')"` 
slave_gtid_executed=`mysql -uroot -proot -N -e "select replace(@@global.gtid_executed,char(10),'')"` 
sql="select gtid_subset('$master_gtid_executed', '$slave_gtid_executed')"
mysql -uroot -proot -e "$sql"
-- 返回结果 --
mysql: [Warning] Using a password on the command line interface can be insecure.
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| gtid_subset('dd746660-528a-11ed-9c86-000c293b9f86:1-14,e189b1a5-529d-11ed-992e-000c29c1da06:1', 'dd746660-528a-11ed-9c86-000c293b9f86:1-14,e189b1a5-529d-11ed-992e-000c29c1da06:1') |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|                                                                                                                                                                                   1 |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

如果返回 0 则master_gtid_executed中的某些 GTID 不存在于slave_gtid_executed中,因此从库不是最新的。要使用GTID_SUBTRACT执行检查,请在从库上执行以下命令:

[root@mysql80-slave01 ~]# 
master_gtid_executed=`mysql -uroot -proot -h192.168.2.80 -N -e "select replace(@@global.gtid_executed,char(10),'')"` 
slave_gtid_executed=`mysql -uroot -proot -N -e "select replace(@@global.gtid_executed,char(10),'')"` 
sql="select gtid_subtract('$master_gtid_executed', '$slave_gtid_executed')"
mysql -uroot -proot -e "$sql"
-- 返回结果 --
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| gtid_subtract('dd746660-528a-11ed-9c86-000c293b9f86:1-14,e189b1a5-529d-11ed-992e-000c29c1da06:1', 'dd746660-528a-11ed-9c86-000c293b9f86:1-14,e189b1a5-529d-11ed-992e-000c29c1da06:1') |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|                                                                                                                                                                                       |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

返回master_gtid_executed中但未在slave_gtid_executed中的 GTID。如果返回值不为空,则从库不是最新的。

7.3.2 验证mysqldump导出导入

自定义函数GTID_IS_EQUALGTID_IS_DISJOINTGTID_IS_DISJOINT_UNION可用于验证涉及多个数据库和服务器的备份和还原操作。此示例中,server1 包含数据库 db1,server2 包含数据库 db2。目标是将数据库 db2 复制到 server1,server1 上的结果应该是两个数据库的并集。过程是使用 mysqldump 备份 server2,然后在 server1 上恢复此备份。

如果 mysqldump 的选项--set-gtid-purged设置为 ON 或默认值为 AUTO,则程序的输出包含SET @@GLOBAL.gtid_purged语句,该语句将 server2 中的gtid_executed集添加到 server1 上的gtid_purged集。gtid_purged 集包含已在服务器上提交但在服务器上的任何二进制日志文件中不存在的所有事务的 GTID。将数据库 db2 复制到 server1 时,必须将 server2 上提交的事务的 GTID (不在 server1 上的二进制日志文件中) 添加到 server1 的gtid_purged集中以使该集完成。

7.3.2.1 GTID_IS_EQUAL

使用 GTID_IS_EQUAL 验证备份操作是否为SET @@GLOBAL.gtid_purged语句计算了正确的 GTID 集。在 server2 上,从mysqldump输出中提取该语句,并将 GTID 集存储到本地变量中,例如$gtid_purged_set。然后执行以下语句:

server2> SELECT GTID_IS_EQUAL($gtid_purged_set, @@GLOBAL.gtid_executed);

如果结果为1,则两个GTID集相等,并且已正确计算该集。

7.3.2.2 GTID_IS_DISJOINT

使用 GTID_IS_DISJOINT 验证 mysqldump 输出中设置的 GTID 与 server1 上的 gtid_executed 集不重叠。如果存在任何重叠,则在将数据库 db2 复制到 server1时会出现错误。将输出中的 gtid_purged 集提取并存储到如上所述的局部变量中,然后执行以下语句:

server1> SELECT GTID_IS_DISJOINT($gtid_purged_set, @@GLOBAL.gtid_executed);

如果结果为 1,则两个 GTID 集之间没有重叠,因此不存在重复的 GTID。

7.3.2.3 GTID_IS_DISJOINT_UNION

使用 GTID_IS_DISJOINT_UNION 验证还原操作是否导致 server1 上的 GTID 状态正确。在恢复备份之前,在 server1 上,通过执行以下语句获取现有的 gtid_executed 集:

server1> SELECT @@GLOBAL.gtid_executed;

将结果存储在本地变量$original_gtid_executed中 。还将 gtid_purged 集存储在局部变量中。当 server2 的备份已恢复到 server1 上时,执行以下语句以验证 GTID 状态:

server1> SELECT GTID_IS_DISJOINT_UNION($original_gtid_executed, 
                                       $gtid_purged_set, 
                                       @@GLOBAL.gtid_executed);

如果结果为1,则存储的函数已验证来自 server1 的原始 gtid_executed 集$original_gtid_executed和从 server2 添加的 gtid_purged集$gtid_purged_set没有重叠,并且 server1 上已更新的 gtid_executed 集现在包含来自server1的前一个 gtid_executed 集加上来自 server2 的 gtid_purged 集,这是所需的结果。确保在 server1 上进行任何进一步的事务之前执行此检查,否则 gtid_executed 集中的新事务将会失败。

7.3.3 手工选择作为新主库的从库

自定义函数 GTID_UNION 可用于从一组复制从库中识别最新的从库,以便在主库意外停止后执行手动切换。如果某些从库遇到复制延迟,则此函数可用于计算最新的从库,而无需等待所有从库应用完其现有的中继日志,从而最大限度地缩短主从切换时间。该函数可以返回每个从库上的gtid_executed集合与从库接收的事务集合的并集,后者记录在performance_schema.replication_connection_status表中。可以比较这些结果,以查找哪个从库的事务记录是最新的,即使并非所有交易都已提交。

在每个复制从属服务器上,通过发出以下语句来计算完整的事务记录:

SELECT GTID_UNION(RECEIVED_TRANSACTION_SET, @@GLOBAL.gtid_executed) 
    FROM performance_schema.replication_connection_status 
    WHERE channel_name = 'name';

然后比较每个从库的结果,选择具有最新事务记录的从库用作新主库。

7.3.4 检查从库上的异常事务

自定义函数 GTID_SUBTRACT_UUID 可用于检查从库是否只接收到源自其指定主库的事务。对于单主复制,执行以下语句,server_uuid_of_master 是主库的 server_uuid:

SELECT GTID_SUBTRACT_UUID(@@GLOBAL.gtid_executed, server_uuid_of_master);

如果结果不为空,则返回的事务是不是源自指定主库的异常事务。对于多主复制拓扑中的从库,重复该功能,例如:

SELECT GTID_SUBTRACT_UUID(GTID_SUBTRACT_UUID(@@GLOBAL.gtid_executed,
                                             server_uuid_of_master_1),
                                             server_uuid_of_master_2);

7.3.5 验证复制拓扑中的服务器是否执行过本地事务

自定义函数 GTID_INTERSECTION_WITH_UUID 可用于验证服务器是否执行过本地事务。可以在服务器上发出以下语句来检查:

SELECT GTID_INTERSECTION_WITH_UUID(@@GLOBAL.gtid_executed, my_server_uuid);

7.3.6 在多主复制设置中验证附加从库

假设 master1 和 master2 为双主复制,互为主从,同时 master2 还有自己的从库 slave3,如下图所示。

如果 master2 配置了log_slave_updates = ON,slave3 也将接收并应用master1的事务,如果 master2 使用log_slave_updates = OFF,则不会这样做。在这种情况下,自定义函数 GTID_INTERSECTION_WITH_UUID 可用于标识 master2 发起的事务,丢弃 master2 从 master1 复制的事务。然后可以使用内置函数 GTID_SUBSET 将结果与 slave3 上的 gtid_executed 集进行比较。如果 slave3 与 master2 保持同步,则 slave3 上的 gtid_executed 设置包含交集中的所有事务 (源自 master2 的事务)。要执行此检查,可将 master2 的 gtid_executed、master2 的 server_uuid 和 slave3 的 gtid_executed 集存储到客户端变量中,例如:

    $master2_gtid_executed :=
      master2> SELECT @@GLOBAL.gtid_executed;
    $master2_server_uuid :=
      master2> SELECT @@GLOBAL.server_uuid;
    $slave_gtid_executed :=
      slave3> SELECT @@GLOBAL.gtid_executed;

然后使用 GTID_INTERSECTION_WITH_UUID 和 GTID_SUBSET 将这些变量作为输入,例如在 slave3 上执行:

SELECT GTID_SUBSET(GTID_INTERSECTION_WITH_UUID($master2_gtid_executed,
                                               $master2_server_uuid),
                                               $slave_gtid_executed);

来自 master2 的服务器标识符$master2_server_uuid与 GTID_INTERSECTION_WITH_UUID 一起使用,以识别并返回源自 master2 的 gtid_executed 集合中的那些 GTID,省略源自 master1 的那些 GTID。然后使用 GTID_SUBSET 将得到的 GTID 集与从库上所有已执行 GTID 的集合进行比较。如果此语句返回非零 (true),则来自 master2 的所有已识别的 GTID (第一个集输入) 也位于从库的 gtid_executed 集(第二个集输入)中,这意味着从库已复制源自 master2 的所有事务。

8. GTID限制

8.1 涉及非事务存储引擎的更新

使用 GTID 时,一条语句或一个事务中,不能对非事务性存储引擎 (如 MyISAM ) 表和事务存储引擎 (如 InnoDB ) 的表一起更新,因为这种混合引擎同时更新可能导致将多个 GTID 分配给同一事务。下面两组命令都会报同样的错误。

use test;
create table t_myisam(a int) engine=myisam;
create table t_innodb(a int) engine=innodb;
update t_myisam, t_innodb set t_myisam.a=1, t_innodb.a=1;
 
begin;
insert into t_myisam select 1;
insert into t_innodb select 1;
update t_myisam set a=2;
update t_innodb set a=2;
commit;
ERROR 1785 (HY000): Statement violates GTID consistency: Updates to non-transactional tables can only be done in either autocommitted statements or single-statement transactions, and never in the same statement as updates to transactional tables.

在 MySQL 8 中,这个限制并没有多大影响,因为包括系统表在内都是 InnoDB 表,缺省已经没有 MyISAM 表了,除非用户建表时显示定义。

8.2 CREATE TABLE ... SELECT语句

CREATE TABLE ...使用基于 GTID 的复制时不允许使用 SELECT 语句。

  • binlog_format设置为 STATEMENT 时,CREATE TABLE ... SELECT 语句作为一个具有单一 GTID 的事务记录在二进制日志中。
  • 但如果使用 ROW 格式,则该语句将记录为具有两个 GTID 的两个事务。
  • 如果主服务器使用 STATEMENT 格式而从服务器使用 ROW 格式,则从服务器将无法正确处理事务,因此 GTID 不允许使用 CREATE TABLE ... SELECT 语句来防止出现这种情况。
mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.00 sec)
 
mysql> create table t2 as select * from t1;
ERROR 1786 (HY000): Statement violates GTID consistency: CREATE TABLE ... SELECT.
mysql> set binlog_format=statement;
Query OK, 0 rows affected (0.00 sec)
 
mysql> create table t2 as select * from t1;
ERROR 1786 (HY000): Statement violates GTID consistency: CREATE TABLE ... SELECT.
mysql>

8.3 临时表

  • binlog_format设置为 STATEMENT,服务器上启用 GTID 时,不能在事务、过程、函数或触发器内使用CREATE TEMPORARY TABLEDROP TEMPORARY TABLE语句。如果设置了autocommit = 1,则可以在使用 GTID 时在这些上下文之外使用它们。
  • 从 MySQL 8.0.13 开始,当binlog_format设置为 ROW 或 MIXED 时且启用 GTID 时,允许在事务、过程、函数或触发器内使用CREATE TEMPORARY TABLEDROP TEMPORARY TABLE语句。这些语句不会写入二进制日志,因此不会复制到从库。
mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.01 sec)
 
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
 
mysql> create temporary table tmp1 select * from t1;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0
 
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
 
mysql> set binlog_format=statement;
ERROR 3745 (HY000): Changing @@session.binlog_format is disallowed when the session has open temporary table(s). You could wait until these temporary table(s) are dropped and try again.
mysql> drop temporary table tmp1;
Query OK, 0 rows affected (0.00 sec)
 
mysql> set binlog_format=statement;
Query OK, 0 rows affected (0.00 sec)
 
mysql> create temporary table tmp1 select * from t1;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0
 
mysql> drop temporary table tmp1;
Query OK, 0 rows affected (0.00 sec)
 
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
 
mysql> create temporary table tmp1 select * from t1;
ERROR 3748 (HY000): Statement violates GTID consistency: CREATE TEMPORARY TABLE and DROP TEMPORARY TABLE are not allowed inside a transaction or inside a procedure in a transactional context when @@session.binlog_format=STATEMENT.
mysql>

要防止执行会导致基于 GTID 的复制失败的语句,必须在启用 GTID 时使用--enforce-gtid-consistency选项启动所有服务器。这会导致前面讨论的语句失败并显示错误。

8.4 忽略服务器

使用 GTID 时,不推荐使用CHANGE MASTER TO语句的IGNORE_SERVER_IDS选项,因为已经应用的事务会自动被忽略。在启动基于 GTID的复制之前,需要检查并清除(CHANGE MASTER TO IGNORE_SERVER_IDS = ();)之前在相关服务器上设置的所有忽略的服务器ID列表。可以为各个通道发出的SHOW SLAVE STATUS语句显示被忽略的服务器 ID 列表(如果有)。如果没有则Replicate_Ignore_Server_Ids字段为空。

8.5 GTID模式和mysqldump

可以将使用 mysqldump 创建的转储导入到启用了 GTID 模式的 MySQL 服务器中,前提是目标服务器的二进制日志中没有重叠的 GTID。

9. 主从开关机顺序

==== 关机 ===================================
#1.查看当前的主从同步状态,看是否双yes
mysql> show slave status\G;
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
#2.关闭从库
	#2.1 停止从库同步
	mysql> stop slave;
	#2.2 停止从库服务
	[root@Mysql57-Slave ~]# 
	mysqladmin shutdown -uroot -proot
	#2.3 查看服务状态
	systemctl status mysqld
	#如果部署了多个实例,那每个实例都要按照以上步骤来操作
#3.关闭主库
	#3.1 停止主库服务
	[root@Mysql57-Master ~]# 
	mysqladmin shutdown -uroot -proot
	#3.2 查看服务状态
	systemctl status mysqld
==== 开机 ===================================
#1.启动主库
	#1.1 开启主库服务
	mysqladmin start -uroot -proot
	#1.2 查看服务状态
	[root@Mysql57-Master ~]# mysql -uroot -proot
	mysql> systemctl status mysqld
#2.启动从库
	#2.1 启动从库服务
	mysqladmin start -uroot -proot
	#2.2 启动从库同步
	[root@Mysql57-Slave ~]# mysql -uroot -proot
	mysql> start slave;
	#2.3 检查同步状态
	mysql> show slave status\G;
	#2.4 查看服务状态
	[root@Mysql57-Slave ~]# systemctl status mysqld

posted on 2023-11-13 15:50  一介IT  阅读(480)  评论(0)    收藏  举报

导航