暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL GTID介绍, 以及GTID在failover中的使用

ioSeeker 2021-10-18
808

前言

这篇关于GTID的文章是我在15年写下的学习笔记, 当时所在的云数据库团队需要新上MySQL 5.6-GTID的实例, 并且需要升级数据库管控系统的功能. 主要是将MySQL的failover和switchover流程从以往的文件位点(file&pos)的方式升级到GTID的方式.  当时调研的过程中便写下了这个笔记. 如今已经是18年,  我又萌生了写博的想法(大学的时候有写, 工作之后就完全荒废了), 所以重新整理发到博客上,

对于一主多从的MySQL复制拓扑来说, 使用file&pos的同步方式对failover的过程来说是非常不友好的, 因为当复制拓扑中的Master宕机并新选出某个Slave作为New Master后, 其他Slave如何与New Master建立主从关系是个比较头痛的问题, 主要是因为其他Slave需要找到一个合适的位点来连接New Master, 这是一个很费力的过程: 扫描Slave各自的relaylog并找到在New Master上对应的位点.

但是, 如果使用了GTID, 一切都变得简单了.

GTID介绍

概念

GTID (global transaction identifier)是已提交事务的唯一表示, 不单在它产生的originating server是唯一的, 在复制拓扑中的所有server范围内都是唯一的, 该特性当gtid_mode=ON时才生效.

GTID的格式:

source_id:transaction_id

其中:

  • source_id就是产生该事务所在server的server_uuid,

  • transaction_id与事务提交顺序有关, 取值从1开始, 之后在同一个originating server的提交的事务, 它的transaction_id逐一递增.

比如,在 UUID为 3e11fa47-71ca-11e1-9e33-c80aa9429562 的server上提交的第58个事务的 GTID是:

3e11fa47-71ca-11e1-9e33-c80aa9429562:58

GTID Set

若干系统函数, 系统变量的输出形式是GTID set.
GTID set的形式如下:

uuid:interval [, uuid:interval] 

其中interval是1或者1-5这样的区间.

比如GTID_SUBTRACT(set, subset)这个函数, 返回值是一个new set, 其中newset = set - subset. 像下面的例子:

select GTID_SUBTRACT('f76eb90f-82a2-11e5-a162-7ca23e9126c5:1-5,f76eb90f-82a2-11e5-a162-7ca23e9126c5:10-15', 'f76eb90f-82a2-11e5-a162-7ca23e9126c5:1-2')

返回结果:

f76eb90f-82a2-11e5-a162-7ca23e9126c5:3-5:10-15

GTID生命周期

我们可以在binlog里看到GTID, 并通过GTID分辨出事务来自哪个server. 另外, 如果事务已经在某个server上提交了,  后续的具有相同GTID的事务将会被忽略. 这样提供了一致性保证.

使用GTID后, slave不再需要master的上的文件名和偏移量(file&pos), 也即是 CHANGE MASTER TO语句不再需要指定MASTER_LOG_FILE 和 MASTER_LOG_POS参数.

  1. 事务在master上提交时, 被赋予一个GTID, GTID会立即写到binlog中,在事务本身的前面:

    alt
  2. binlog数据传输到slave上并存到relay log后, slave将读取GTID, 并设置到gtid_next这个变量里, 之后slave上提交的事务必须使用这个GTID. 也就是说, slave对这个事务不产生新的GTID.

  3. slave确认这个GTID没有被自己的binlog使用后, 开始执行事务. 事务提交时会将GTID写到binlog,  并将事务的其他event写到binlog中.  

系统变量GTID_NEXT

gtid_next是session变量, 默认值是AUTOMATIC, 表示生成新GTID.
slave线程从relaylog读取到GTID后, 会设置gtid_nex参数. 比如, 从relaylog读取到了4d8b564f-03f4-4975-856a-0e65c3105328:4711这个GTID, 它会执行下面这个语句:

    SET GTID_NEXT = 4d8b564f-03f4-4975-856a-0e65c3105328:4711;

