MySQL5.7 GTID原理与实战

alt

工作多年,当人家问你是不是初入社会,不是因为你看起来年轻,而是因为觉得你怎么这么笨。

前言

GTID是什么?

GTID 是Global Transaction Identifiers的缩写,简称GTID

GTID组成和架构

1) GTID = source_id:transaction_id
2) server_uuid 来源于 auto.cnf

1
3E11FA47-71CA-11E1-9E33-C80AA9429562

3) GTID: 在一组复制中,全局唯一

The syntax for a GTID set is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gtid_set:
uuid_set [, uuid_set] ...
| ''

uuid_set:
uuid:interval[:interval]...

uuid:
hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh

h:
[0-9|A-F]

interval:
n[-n]

(n >= 1)

mysql.gtid_executed表的压缩由名为thread/sql/compress_gtid_table的专用前台线程执行。 此线程未在SHOW PROCESSLIST的输出中列出,但可以将其视为线程表中的一行,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> SELECT * FROM performance_schema.threads WHERE NAME LIKE '%gtid%'\G
*************************** 1. row ***************************
THREAD_ID: 26
NAME: thread/sql/compress_gtid_table
TYPE: FOREGROUND
PROCESSLIST_ID: 1
PROCESSLIST_USER: NULL
PROCESSLIST_HOST: NULL
PROCESSLIST_DB: NULL
PROCESSLIST_COMMAND: Daemon
PROCESSLIST_TIME: 1509
PROCESSLIST_STATE: Suspending
PROCESSLIST_INFO: NULL
PARENT_THREAD_ID: 1
ROLE: NULL
INSTRUMENTED: YES
HISTORY: YES
CONNECTION_TYPE: NULL
THREAD_OS_ID: 18677

GTID和Binlog的关系
  • GTID在binlog中的结构

  • GTID event 结构

  • Previous_gtid_log_event

  1. Previous_gtid_log_event 在每个binlog 头部都会有
  2. 每次binlog rotate的时候存储在binlog头部
  3. Previous-GTIDs在binlog中只会存储在这台机器上执行过的所有binlog,不包括手动设置gtid_purged值。
  4. 换句话说,如果你手动set global gtid_purged=xx; 那么xx是不会记录在Previous_gtid_log_event中的。
  • GTID和Binlog之间的关系是怎么对应的呢
    如何才能找到GTID=?对应的binlog文件呢?
    1
    2
    3
    4
    5
    6
    7
    8
    * 假设有4个binlog: bin.001,bin.002,bin.003,bin.004
    * bin.001 : Previous-GTIDs=empty; binlog_event有:1-40
    * bin.002 : Previous-GTIDs=1-40; binlog_event有:41-80
    * bin.003 : Previous-GTIDs=1-80; binlog_event有:81-120
    * bin.004 : Previous-GTIDs=1-120; binlog_event有:121-160
    1. 假设现在我们要找GTID=$A,那么MySQL的扫描顺序为: 从最后一个binlog开始扫描(即:bin.004)
    2. bin.004的Previous-GTIDs=1-120,如果$A=140 > Previous-GTIDs,那么肯定在bin.004中
    3. bin.004的Previous-GTIDs=1-120,如果$A=88 包含在Previous-GTIDs中,那么继续对比上一个binlog文件 bin.003,然后再循环前面2个步骤,直到找到为止
重要参数的持久化
  • GTID相关参数
variables comment
gtid_executed 执行过的所有GTID
gtid_purged 丢弃掉的GTID
gtid_mode gtid模式
gtid_next session级别的变量,下一个gtid
gtid_owned 正在运行的gtid
enforce_gtid_consistency 保证GTID安全的参数
  • 重要参数如何持久化
  1. 如何持久化gtid_executed [ log-bin=on,log_slave_update=on ]
    1
    2
    3
    gtid_executed = mysql.gtid_executed #[normal]
    or
    gtid_executed = mysql.gtid_executed + last_binlog 中最后没写到mysql.gtid_executed中的gtid_event #[recover]
  • 如何持久化重置的gtid_purged值?
    1
    2
    reset master;
    set global gtid_purged='$A:a-b';
1
2
3
4
1. 由于有可能手动设置过gtid_purged=$A:a-b, binlog.index中,last_binlog的Previous-GTIDs并不会包含$A:a-b  
2. 由于有可能手动设置过gtid_purged=$A:a-b, binlog.index中,first_binlog的Previous-GTIDs肯定不会出现$A:a-b
3. 重置的gtid_purged = @@global.gtid_executed(mysql.gtid_executed:注意,考虑到这个表的更新触发条件,所以这里用@@global.gtid_executed代替) - last_binlog的Previous-GTIDs - last_binlog所有的gtid_event
4. 下面就用 $reset_gtid_purged 来表示重置的gtid
  • 如何持久化gtid_purged [ log-bin=on,log_slave_update=on ]
    1
    gtid_purged=binlog.index:first_binlog的Previous-GTIDs  + $reset_gtid_purged
