1.什么是 GTID

GTID (Global Transaction Identifiers)是对于一个已提交事务的编号,事务的唯一编号,并且是一个全局唯一的编号。GTID 和事务会记录到 binlog 中,用来标识事务。
GTID 是用来替代以前 classic 复制方法,MySQL-5.6.2 开始支持 GTID,在 MySQL-5.6.10 后完善。
有了 GTID,一个事务在集群中就不再孤单,在每一个节点中,都存在具有相同标识符的兄弟们和它作伴,可以避免同一个事务,在同一个节点中出现多次的情况。
GTID 的出现,最直接的效果就是,每一个事务在集群中具有了唯一性的意义,这在运维方面具有更大的意义,因为使用 GTID 后再也不需要为了不断地找点而烦恼了,给 DBA 带来了很大的便利性。

GTID 组成:
GTID 是由 server_uuid:Sequence_Number 。
Server_Uuid:是一个 MySQL 实例的全局唯一标识;存放为在$datadir/auto.cnf
Sequence_Number:是 MySQL 内部的一个事务的编号,一个 MySQL 实例不会重复的序列号(保证服务器内唯一),也表示在该实例上已经提交事务的数量,并且随着事务提交而递增。
根据 GTID 可以知道事务最初是在哪个实例上提交的,方便故障排查和切换。

2.GTID 主从复制原理

(1) 当一个事务在主库端执行并提交时,产生 GTID,一同记录到 binlog 日志中。
(2) binlog 传输到 slave,并存储到 slave 的 relaylog 后,读取这个 GTID 的这个值设置 gtid_next 变量,即告诉 Slave,下一个要执行的 GTID 值。
(3) sql 线程从 relay log 中获取 GTID,然后对比 slave 端的 binlog 是否有该 GTID。
(4) 如果有记录,说明该 GTID 的事务已经执行,slave 会忽略。
(5) 如果没有记录,slave 就会执行该 GTID 事务,并记录该 GTID 到自身的 binlog;
(6) 在解析过程中会判断是否有主键,如果没有就用二级索引,如果没有就用全部扫描。

3.GTID 优势和限制

GTID 的优势:
(1) 根据 GTID 可以快速的确定事务最初是在哪个实例上提交的。
(2) 简单的实现 failover,不用以前那样在需要找 log_file 和 log_pos。
(3) 更简单的搭建主从复制,确保每个事务只会被执行一次。
(4) 比传统的复制更加安全。
(5) GTID 的引入,让每一个事务在集群事务的海洋中有了秩序,使得 DBA 在运维中做集群变迁时更加方便,能够做到胸有成竹心中有数。
GTID 的限制:
因为基于 GTID 的复制依赖于事务,所以在使用 GTID 时,有些 MySQL 特性是不支持的。
(1) 不允许在一个 SQL 同时更新一个事务引擎和非事务引擎的表;
事务中混合多个存储引擎,就会产生多个 GTID。当使用 GTID 时,如果在同一个事务中,更新包括了非事务引擎(如 MyISAM)和事务引擎(如 InnoDB)表的操作,就会导致多个 GTID 分配给了同一个事务。

(2) 主从库的表存储引擎必须是一致的;
主从库的表存储引擎不一致,就会导致数据不一致。如果主从库的存储引擎不一致,例如一个是事务存储引擎,一个是非事务存储引擎,则会导致事务和 GTID 之间一对一的关系被破坏,结果就会导致基于 GTID 的复制不能正确运行;

(3) 不支持 create table … select 语句复制(主库直接报错)
由于使用基于行模式的复制时,create table ...select 语句会被记录为两个单独的事件(会生成两个 sql),一个是 DDL 创建表 SQL,一个是 insert into 插入数据的 SQL。由于 DDL 会导致自动提交,所以这个 sql 至少需要两个 GTID,但是 GTID 模式下,只能给这个 sql 生成一个 GTID,如果强制执行会导致和上面(2)中一样的结果。

(4) 在一个复制组中,必须要求统一开启 GTID 或是关闭 GTID;

(5) 开启 GTID 需要重启(5.6 需要,5.7 中不需要)

(6) 开启 GTID 后,就不能在使用原来的传统的复制方式;

(7) 不支持 create temporary table 和 drop temporary table 语句;
使用 GTID 复制时,不支持 create temporary table 和 drop temporary table ,但是在 autocommit=1 情况下可以创建临时表,MASTER 创建临时表不产生 GTID 信息,所以不会同步到 SLAVE 上,但是删除临时表时,产生 GTID 会导致主从复制中断。

(8) 不推荐在 GTID 模式的实例上进行 mysql_upgrade;
因为 mysql_upgrade 的过程要创建或修改系统表(非事务引擎),所以不建议在开启 GTID 的模式的实例上使用带有--write-binlog 选项的 mysql_upgrade;

