目录
一、GTID概念
MySQL从5.6版本开始引入了全局事务标识符(Global Transaction Identifiers, GTID)的概念,为每个事务都分配一个唯一的标识符。GTID以事务为单位管理复制,不再需要靠log_file和log_pos来定位复制位置,在主从切换、故障恢复时更加简单。
1.1 GTID的格式
全局事务标识符由2部分组成,服务器的UUID和一个数字编号,格式为:
GTID = source_id:transaction_id
source_id是发起事务服务器的UUID,用来保证"全局"唯一,transaction_id是事务的编号,按照事务提交的顺序从1开始单调递增。这保证的事务在整个复制拓扑中的是唯一的。
GTID复制拓扑中,无论事务被复制了多少次,事务的GTID都保持不变。一旦某个事务在服务器上提交后,后续所有相同GTID的事务都会被忽略,这种机制可以有效避免重复复制的现象,保持数据一致。
1.2 GTID集
GTID也可以用集合的形式来表示,例如:
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
代表了server_uuid为3E11FA47-71CA-11E1-9E33-C80AA9429562为编号为1至5的事务集合。
如果有多个范围要表示,则事务编号的范围可以用冒号分隔:
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-3:11:47-49
GTID集甚至可以表示来自不同数据源的事务集,下面的GTID表示来自2个服务器的事务集合:
2174B383-5441-11E8-B90A-C80AA9429562:1-3, 24DA167-0C0C-11E8-8442-00059A3C7B00:1-19
1.3 mysql.gtid_executed表
mysql.gtid_executed表记录了是服务器上已经执行事务的GTID。表的结构如下:
desc mysql.gtid_executed;
三个字段意义分别是:
- 发起事务的服务器UUID
- GTID集的起始事务ID
- GTID集的结束事务ID,对于单个GTID,结束ID和起始ID相同。
mysql.gtid_executed表供MySQL服务器内部使用:
- 当从库禁用二进制日志时用该表记录GTID,或者当二进制日志丢失时,可从该表查询GTID状态。
- 当从库启用了二进制日志(log_slave_update),则当刷新二进制日志或重启服务器时,服务器都会将当前二进制日志中所有事务的GTID写入mysql.gtid_executed表。
因此当启用二进制日志记录时,mysql.gtid_executed表并不保存最新已执行事务GTID,最新状态可以由gtid_executed全局系统变量(@@global.gtid_executed)提供,该变量在每次提交事务后更新。为了节省空间,MySQL服务器会定期压缩mysql.gtid_executed表,方法是将多行表示为单行GTID集。
1.4 GTID的生命周期
一个典型的GTID生命周期过程如下:
- 在主库提交一个事务,主库将UUID和最小的非0序列号组成GTID分配给此事务,并将GTID和事务本身写入二进制日志(gtid_log_event),这是一个原子操作保证GTID和事务内容同时写入日志。
- 事务提交后很短的时间内,将GTID写入@@global.gtid_executed状态变量。此变量包含了所有已执行的事务,代表了主库的当前状态。
- 复制将二进制日志数据传输到从库并存储在从库的中继日志中,从库读取GTID并将其设置为gtid_next系统变量的值。表示从库下一个即将执行的事务。
- 从库在执行事务前会先进行检查,保证此GTID没有被执行,且当前的没有其他会话读取此GTID。如果有多个会话读取了此GTID,只有1个可以执行,其他的都会阻塞。从库的gtid_owned系统变量@@global.gtid_owned显示当前正在使用的GTID以及拥有它的线程ID。如果已经使用了该GTID,通过自动跳过功能忽略该事务,并且不会引发错误。
- 如果此GITD未执行过,从库应用此事务,并使用主库生成的GTID,不会重新分配。
- 如果从库开启了二进制日志(log_slave_updates)。则GTID和主库一样通过gtid_log_event原子事件写入自身的二进制日志。只有当轮换日志或关闭服务器时,MySQL才将binarylog中的GTID写入mysql.executed表(开启二进制日志时mysql.gtid_executed不代表服务器的最新状态)。
- 如果从库禁用了二进制日志,MySQL会在事务后附加一个原子插入操作,将GTID直接写入mysql.gtid_executed表(关闭二进制日志时mysql.gtid_executed代表服务器的最新状态)。
- 从库提交后很短的时间内,将GTID写入从库@@global.gtid_executed状态变量。
上述第8步,不管从库是否开启二进制日志,此变量都包含了所有已执行的事务,代表了从库的当前状态。因此建议始终查询@@global.gtid_executed来查看服务器的状态。主库上过滤掉的事务不会分配GTID,它们不会添加到gtid_executed系统变量中,也不会添加到mysql.gtid_executed表中。
但被从库过滤掉的GTID是会持久化记录的:
- 从库开启了二进制日志,过滤掉的事务作为gtid_log_event写入二进制日志,后跟一个begin和commit的空事务(事务内容被过滤掉了,但GTID保留下来)。
- 从库未开启二进制日志,则仅将GTID写入mysql.gtid_executed表。
从库为过滤掉事务保留GTID,可以保证再次连接主库时,不会再次检索被过滤掉的的事务。
二、配置GTID复制
我们以之前配置好的异步复制为基础来配置GTID复制来演示GTID配置的过程:
MySQL复制(一):异步复制(Asynchronous replication)_V1ncent Chen的博客-优快云博客
2.1 复制相关参数
GTID模式复制最主要涉及2个参数,enforce_gtid_consistency和gtid_mode(其余参数在下面解释):
enforce_gtid_consistency
此参数设置为true,服务器仅允许GTID安全的语句执行,防止某些不安全的语句导致GTID复制失败。在启用GTID模式复制前,必须设置为true。当设置为true以后,以下操作将不再可用:
- create table … select语句(MySQL8.0.21后,如果引擎支持原子DDL,可以使用此语句)。
- 事务内的create temporary table和drop temporary table。
- 同时更新事务表和非事务表的事务或语句。
enforce_gtid_consistency验证语句的GTID兼容性是在写二进制日志的时候,如果在服务器上禁用了二进制日志或过滤器删除了语句而未写入二进制日志,都不会检查。
gtid_mode
控制开启GTID模式日志及日志可以包含事务的类型。事务可以是匿名的或基于GTID的,匿名事务即是基于文件名和位置来进行定位的事务。GTID事务可以通过GTID来定位事务。gtid_mode可以设置为下列值:
off:新增和复制的事务都必须是匿名事务。
off_permissive:新增的事务是匿名的,复制的事务可以是匿名事务或GTID事务。
on_permissive:新增的事务是GTID事务,复制的事务可以是匿名事务或GTID事务。
on:新增和复制的事务都必须是GTID事务。
在未关闭MySQL实例的情况下,这些值只能按顺序一次更新一步,例如,将gtid_mode的值从off修改为on,必须通过off_permissive->on_permissive->on这3步来完成变更,不可以直接修改为on。如果是重启则可以直接设置为on。
2.2 在线切换GTID复制
假设主从复制正在运行,我们可以在线切换模式至GTID复制,此方法不需要重启MySQL实例,非常适合生产环境使用,具体步骤如下:
1.确定GTID模式复制的兼容性,在主从上分别执行:
set global enforce_gtid_consistency=warn;
以正常的工作负载运行一段时间,如果日志中有告警产生,说明应用和GTID复制不兼容,需要调整应用直至不再产生告警。满足条件后,再进行下面的步骤。
2.在主库和从库上启用GTID强一致性检查,防止GTID不兼容的语句导致复制失败:
set global enforce_gtid_consistency=true;
3.修改主库和从库的gtid_mode为off_permissive:
set global gtid_mode=off_permissive;
4.修改主库和从库的gtid_mode为on_permissive,所有新事务都是GTID事务:
set global gtid_mode=on_permissive;
5.在主库和从库上等待所有匿名事务复制完成,通过观察ongoing_anonymous_transaction_count变量归零,注意主库和从库都要归零:
show status like 'ongoing_anonymous_transaction_count';
6.在主库和从库启用GTID模式:
set global gtid_mode=on;
建议同时将enforce_gtid_consistency=true和gtid-mode=on添加到主从配置文件中,防止丢失。
7.重启复制线程,切换到GTID模式:
stop slave;
change master to master_auto_position=1;
start slave;
至此,GTID模式的复制就配置完成了。
注意:GTID事务的二进制日志无法和匿名事务的备份配合使用,反之亦然。如果二进制日志有其他用途(例如时间点恢复)则在第5步所有匿名日志复制完成后备份数据库和二进制日志(备份时使用flush logs切换日志),同样切换为GTID事务后也立刻进行一次备份。
三、GTID相关系统变量
下面是监控和调整GTID复制的相关变量解释:
enforce_gtid_consistency
控制SQL语句是否仅允许违反GTID一致性,可以选择的值有off、warn、on:
off:允许语句违反GTID一致性。
warn:允许违反GTID一致性,但是会生成告警信息。
on:不允许语句违反GTID一致性,如果使用GTID复制,必须设置为on。
gtid_mode
控制是否开启GTID模式的日志记录及日志内可以包含的事务类型。
off:新增和复制的事务都必须是匿名事务。
off_permissive:新增的事务是匿名的,复制的事务可以是匿名事务或GTID事务。
on_permissive:新增的事务是GTID事务,复制的事务可以是匿名事务或GTID事务。
on:新增和复制的事务都必须是GTID事务。
gtid_executed
全局变量(@@global.gtid_executed),包含服务器执行的所有事务的GTID集和gtid_purged被设置的变量,和show master status; 语句输出executed_gtid_set列的值相同。当服务器启动时,会根据binlog_gtid_simple_reocovery参数选择不同的方法初始化其值。
show global variables like 'gtid_executed';
show master status;
某些旧的MySQL版本此变量可以用在会话级别,但目前会话级的gtid_executed已经废弃。
gtid_next
gtid_next是会话级变量,代表下一个事务即将分配的GTID。当事务在主库提交时,系统会自动分配一个新的GTID,当事务复制到从库时,会保留主库分配的GTID,而不是由从库分配新的GTID。GTID可以设置为下列值:
automatic:系统为事务自动生成GTID
anonymous:事务没有GTID(匿名事务),只能通过文件名和偏移量定位
UUID:NUMBER:明确设定一个有效的GTID,下一个事务将分配这个GTID。
此参数的功能还受到gtid_mode参数的影响,如果gtid_mode设置为off(只有匿名事务),那么此参数无效。
如果用UUID:NUMBER手动为事务分配了GTID,无论事务提交或回滚,在开启下一个事务前,必须再次手动设置gtid_next的值或设置为automatic。
gtid_purged
gtid_purged是全局变量,代表所有在服务器上已提交,但是不在binarylog中的事务的GTID集。gtid_purged是gtid_exeucted的子集。
gtid_purged中的GTID包含下列类别:
- 从库禁用binarylog,则提交后的事务GTID也会加入gtid_purged。
- 从库启用binarylog,事务也写入二进制日志,但是日志文件被purged(删除)掉了。
- 通过set @@global.gtid_purged 语句显示加入到集合中。
gtid_purged和gtid_executed一样,会在服务器启动或重启时根据binlog_gtid_simple_reocovery参数选择不同的方法初始化其值。
如果要求过滤某些事务,可以手动设置gtid_purged值,这些值会被加入gtid_executed,即使这些事务从未执行。gtid_purged设置方式有2种:替换和追加
SET @@global.gtid_purged = 'gtid_set' -- 替换
SET @@global.gtid_purged = '+gtid_set' -- 追加
binlog_gtid_simple_reocovery
控制MySQL启动或重启时如何检索二进制日志来计算gtid_executed和gtid_purged变量的初始值。当binlog_gtid_simple_recovery=true时,只读取最旧和最新的binlog文件,否则遍历所有binlog文件
gtid_owned
全局或会话级只读变量,通常只内部使用:
全局级:包含服务器正在使用的所有GTID列表和对应的Thread ID。
会话级:包含会话当前正在使用的单个GTID。
gtid_executed_compression_period
控制压缩mysql.gtid_executed表的事务阈值数量。
二进制日志未开启时,执行到指定数量的事务时,对mysql.gtid_executed进行压缩。
二进制日志开启时,此参数无效,只有在二进制日志轮换时才进行压缩。
四、GTID常规维护任务
当GTID复制建立起来后,后续还需要一些常规的监控和维护任务:
4.1 监控GTID复制运行状态
复制建立后,我们要检查复制是否正常运行。你可以通过查看复制的心跳、从库状态、主库上相关信息来确定复制是否正常运行。
4.1.1 查看复制心跳
复制建立后,主库会定期向从库发送心跳信号(即使没有发送事务),心跳可以在建立主从复制时,通过change master to的master_heartbeat_period子句指定。如果未指定则默认是从库连接超时的一半:
show variables like 'slave_net_timeout'; -- 值是60,则心跳间隔是30
最近一次的心跳时间可以通过performance_schema.replication_connection_status表中的last_heartbeat_timestamp查看:
select * from performance_schema.replication_connection_status;
4.1.2 查看单一从库状态
可以用show slave status来查看单一从库的详细运行状态:
show slave status \G;
其中比较重要的状态有:
Slave_IO_State:从库当前的状态。
Slave_IO_Running:从库的I/O线程是否运行,通常情况为Yes.
Slave_SOL_Running:从库的SQL线程是否运行,通常情况为Yes.
Last_IO_Error,Last_SQL_Error:最近I/O线程和SQL线程的错误,通常情况下应该为空,代表复制正常运行。
Seconds_Behind_Master:从库落后主库的秒数。
4.1.3 查看主库binlog dump线程
在主库上通过show processlist可以查看binlog dump状态:
show processlist \G;
主库上binlog dump线程和从库的I/O线程连接,负责发送binlog到从库。
4.1.4 查看主库有多少从库
从主库通过show slave hosts (8.0.22后用 show replicas;)查看从库的基本信息:
show slave hosts;
4.2 跳过错误的事务
当复制遇到问题失败时会停止复制,你可以选择跳过这个事务。
使用GTID复制时,基于日志和位置的传统跳过事务的方式(sql_slave_skip_counter)不可用,必须以事务为单位进行跳过。
可以用注入一个空事务的方式来跳过错误,主要步骤如下:
1.停止复制,获取最后出错的事务的GTID,通过performance_schema.replication_applier_status_by_worker表的applying_transaction字段来获取。
stop slave;
select * from performance_schema.replication_applier_status_by_worker \G;
这里我提前在备库手动删除了test2表,模拟复制报错。
2.将gtid_next设置为上一步获取的值,注意gtid_next只能是单个GTID值
set gtid_next='7824aa06-333a-11ed-8494-0800276c19d8:23';
3.注入空事务,并将gtid_next恢复为自动模式后重启复制,发现错误的事务已经被跳过了,复制正常运行。
begin;
commit;
set gtid_next='automatic';
start slave;
show slave status \G;