开启GTID的必备条件
  • MySQL 5.6

    1
    2
    3
    4
    gtid_mode=ON                (必选)    
    log_bin=ON (必选)
    log-slave-updates=ON (必选)
    enforce-gtid-consistency (必选)
  • MySQL 5.7

    MySQL5.7.13 or higher

    1
    2
    3
    4
    gtid_mode=ON                (必选)  
    enforce-gtid-consistency (必选)
    log_bin=ON (可选)--高可用切换,最好设置ON
    log-slave-updates=ON (可选)--高可用切换,最好设置ON
新的复制协议 COM_BINLOG_DUMP_GTID
  • slave会将已经执行过的gtid,以及以及接受到relay log中的gtid的并集发送给master

    1
    2
    * http://dev.mysql.com/doc/refman/5.7/en/change-master-to.html
    UNION(@@global.gtid_executed, Retrieved_gtid_set - last_received_GTID)
  • Master send all other transactions to slave

  • 同样的GTID不能被执行两次,如果有同样的GTID,会自动被skip掉。
- slave1 : 将自身的UUID1:1 发送给 master,然后接收到了 UUID1:2,UUID1:3 event
- slave2 : 将自身的UUID1:1,UUID1:2 发送给 master,然后接收到了UUID1:3 event
GTID重要函数和新语法
  • 重要函数
Name Description
GTID_SUBSET(subset, set) returns true (1) if all GTIDs in subset are also in set
GTID_SUBTRACT(set,subset) returns only those GTIDs from set that are not in subset
WAIT_FOR_EXECUTED_GTID_SET(gtid_set[, timeout]) Wait until the given GTIDs have executed on slave.
WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(gtid_set[, timeout][,channel]) Wait until the given GTIDs have executed on slave.
  • 新语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    START SLAVE [thread_types] [until_option] [connection_options]
    thread_types:
    [thread_type [, thread_type] ... ]
    thread_type:
    IO_THREAD | SQL_THREAD
    until_option:
    UNTIL { {SQL_BEFORE_GTIDS | SQL_AFTER_GTIDS} = gtid_set
    | MASTER_LOG_FILE = 'log_name', MASTER_LOG_POS = log_pos
    | RELAY_LOG_FILE = 'log_name', RELAY_LOG_POS = log_pos
    | SQL_AFTER_MTS_GAPS }
  • 举个栗子:

  1. START SLAVE SQL_THREAD UNTIL SQL_BEFORE_GTIDS = 3E11FA47-71CA-11E1-9E33-C80AA9429562:11-56

    表示,当SQL_thread 执行到3E11FA47-71CA-11E1-9E33-C80AA9429562:10 的时候停止,下一个事务是11

  2. START SLAVE SQL_THREAD UNTIL SQL_AFTER_GTIDS = 3E11FA47-71CA-11E1-9E33-C80AA9429562:11-56

    表示,当SQL_thread 执行到3E11FA47-71CA-11E1-9E33-C80AA9429562:56 的时候停止,56是最后一个提交的事务。

GTID有什么好处

classic replication [运维之伤]
GTID replication [so easy]

GTID的Limitation

  • 不安全的事务
    1
    设置enforce-gtid-consistency=ON
1
2
3
1. CREATE TABLE ... SELECT statements  
2. CREATE TEMPORARY TABLE or DROP TEMPORARY TABLE statements inside transactions
3. 同时更新 事务引擎 和 非事务引擎。

MySQL5.7 GTID crash-safe

关于 GTID crash safe 可以参考官方文档列出的安全配置

  • 单线程复制
    Non-GTID 推荐配置:
    • relay_log_recovery=1
    • relay_log_info_repository=TABLE
    • master_info_repository=TABLE

GTID 推荐配置:

* MASTER_AUTO_POSITION=on
* relay_log_recovery=0
  • 多线程复制
    Non-GTID 推荐配置:
    • relay_log_recovery=1
    • sync_relay_log=1
    • relay_log_info_repository=TABLE
    • master_info_repository=TABLE
      GTID 推荐配置:
    • MASTER_AUTO_POSITION=on
    • relay_log_recovery=0

实战

使用GTID搭建Replication