(9) 不支持 sql_slave_skip_counter;

4.为什么要使用 GTID

GTID 的存在方便了 Replication 的 Failover

假如我们有如下的环境:


问题场景:

从图中可看出此时,Master 服务器宕机,需要将写业务切换到 New Master 上。同时,Slave2 的复制源改为 New Master 。

在 MySQL 5.6 GTID 出现之前 Replication failover 的操作过程:
修改复制源的命令语法为:

mysql> CHANGE MASTER TO
     MASTER_HOST='XXXX',
     MASTER_USER='XXXX',
     MASTER_PASSWORD='XXXXX',
     MASTER_LOG_FILE='XXXXX',
     MASTER_LOG_POS=XXXXX;

而比较麻烦的地方是:由于同一个事务在每台服务器上所在的 binlog 名字和 Postion 位置点都不一样,那么怎么找到 slave2 当前同步停止点,对应 New Master 的 master_log_file 和 Master_log_pos 是什么的时候就成为了难题。这也就是为什么 M-S 复制集群需要使用 MMM,MHA 这样的额外管理工具的一个重要原因。
其实也可以找到,只是比较麻烦,我们都知道主从复制环境中 master 的 binlog 复制到 slave 上后 事务执行时的时间戳是不变的,所有 slave 上同一个事务的时间戳都是相同的。可以根据这个时间戳定位到 Master_log_file 和 Master_log_pos。只是很费时间;麻烦。。。

GTID 出现之后:
在 MySQL 5.6 的 GTID 出现之后,处理这个问题就非常简单了。
由于同一个事务的 GTID 在所有的节点上都是一致的,那么根据 Slave 当前停止点的 GTID 就能唯一定位到 New Master 的 GTID。
更简单的是,由于 MASTER_AUTO_POSITION 功能的出现,我们都不需要知道 GTID 的具体值。直接使用

mysql> CHANGE MASTER TO
     MASTER_HOST='XXXX',
     MASTER_USER='XXXXX',
     MASTER_PASSWORD='XXXXX',
     MASTER_PORT=3306,
     MASTER_AUTO_POSITION=1;

命令就可以直接完成 failover 的工作了。
使用 GTID 处理这个问题就简单很多。。

5.如何记录 GTID

MySQL 5.7 GTID 会存储到 msyql.gtid_executed 表里
支持 slave 不开启 binlog (log-slave-updates)