mysqlbinlog也使用这样的机制, 当它读取到一个gitd-event, 便输出一个SET GTID_NEXT 语句.

当一个事务提交时, innodb会检查它是会否已经存在于系统变量GTID_EXECUTED中, 如果存在的话, 这个事务将会被忽略.

系统变量GTID_EXECUTED

变量GTID_EXECUTED中, 存有所有写到binlog中的所有gitd set, 它的值和SHOW MASTER STATUS 或者SHOW SLAVE STATUS中的Executed_Gtid_Set
字段是一样的.
比如server上应用了下事务:

    0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:1
   0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:2
   4D8B564F-03F4-4975-856A-0E65C3105328:1
   0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:3
   4D8B564F-03F4-4975-856A-0E65C3105328:2

那么, GTID_EXECUTED的值就是:

    "0EB3E4DB-4C31-42E6-9F55-EEBBD608511C:1-3,
   4D8B564F-03F4-4975-856A-0E65C3105328:1-2"

对于slave, 如果io线程收到到数据, 存放在relaylog中,  可以通过show slave status查看Retrieved_Gtid_Set字段, 了解slave已经收到的事务.

复制协议

在5.6之前的主从复制, 当slave连接master时, 需要指定master的binlog位点(file&pos), 然后master发送这个点之后的所有数据.

现在新的协议是:

  1. 当slave连接master时, 发送它已经执行过的事务gitd区间.

  2. master发送其他的事务给slave, 也就是slave还没有执行的事务.

alt

相关函数

  • GTID_SUBSET()
    Return true if all GTIDs in subset are also in set; otherwise false.

  • GTID_SUBTRACT()
    Return all GTIDs in set that are not in subset.

  • WAIT_FOR_EXECUTED_GTID_SET()
       Wait until the given GTIDs have executed on slave.

  • WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS()
       Wait until the given GTIDs have executed on slave.

搭建主从

master上一定要开启binlog, 以及配置serverid

[mysqld]
log-bin=mysql-bin
server-id=1

启动master, 注意打开gtid_mode:

mysqld_safe --gtid_mode=ON --log-bin --log-slave-updates --enforce-gtid-consistency&

启动slave, 注意加上skip-slave-start 选项:

mysqld_safe --gtid_mode=ON --log-bin --log-slave-updates --enforce-gtid-consistency --skip-slave-start 

在slave执行:

CHANGE MASTER TO  MASTER_HOST = host, MASTER_PORT = port, MASTER_USER = user, MASTER_PASSWORD = password, MASTER_AUTO_POSITION = 1;
START SLAVE;

这样简单的一主一从就搭建好了.

具体操作:
http://dev.mysql.com/doc/refman/5.6/en/replication-gtids-howto.html

GTID在failover中的使用

场景1: newest slave当作new master

考虑下图的一主两从星型结构, A当作master, B,C都是它的slave:

alt

假设现在A宕机了, 我们需要做切换操作, 从B和C中找一个server来当作新的master, 并把其他的server当作它的slave:

alt

由于主从复制是异步的, B和C从原来的master中收到的、应用的事务个数都不一样, 可能其中一个数据比较新. 假设B的数据比C要新, 我们将B当作新的master. 那么我们需要将C当作B的slave, 并从C还没应用的事务开始主从复制.
假设A提交了3个事务,  B复制了这3个事务,  但是C只复制了第一个事务:

alt

当A宕机后, 我们用B当作新的master, 把C当作B的slave. 在新的 replication协议下, C将会发送id1给B, 之后B将会把C没有执行的事务, 也就是(id2,tr2) , (id3, tr3) 发送给C, 之后在B上新提交的事务, 也会源源不断的发送给C.

场景2: 任意指定某 slave当作new master

有些时候, newest slave并不是理想的 new master, 可能是它的硬件配置不高, 网络不好. 我们一开始就预先选定了某硬件很强劲的slave当作备选master.

在下图中, 一开始A是master, BCD都是slave. 而且我们指定B当作备选master:

alt

假设这时候A宕机了, 这个时候需要作切换, 因为D现在是newest, 就简单的做法就是将D当作新的master, 让其他server当作slave. 但是我们现在要把B当作新的master, 在此之前, 我们要更新B的数据, 做法是让B当作D的slave, 当B追上D时, 再切换B为master.

但是上图仅仅是现实情况的简化版, 真实情况是,  slave可能已经将数据写到relaylog 但是还没有应用这些事务, 如下图:

alt

这个情景中, BCD分别收到了1, 2, 3个事务, 但是B一个事务都还没应用, C应用了2个, D应用了1个.

我们的目标是把B当作新的master, 再这之前需要更新他的数据.

第一种做法是, 让B依次连接C和D, 连接后, 等待B执行完C或者D中relaylog和binlog中的事务. 这样一轮下来, B的数据已经是newest, 已经可以切换成为新的master.

第二种方法, 是找到most slave, 也就是relay log数据最多的slave, 在这个例子中是D, 等待D的sql线程执行完relay log后, 再让B连接D, 等B的数据达到newest后, 切换成为新的master.

前面说过, slave收到的事务可以在SHOW SLAVE STATUS中的Retrieved_GTID_Set看到,  已经应用的事务可以在变量gtid_executed看到.

上述第一种做法的操作, 可以下面的伪代码实现, 其中candidate是备选master:

for each slave in slaves_of_the_master:
   if slave != candidate:
     candidate.query(STOP SLAVE)
     candidate.query(CHANGE MASTER TO
                       MASTER_HOST = slave.host,
                       MASTER_PORT = slave.port)
     candidate.query(START SLAVE)
     slave_executed_gtids :=
         slave.query(SELECT @@GLOBAL.GTID_EXECUTED)
     slave_queued_gtids :=
         slave.query(SHOW SLAVE STATUS)
                    [column = Retrieved_GTID_Set]
     slave_all_gtids :=
         slave_executed_gtids + ',' + slave_queued_gtids
     candidate.query(
         SELECT WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(
           slave_all_gtids))

云数据库行业内failover的实现方式稍微更复杂一些, 因为我们需要在RPO和RTO之间做平衡, 首要是保证RPO(也就是尽量保证不丢数据), 但是也要兼顾RTO(不能因为failover中由于额外某些slave的故障导致failover时间太长)

使用GTID的一些限制

因为GTID是基于事务的, 当使用GTID时, 一些mysql的特性将会收到限制.

  • 更新涉及到非事务引擎: 当在一个trx中, 既更新基于事务引擎的表, 又更新了非事务引擎的表, 这种情况会导致这个trx产生多个GTID. 这就破坏了GTID和事务之间一对一的关系, 进而导致基于GTID的主从复制不能正确工作. 其实现在已经很少人这么用, 我们团队5.6数据库已经默认把用户的MyISAM转换成Innodb.

  • CREATE TABLE … SELECT语句: 当使用row-based格式的binlog, 这个语句会产生两个独立的event: 一个新建table, 另外一个插入数据. 如果在trx中执行该语句, 这两个event可能会产生相同的GTID, 也就意味着插入数据的那个evnet会被跳过. 其实令一条SQL语句产生两个GTID即可避免这个问题,  对于这一点,  我提了个patch给官方:#82919 .

  • 临时表: 当使用GTID时, 不支持CREATE TEMPORARY TABLE和 DROP TEMPORARY TABLE 这两个语句, 但是如果在事务外, 而且autocommit=1时, 可以执行这两个语句. 也即是事务内才会有问题, 事务外是ok的.

通过--enforce-gtid-consistency选项, 可以阻止上述语句的执行. 开启该选项后, 相关的语句执行会导致错误.

create table test2 select * from  test1;
ERROR 1786 (HY000): CREATE TABLE ... SELECT is forbidden when @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1.

参考资料

  1. replication-gtids-failover

  2. flexible-fail-over-policies-using-mysql


文章转载自ioSeeker,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论