从0开始搭建
  • step 1: 让所有server处于同一个点

    1
    mysql> SET @@global.read_only = ON;
  • step 2: 关闭所有MySQL

    1
    shell> mysqladmin -uusername -p shutdown
  • step 3: 重启所有MySQL,并开启GTID

    1
    shell> mysqld --gtid-mode=ON --log-bin --enforce-gtid-consistency &

当然,在my.cnf中配置好最佳

  • step 4: change master

    1
    2
    3
    mysql> CHANGE MASTER TO MASTER_HOST = host, MASTER_PORT = port, MASTER_USER = user, MASTER_PASSWORD = password, MASTER_AUTO_POSITION = 1, MASTER_CONNECT_RETRY=10;

    mysql> START SLAVE;
  • step 5: 让master 可读可写

    1
    mysql> SET @@global.read_only = OFF;
从备份中恢复&搭建
  • step 1: 备份

    1
    2
    3
    mysqldump xx 获取并且记录gtid_purged值  
    or
    冷备份 --获取并且记录gtid_executed值,这个就相当于mysqldump中得到的gtid_purged
  • step 2: 在新服务器上reset master,导入备份

    1
    2
    3
    reset master; --清空gtid信息  
    导入备份; --如果是逻辑导入,请设置sql_log_bin=off
    set global gtid_purged='xx';
  • step 3: change master

    1
    2
    3
    mysql> CHANGE MASTER TO MASTER_HOST = host, MASTER_PORT = port, MASTER_USER = user, MASTER_PASSWORD = password, MASTER_AUTO_POSITION = 1, MASTER_CONNECT_RETRY=10;

    mysql> START SLAVE;

如何从classic replication 升级成 GTID replication

offline 方式升级

offline 的方式升级最简单。全部关机,然后配置好GTID,重启,CHANGE MASTER TO MASTER_AUTO_POSITION=1

online 方式升级

这里先介绍几个重要GTID_MODEvalue

  • GTID_MODE = OFF 不产生Normal_GTID,只接受来自master的ANONYMOUS_GTID
  • GTID_MODE = OFF_PERMISSIVE 不产生Normal_GTID,可以接受来自master的ANONYMOUS_GTID & Normal_GTID
  • GTID_MODE = ON_PERMISSIVE 产生Normal_GTID,可以接受来自master的ANONYMOUS_GTID & Normal_GTID
  • GTID_MODE = ON 产生Normal_GTID,只接受来自master的Normal_GTID

master和slave的gtid_mode 组合搭配矩阵图

水平的GTID_MODE为:master

垂直的GTID_MODE为:slave

gtid_mode OFF(master) OFF_PERMISSIVE(master) ON_PERMISSIVE(master) ON(master)
OFF(slave) Y Y N N
OFF_PERMISSIVE(slave) Y Y Y Y(auto_position可以开启)
ON_PERMISSIVE(slave) Y Y Y Y(auto_position可以开启)
ON(slave) N N Y Y(auto_position可以开启)

归纳总结:

  1. 当master产生Normal_GTID的时候(ON_PERMISSIVE,ON),如果slave的gtid_mode(OFF)不能接受Normal_GTID,那么就会报错
  2. 当master产生ANONYMOUS_GTID的时候(OFF_PERMISSIVE,OFF),如果slave的gtid_mode(ON)不能接受ANONYMOUS_GTID,那么就会报错
  3. 设置auto_position的条件: 当master gtid_mode=ON时,slave可以为OFF_PERMISSIVE,ON_PERMISSIVE,ON。除此之外,都不能设置auto_position = on

下面罗列下,如何online 升级为GTID模式。

  • step 1: 每台server执行

    检查错误日志,直到没有错误出现,才能进行下一步

    1
    SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = WARN;
  • step 2: 每台server执行

    1
    SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = ON;
  • step 3: 每台server执行

    不用关心一组复制集群的server的执行顺序,只需要保证每个Server都执行了,才能进行下一步

    1
    SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE;
  • step 4: 每台server执行

    不用关心一组复制集群的server的执行顺序,只需要保证每个Server都执行了,才能进行下一步

    1
    SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE;
  • step 5: 在每台server上执行,如果ONGOING_ANONYMOUS_TRANSACTION_COUNT=0就可以

    不需要一直为0,只要出现过0一次,就ok

    1
    SHOW STATUS LIKE 'ONGOING_ANONYMOUS_TRANSACTION_COUNT';
  • step 6: 确保所有anonymous事务传递到slave上了

  • master

    1
    SHOW MASTER STATUS;
  • 每个slave

    1
    SELECT MASTER_POS_WAIT(file, position);