CREATE TABLE `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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

分开启 binlog 和未开启 binlog 两种情况:
第一种情况:开启 binlog:
如果开启 binlog ,在 binlog 切换时,将当前的 GTID 插入到 gtid_executed 表中;

insert into mysql.gtid_executed(UUID,1000,2000);

第二种情况:未开启 binlog:
如果没有开启 binlog,那么每个事务提交前,将会执行一个 insert 操作;

BEGIN;
................[事务操作].................
insert into mysql.gtid_execute(UUID,1000,2000);   #隐式 MySQL 内部添加
COMMIT;

6.创建 GTID 模式主从复制的方法

(1) 如果 MASTER 是新搭建的并且无数据:
这种情况我们可以直接使用 CHANGE MASTER TO 语句来搭建 GTID 主从;

mysql> CHANGE MASTER TO 
MASTER_HOST='XXXX',
MASTER_USER='XXXX',
MASTER_PASSWORD='XXXX',
MASTER_PORT=XXXX,
MASTER_AUTO_POSITION=1;

(2) MASTER 运行不久,并且所有的 Binlog 保存完整:
这种情况,可以使用类似(1)方式搭建,我们可以直接使用 CHANGE MASTER TO 语句,从 MASTER 上获取所有的 GTID,然后在 SLAVE 上执行。
有点事简单快捷,缺点是如果 Binlog 相对比较多,SLAVE 同步时间就会较大,可能导致网络压力过大。

(3) MASTER 具有大量数据,并且 Binlog 日志保留不全:
针对这种情况,可能就不能使用(2) 方法了,因为最原始的 Binlog 已经被删除了,无法从开头获取所有的 GTID 信息。
那么需要从 MASTER 上获取数据及该数据的 GTID 范围,然后通过在 SLAVE 上设置选项gtid_purged的方式来跳过这些 GTID。最后通过 CHANGE MASTER TO 的方式搭建 GTID 模式主从,具体操作步骤如下:

  • 利用备份方式获取 MASTER 的数据及 GTID 范围,包含了gtid_purged='uuid:interval[-interval]'。使用 innobackupex 备份会将信息保存在xtrabackup_binlog_info文件中。
  • 利用备份的数据,搭建 SLAVE 实例;
  • 启动 SLAVE 实例,并且设置 gtid_purged 的值,跳过这段范围。命令为:
SET @@GLOBAL.GTID_PURGED='uuid:interval[-interval]'
  • 利用 CHANGE MASTER TO 语句,配置主从复制;
  • 启动 SLAVE 复制,SLAVE 会自动跳过这段 GTID 范围,拉取最新的 GTID 信息。

二、创建基于 GTID 主从复制

1.安装 MySQL 5.7

请参考 我的另一篇文章:
Centos7 安装 MySQL5.7

2.配置 GTID 参数

master:/etc/my.cnf

# GTID
gtid-mode = ON  #开启 GTID 模式
enforce-gtid-consistency = ON  #使用 GTID 模式复制时,需要开启此参数,用来保证 GTID 的一致性;
server-id =2303306
# bin log 
binlog_format = ROW #建议使用 ROW 格式,其他格式可能造成数据不一致
log-bin = /data/mysql/mysql3306/logs/mysql-bin     #必须开启 Binlog

slave:/etc/my.cnf

# GTID
gtid-mode = ON
enforce-gtid-consistency = ON
server-id =1313306
binlog_format = ROW
log-bin = /data/mysql/mysql3306/logs/mysql-bin
log_slave_updates = ON  #建议开启
skip-slave-start = 1   # 当 slave 数据库重启的时候,slave 不会自动开启复制

3.创建复制账号

mysql> grant replication slave on *.* to 'repl'@'192.168.199.%' identified by 'unixfbi';
mysql> flush privileges;

4.备份 master 数据

如果是新库,可以省略该步骤,直接在 slave 上直接 change master to ... 语句

# mysqldump -uroot -p --master-data=2 --single-transaction -R --triggers --events -A > master.sql

确认一下 master 实例 GTID 信息:

root@localhost [(none)]>show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000010 |      194 |              |                  | b5a3240c-8946-11e7-bf07-d067e528dfb8:1-13 |
+------------------+----------+--------------+------------------+-------------------------------------------+
1 row in set (0.00 sec)

5.恢复到 slave 实例

# scp  master.sql  root@192.168.199.131:~/
# mysql -uroot -p < master.sql

6.开始复制

mysql> CHANGE MASTER TO 
MASTER_HOST='192.168.199.230',
MASTER_USER='repl',
MASTER_PASSWORD='unixfbi',
MASTER_PORT=3306,
MASTER_AUTO_POSITION=1;

start slave;

7.查看复制状态

root@localhost [(none)]>show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.199.230
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000010
          Read_Master_Log_Pos: 194
               Relay_Log_File: relay-bin.000002
                Relay_Log_Pos: 367
        Relay_Master_Log_File: mysql-bin.000010
             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: 194
              Relay_Log_Space: 568
              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: 2303306
                  Master_UUID: b5a3240c-8946-11e7-bf07-d067e528dfb8
             Master_Info_File: /data/mysql/mysql3306/data/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave 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: 
            Executed_Gtid_Set: b5a3240c-8946-11e7-bf07-d067e528dfb8:1-13
                Auto_Position: 1
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
1 row in set (0.00 sec)

确认一下全备份时的 GTID 信息:
我们在 mysqldump 时导出的数据里面已经包含了执行备份时 master 的 GTID 信息

SET @@SESSION.SQL_LOG_BIN= 0;

--
-- GTID state at the beginning of the backup 
--

SET @@GLOBAL.GTID_PURGED='b5a3240c-8946-11e7-bf07-d067e528dfb8:1-13';

备份时 master 的 GTID 为:b5a3240c-8946-11e7-bf07-d067e528dfb8:1-13

通过对比 slave 的 show slave status\G 中的 Executed_Gtid_Set: b5a3240c-8946-11e7-bf07-d067e528dfb8:1-13 和 master 备份时的 GTID 可以得出 已经同步完成。

三、GTID 主从复制怎么确定从哪个 GTID 开始复制

GTID 做主从复制时,slave 是怎么知道应该从哪个 GTID 事务开始复制?
1.执行全备时,会在备份文件中记录当时的 GTID 信息 SET @@GLOBAL.GTID_PURGED='b5a3240c-8946-11e7-bf07-d067e528dfb8:1-5';

2.从库进行恢复,会将从库的 gtid_purged 设置为 master 在执行备份时的 Executed_Gtid_Set (也可以理解为全备数据文件中的 SET @@GLOBAL.GTID_PURGED='b5a3240c-8946-11e7-bf07-d067e528dfb8:1-5'; )告诉主库 gtid_purged 里的 GTID 事务我已经执行完成了。
3.Slave 连接 Master 时,会把 gtid_executed 中的 gtid 发给 master,Master 会 Skip 过 Executed_Gtid_Set 把没有执行过的 GTID 事务发送给 Slave;

全备数据文件中有:SET @@GLOBAL.GTID_PURGED='b5a3240c-8946-11e7-bf07-d067e528dfb8:1-5'; 表示备份时 master 已经执行完成了 1-5,所以从库会从 6 开始复制;

四、GTID 复制参数介绍

mysql> show variables like '%GTID_%';
+----------------------------------+-----------+
| Variable_name                    | Value     |
+----------------------------------+-----------+
| binlog_gtid_simple_recovery      | ON        |
| enforce_gtid_consistency         | ON        |
| gtid_executed_compression_period | 1000      |
| gtid_mode                        | ON        |
| gtid_next                        | AUTOMATIC |
| gtid_owned                       |           |
| gtid_purged                      |           |
| session_track_gtids              | OFF       |
+----------------------------------+-----------+
8 rows in set (0.00 sec)

gtid_mode:
- ON: 产生 GTID,slave 只接受带 GTID 的事务
- ON_PERMISSIVE: 产生 GTID,slave 接受不带 GTID 事务也接受带 GTID 的事务
- OFF : 不产生 GTID,slave 只接受不带参 GTID 的事务
- OFF_PERMISSIVE: 不产生 GTID,slave 接受不带 GTID 事务也接受带 GTID 的事务

enforce-gtid-consistency
- ON: 当发现语句/事务不支持 GTID 时,返回错误信息
- WARN: 当发现不支持语句/事务,返回警告,并在日志中记录警告信息
- OFF: 不检查是否有 GTID 不支持的语句/事务
在线上从非 GTID 到 GTID 切换过程中,可以先设置成:WARN
gtid_executed_compression_period
这个参数是控制表压缩率

mysql> set global gtid_executed_compression_period=N (N 事务个数,默认是 1000)

如果这个值设置为 2000
压缩前:

source_uuid

interval_start

interval_end

uuid

1

1001

uuid

1002

2001

压缩后:

source_uuid

interval_start

interval_end

uuid

1

2001

uuid

2002

4002

五、GTID 添加 slave 方法

通过备份搭建新的 slave

方法一:mysqldump 的方式

使用 mysqldump 的命令在 dump 文件里可以看到下面两个信息:
SET @@GLOBAL.GTID_PURGED='b5a3240c-8946-11e7-bf07-d067e528dfb8:1-13';3、将备份还原到 slave 后,使用 change master to 命令挂载 master 端。

注意:在 mysql5.6.9 以后的命令才支持这个功能。

方法二:percona Xtrabackup
原理:获取 master 的数据和这些数据对应的 GTID 范围,然后通过 slave 设置@@global.gtid_purged 跳过备份包含的 gtid。
(1) Xtrabackup_binlog_info 文件中,包含 global.gtid_purged='XXXXXX:XXXX'的信息。
(2) 然后到 slave 去手工执行 SET GLOBAL.GTID_PURGED='XXXXXX:XXXX'。
(3) 恢复备份,开启 change master to 命令。

注意:如果 MASTER 运行了很久,Binlog 日志信息不全,可以通过上面的方式进行查找。

六、GTID 跳过一个事务

怎么跳过事务?
1.这个功能主要跳过事务,代替原来的 set global sql_slave_skip_counter = 1。
2.由于在这个 GTID 必须是连续的,正常情况同一个服务器产生的 GTID 是不会存在空缺的。所以不能简单的 skip 掉一个事务,只能通过注入空事物的方法替换掉一个实际操作事务。

假如主从复制出现错误
当在 slave 上执行 show slave status\G
Retrieved_Gtid_Set: UUID:1-12
Executed_Gtid_Set: UUID:1-8
此时 Slave_SQL_Running: No
上面的信息表明:
slave 收到了 UUID:1-12 个事务,执行成功 UUID:1-8,1-8 表示已经执行完成了。在这里出现了错误,也就是说执行 UUID:1-9 时出现了错误。所以我们应该要跳过下一个事务即 9;
解决方法:
按照下列步骤执行

stop slave  sql_thread;
set gtid_next='UUID:9'     #Executed_Gtid_Set + 1
begin; 
commit;          #执行一个空事务
set  gtid_next='automatic';
start  slave sql_thread;

跳过事务,使 SLAVE 正常运行,这种方式虽然有时候在处理问题时,确实很方便,但也是非常危险的,因为可能会出现事务不一致的情况。所以,在跳过之前,分析一下 Binlog 并且记录下来,分析是否可以跳过。跳过之后,看一下主从数据是否一致,是否需要修复数据等等。总之,需要具体问题具体分析,请谨慎操作。