或者,等一段时间,只要不是大的延迟,一般都没问题

  • step 7: 每台Server上执行

    1
    SET @@GLOBAL.GTID_MODE = ON;
  • step 8: 在每台server上将my.cnf中添加好gtid配置

    1
    2
    3
    4
    gtid_mode=ON                (必选) 
    enforce-gtid-consistency (必选)
    log_bin=ON (可选)--高可用切换,最好设置ON
    log-slave-updates=ON (可选)--高可用切换,最好设置ON
  • step 9: change master

    1
    2
    3
    STOP SLAVE;  
    CHANGE MASTER TO MASTER_AUTO_POSITION = 1;
    START SLAVE;

GTID failover

MySQL crash
> 配置好loss-less semi-sync replication,可以更可靠的保证数据零丢失。
  以下说的都是crash 后,起不来的情况
  • binlog 在master还有日志没有传递到 slave
    1
    2
    3
    4
    5
    6
    7
    8
    1. 选取最新的slave, CHANGE MASTER TO maseter_auto_position 同步好  
    2. mysqlbinlog 将没传递过来的binlog在新master上replay
    3. 打开新 master 的 SET GLOBAL surper_read_only=off;
    ```
    * binlog 已经传递到slave
    ```bash
    1. 选取最新的slave, CHANGE MASTER TO maseter_auto_position同步好
    2. 打开新master的 SET GLOBAL surper_read_only=off;
OS crash
1
2
1. 选取最新的slave, CHANGE MASTER TO maseter_auto_position同步好  
2. 打开新master的 SET GLOBAL surper_read_only=off;

以上操作,在传统模式复制下,只能通过MHA来实现,MHA比较复杂。
现在,在GTID模式下,实现起来非常简单,且非常方便。

GTID 运维和错误处理

  1. 使用GTID后,对原来传统的运维有不同之处了,需要调整过来。
  2. 使用Row模式且复制配置正确的情况下,基本上很少发现有复制出错的情况。
  3. slave 设置 super_read_only=on
错误场景: Errant transaction

出现这种问题基本有两种情况

  1. 复制参数没有配置正确,当slave crash后,会出现重复键问题
  2. DBA操作不正确,不小心在slave上执行了事务

对于第一个重复键问题

  • 对于第一个重复键问题

    1
    2
    3
    * skip transation; 
    SQL> SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
    SQL> START SLAVE;
  • GTID模式

    1
    2
    3
    4
    SQL> SET GTID_NEXT='b9b4712a-df64-11e3-b391-60672090eb04:7';   --设置需要跳过的gtid event
    SQL> BEGIN;COMMIT;
    SQL> SET GTID_NEXT='AUTOMATIC';
    SQL> START SLAVE;

对于第二种不小心多执行了事务

这种情况就比较难了,这样已经导致了数据不一致,大多数情况,建议slave重做
如何避免: slave 设置 super_read_only=on;

重点: 当发生inject empty transction后,有可能会丢失事务

这里说下inject empty transction的隐患
    当slave上inject empty transction,说明有一个master的事务被忽略了(这里假设是 $uuid:100)
    事务丢失一:如果此时此刻master挂了,这个slave被选举为新master,那么其他的slave如果还没有执行到$uuid:100,就会丢失掉$uuid:100这个事务。
    事务丢失二:如果从备份中重新搭建一个slave,需要重新执行之前的所有事务,而此时,master挂了, 又回到了事务丢失一的场景。

QA

如何重置gtid_executed,gtid_purged。

  • 设置gtid_executed

    现在只能执行:

    1
    mysql> reset master;
  • 设置gtid_purged

  • 当gtid_executed 非空的时候,不能设置gtid_purged
  • 当gtid_executed 为空的时候(即刚刚备份好的镜像,刚搭建的mysql)
    1
    mysql> SET @@GLOBAL.GTID_PURGED='0ad6eae9-2d66-11e6-864f-ecf4bbf1f42c:1-3';
如果auto.cnf 被删掉了,对于GTID的复制会有什么影响?
> 如果被删掉,重启后,server-uuid 会变
手动设置 set @@gtid_purged = xx:yy, mysql会去主动修改binlog的头么
> 不会
GTID和复制过滤规则之间如何协同工作?MySQL,test还能愉快的过滤掉吗?
> 可以,改过滤的会自己过滤,不用担心

Automatic failover with mysqlfailover+GTID

mysqlfailover+GTID

-------------本文结束感谢您的阅读-------------

本文标题:MySQL5.7 GTID原理与实战

文章作者:Wang Jiemin

发布时间:2019年04月22日 - 14:04

最后更新:2019年04月22日 - 15:04

原始链接:https://jiemin.wang/2019/04/22/mysql-GTID/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%