记一次 MySQL 主从同步异常的排查记录,百转千回!
relay log 损坏了,导致从库的 SQL 线程解析 relay log 时出现异常。从库恢复方式是通过手动设置当时出错的 GTID 的下一个值,让从库不从主库同步这个 GTID,最后从库就能正常同步这个 GTID 之后的 binlog 了,后续 SQL 线程也能正常解析 relay log 了。
你好,我是悟空。
本文主要内容如下:
目录
一、现象
最近项目的测试环境遇到一个主备同步的问题:
备库的同步线程停止了,无法同步主库的数据更改。
备库报错如下:
从库同步报错信息
完整的错误信息:
复制
Relay log read failure: Could not parse relay log event entry. The possible reasons are: the master's binary log is corrupted (you can check this by running 'mysqlbinlog' on the binary log), the slave's relay log is corrupted (you can check this by running 'mysqlbinlog' on the relay log), a network problem, or a bug in the master's or slave's MySQL code. If you want to check the master's binary log or slave's relay log, you will be able to know their names by issuing 'SHOW SLAVE STATUS' on this slave.
- 1.
上面的报错信息是什么意思呢?
翻译一下就是主库的 binlog 或者从库的 relay log 损坏了,造成这个问题的原因:
- 可能是网络问题。
- 也可能是主库或备库的代码 bug。
首先我们还是得复习下主从同步的原理才能更好地分析原因。
二、主从同步的原理
主从同步的原理
- 从库会生成两个线程,一个 I/O 线程,名字叫做 Slave_IO_Running,另外一个是 SQL 线程,名字叫做 Slave_SQL_Running;
- 从库的 I/O 线程会去请求主库的 binlog 日志文件,并将得到的 binlog 日志文件写到本地的 relay log (中继日志)文件中;
- 主库会生成一个 dump 线程,用来给从库 I/O 线程传 binlog;
- 从库 SQL 线程,会读取 relay log 文件中的日志,并解析成 SQL 语句逐一执行。
三、排查思路
3.1 分析从库的同步状态
我们可以打印下从库的同步状态,看到如下几个关键信息:
分析从库的同步状态
Master_Log_File: mysql-bin.000956,代表从库读到的主库的 binlog file,
Read_Master_Log_Pos: 528071913,代表从库读到的主库的 binlog file 的日志偏移量
Relay_Log_File: relay-bin.000094,代表从库执行到了哪一个 relay log
Relay_Log_Pos: 123408769,代表从库执行的 relay log file 的日志偏移量
Relay_Master_Log_File: mysql-bin.000955,代表从库已经重放到了主库的哪个 binlog file。
Exec_Master_Log_Pos: 123408556,代表从库已经重放到了主库 binlog file 的偏移量。
Slave_IO_Running: Yes,说明 I/O 线程正在运行,可以正常获取 binlog 并生成 relay log。
Slave_SQL_Running: No,说明 SQL 线程已经停止运行,不能正常解析 relay log,也就不能执行主库上已经执行的命令。
Master_Log_File 和 Read_Master_Log_Pos 这两个参数合起来表示的是读到的主库的最新位点。
Relay_Master_Log_File 和 Exec_Master_Log_Pos,这两个参数合起来表示的是从库执行的最新位点。
如果红色框起来的两个参数:Master_Log_File 和 Relay_Master_Log_File 相等,则说明从库读到的最新文件和主库上生成的文件相同,这里前者是 mysql-bin.000956,后者是 mysql-bin.000955,说明两者不相同,存在主从不同步。
如果蓝色框起来的两个参数 Read_Master_Log_Pos 和 Exec_Master_Log_Pos 相等,则说明从库读到的日志文件的位置和从库上执行日志文件的位置相同,这里不相等,说明主从不同步。
当上面两组参数都相等时,则说明主从同步正常,且没有延迟。只要有任意一组不相等,则说明主从不同步,可能是从库停止同步了,或者从库存在同步延迟。由于上面的 SQL 线程已经停止了,说明是从库同步出现问题了。
从库同步出现的问题在最开始的报错信息里面已经提到了,可能是网络问题导致,还有可能是 binlog 或 relay log 损坏。
3.2 重启万能大法
先通过重启来恢复从库的 SQL 线程试试看?重启方式就是两种:
- 方式一:从库重新开启同步。就是执行 stop slave; 和 start slave; 命令。
- 方式二:重启从库实例。就是重启 mysql 实例或 mysql 容器。
这两种方式试了后,都不能恢复从库的 SQL 线程。
3.3 查看 binlog
再来看下 binlog 是否有损坏,在主库上通过这个命令打开 mysql-bin.000955 文件。
复制
mysqlbinlog /var/lib/mysql/log/mysql-bin.000955
- 1.
没有报错信息,如下图所示:
binlog 日志
3.4 查看 relay log
看到从库同步的 Relay_Log_File 到 relay-bin.00094 就停止同步了,如下图所示,可能是这个文件损坏了。
relay log 日志
在从库上通过 mysqlbinlog 命令打开这个文件
复制
mysqlbinlog /var/lib/mysql/log/relay-bin.000094
- 1.
可以看到有个报错信息:
复制
ERROR: Error in Log_event::read_log_event(): 'read error', data_len: 7644, event_type: 31
ERROR: Could not read entry at offset 243899899: Error in log format or read error.
- 1.
- 2.
这段文字翻译过来就是读取错误,数据长度 7644,在读取偏移量为 243899899 的日志时发生了错误,可能是日志文件格式错误或是读取文件错误。
relay log 报错信息
3.5 找原因
3.5.1 猜测事务日志太大
根据这个报错信息可以知道这个事务日志数据太长了,data_len: 7644,而导致读取错误。
而且上面还有很多 Update_rows 的操作。
猜测:会不会是主库执行了一个大事务,造成该事务生成的一条 binlog 日志太大了,从库生成的对应的一条 relay log 日志也很大, SQL 线程去解析这条 relay log 日志解析报错。
3.5.2 验证
到主库上查看下 binlog 日志里面有没有在那个时间点做特殊操作。
感觉快找到原因了。执行以下命令来查看
复制
mysqlbinlog File --stop-datetime=T --start-datetime=T
- 1.
stop-datetime 指定为读取 relay log 报错的时刻 2023-04-04 16:47:16,
start-datetime 指定为读取 relay log 报错的时刻 2023-04-04 16:47:30。
发现并没有找到 Update_rows 的操作。继续把时间往后加一点,经过多次尝试,把时间锁定在了 2023-04-04 17:00:30~17:00:31。这 1s 内能找到 2023-04-04 16:47:16 的操作日志。
日志如下,这个命令会打印 N 多日志,直接把屏幕打满了!!
难道真的 binlog 对应的这条事务日志太大了吗???
存疑: 2023-04-04 16:47:16 时刻对数据库中的表做了某个大事务的操作,造成该事务对应的这条 binlog 日志很大很大。生成的 relay log 也很大,SQL 线程解析 relay log 报错。
3.6 这是真相吗?
问了下熟悉这张表的同事,有没有在这个时刻做什么大事务操作。
同事看了下代码,发现有个批量插入的操作,一次执行 400 条,难道是 400 条太多了???这不应该是真正的原因,400 条也不多。
不经意间问了下这张表的数据量有多大,该同事在 4月4号 16:45:25 做了一个手动备份 xx_dance 表的操作,这张表有 25 万条数据。
备份表 xx_dance_0404 的信息
这个备份操作是在一个事务里面执行的,生成的一条 binlog 日志很大。
这里只是一个猜测,还未得到验证,文末会说明真正的原因。
如果真的是这样,那我可以先恢复从库的同步,备份表的操作在从库上其实不需要。
3.7 GTID
不知道细心的你是否有发现上面的 binlog 里面有一个GTID,
复制
'c5d74746-d7ec-11ec-bf8f-0242ac110002:8634832
- 1.
binlog 中 GTID
记住 GTID 中的数字 8634832,后面恢复从库同步时要用到。
我们再来看下从库的状态,发现也有一个 GTID,如下图所示,值为 8634831,正好相差 1,感觉这两个 GTID 值之间有不可告人的秘密。
从库的状态,GTID 集合
那么从库 SQL 线程停止运行的原因就是卡在 8634832 这里了,我们可否跳过这个 GTID 呢?这里就需要了解 GTID 的原理了。
你可能对 GTID 的原理很感兴趣,可以查看之前悟空写的一篇文章:
MySQL 主从模式采用 GTID 的实践
四、GTID 同步方式的原理
这里还是把主从同步采用 GTID 方式的流程拿出来看下,帮助大家快速回顾下,熟悉的同学可以跳过本节内容。
GTID 方案:主库计算主库 GTID 集合和从库 GTID 的集合的差集,然后主库推送差集 binlog 给从库。
当从库设置完同步参数后,假定主库 A 的GTID 集合记为集合 x,从库 B 的 GTID 集合记为 y。
从库同步的逻辑如下:
GTID 同步方式的原理
- 从库 B 指定主库 A,基于主备协议简历连接。
- 从库 B 把集合 y 发给主库 A。
- 主库 A 计算出集合 x 和集合 y 的差集,也就是集合 x 中存在,集合 y 中不存在的 GTID 集合。比如集合 x 是 1~100,集合 y 是 1~90,那么这个差集就是 91~100。这里会判断集合 x 是不是包含有集合 y 的所有 GTID,如果不是则说明主库 A 删除了从库 B 需要的 binlog,主库 A 直接返回错误。
- 主库 A 从自己的 binlog 文件里面,找到第一个不在集合 y 中的事务 GTID,也就是找到了 91。
- 主库 A 从 GTID = 91 的事务开始,往后读 binlog 文件,按顺序取 binlog,然后发给 B。
- 从库 B 的 I/O 线程读取 binlog 文件生成 relay log,SQL 线程解析 relay log,然后执行 SQL 语句。
GTID 同步方案和位点同步的方案区别是:
- 位点同步方案是通过人工在从库上指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
- 而 GTID 方案是通过主库来自动计算位点的,不需要人工去设置位点,对运维人员友好。
五、恢复从库的同步
5.1 查看从库执行 GTID 的进度
在从库上执行 show slave status \G来查看 GTID 集合。
Retrieved_Gtid_Set 表示从库收到的所有日志的 GTID 集合。
Executed_Gtid_Set 表示从库已经执行完成的 GTID 集合。
如果 Executed_Gtid_Set 集合是包含 Retrieved_Gtid_Set,则表示从库接收到的日志已经同步完成。
这里 Executed_Gtid_Set 的集合为 1-8634831,而 Retrieved_Gtid_Set 为 1-9101426,说明从库有些 GTID 是没有执行的。从库已经执行到了 8634831,下一个要执行的 GTID 为 8634832。
因为我们采用的同步方式是 GTID 方式,所以只要让从库跳过这个 GTID ,从下一个 GTID 开始同步就行。
带来的问题就是这个 GTID 对应的事务没有执行。因为报错的操作是从库备份一张大表,所以从库跳过这个备份操作也是可以接受的。
5.2 手动设置 GTID
来,手动设置一把 GTID 试下。
5.2.1 重置从库进度
首先重置下从库同步的进度 reset slave,这条命令会把所有的 relog 给清理掉,重新启用一个新的 relay log文件。
复制
stop slave;
reset slave;
- 1.
- 2.
重新开启同步后,主库会计算主库 GTID 集合和从库 GTID 的集合的差集,然后主库推送差集 binlog 给从库。
5.2.1 设置 GTID 为一个值
执行以下命令设置 GTID 为下一个值。
复制
set gtid_next='c5d74746-d7ec-11ec-bf8f-0242ac110002:8634832';
begin;
commit;
set gtid_next=automatic;
start slave;
- 1.
- 2.
- 3.
- 4.
- 5.
gtid_next 表示设置下一个 GTID = 8634832,这个值是在原来的 8634831 加 1。后面的 begin 和 commit 是提交了一个空事务,把这个 GTID 加到从库的 GTID 集合中。那么从库的 GTID 集合就变成了
复制
'c5d74746-d7ec-11ec-bf8f-0242ac110002:1-8634832';
- 1.
5.2.2 查看当前 GTID 集合
我们可以通过 show master status\G 命令来查看从库的 GTID 集合。下方截图是执行上述命令之前的。GTID集合为 1-8634831。另外 GTID 集合 为 1 和 GTID 集合为 1-4 的可以忽略,因为它们前面的 Master_UUID 不是当前主库的 uuid。
show master status\G 的结果
也可以通过 show slave status\G 命令来查看 GTID 集合,结果也是一样的。
5.3 开启从库同步
再次启动从库的同步(start slave 命令),I/O 线程和 SQL 线程的状态都为 YES,说明启动成功了。
而且查看从库的同步状态时,观察到从库的同步是存在延迟的。通过观察这个字段 Seconds_Behind_Master 在不断减小,说明主从同步的延迟越来越小了。
两个线程都是正常运行,主从同步延迟越来越小
过一段时间后,执行的 GTID 等于收到的 GTID 集合,Seconds_Behind_Master = 0,说明主从完全同步了。
六、原因
上面的推测:备份大表造成 binlog 的一条日志太大,relay log 也跟着变大,SQL 线程无法正常解析。
但这是真相吗?
虽然从库重新开启了同步,且跳过了这条日志,但带来的是从库上就不会出现这个备用表 xx_dance_0404 。
但出现了两个奇怪的问题:
问题 1:从库开启同步后,居然出现了这个备份表 xx_dance_0404。不是跳过这个备份操作了吗?目前没想到原因。
问题 2:为了重现这个问题,我到主库上做了一个备份表的操作,表名为 xx_dance_0412,从库也同步了这个新的备份表 xx_dance_0412。而且 binlog 出现的日志现象也是一样的,对应的这条 binlog 日志也很大,但是从库同步正常。我又备份了一张 300 万的大表,依然没重现。
通过问题 2 可以说明上面的推测是错误的,备份大表并不会影响主从同步。
那么 relay log 报错的原因是什么?
只有一个原因了,relay log 文件真的是损坏的,从库的状态上也说明了原因,relay log is corrupted(损坏)。SQL 线程去解析 relay log 时报错了,导致 SQL 线程停止,从库不能正常执行同步。
小结:relay log 损坏了,导致从库的 SQL 线程解析 relay log 时出现异常。从库恢复方式是通过手动设置当时出错的 GTID 的下一个值,让从库不从主库同步这个 GTID,最后从库就能正常同步这个 GTID 之后的 binlog 了,后续 SQL 线程也能正常解析 relay log 了。
relay log 损坏了 删除了重新传不就可以了
一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。详情看这篇:
在这一个月遇到了很多坑:
-
因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。--可以设置主键每次加2
-
同步失败后,Slave_SQL_Thread 线程就停了,除非解决了同步的错误,才能继续进行同步。
-
同步失败的错误,不会只有一条记录有问题,往往是一大片的同步问题。
-
两个节点互相缺少对方的数据。
-
主从的同步延迟,切换到新主库后,数据不是最新。
-
当出现不一致时,无法确定以哪个库为准。
造成上面问题的主要原因就是因为两个节点都支持写入 + 双主可以随时切换。
解决这种问题的方案有 改进自增主键的步长(影响未评估),使用 GTID 方案(未验证)。即使这样,双主同步的风险还是有,而且不同步后,如何处理是个大难题。
那么回到我们最初的想法:为什么会选择双主?
最开始的目的就是为了高可用。双主就是说有一台 MySQL 节点挂了,另外一台能够顶上,对于用户来说是无感的,给运维人员一定的缓冲时间来排查 MySQL 故障。另外老的主节点恢复后,不用改配置就能立即成为从节点。
经过这一个月的 MySQL 双主模式的试运行,最后我们还是决定切换到 MySQL 主 - 从模式。
双主模式就是两个节点即是主节点也是从节点,那我们现在切换到一主一从模式,就可以认为是降级。接下来我们聊聊双主换成主从的思路和步骤。
二、双主降为主从
双主模式
双主模式的原理图如下:
两个主节点,都安装了 KeepAlived 高可用组件,对外提供了一个 VIP,只有一个节点接管 VIP,客户端访问的请求都是到这个 VIP,另外一个节点处于待机状态。
主从模式
和双主不一样的地方如下,从节点是只读的。
一主一从是主从模式中的一种,具有以下特点:
-
一个主节点,一个从节点,主节点提供给客户端访问,从节点只通过主节点的 binlog 进行数据同步。
-
从节点是只读的。从节点可以作为只读节点提供类似报表查询等耗时读操作。
-
主节点宕机后,从节点成为主节点,也是高可用的一种方案。
相对于双主的高可用方案,不同之处如下:
-
主从切换需要用脚本将从库设置为可读可写。
-
主从切换后,需要将从库设置为不同步老主库。
-
主从切换后,老的主库恢复后,需要人工设置为只读,且开启同步新主库的功能。
这样来看,主从模式在异常情况下,多了些人工操作。
在异常情况下,主从切换一般是这样处理的:通过脚本监测主节点是否宕机,如果主库宕机了,则从库自动切换为新的主库,待老主库恢复后,就作为从库同步新主库数据,新主库上的 Keepalived 接管 VIP。
目前改为主从模式有两种方式:
-
简单方式:人工切换模式,主节点故障后需要人工切换主从。
-
复杂方式:高可用方式,主节点故障后,主从自动切换,读写分离自动切换。
本篇只涉及简单方式,复杂方式的原理和配置步骤放到下篇专门讲解。
三、改为主从的简单方式
简单方式的主从切换流程如下:
和双主模式的主从切换的区别是,从节点是只读的,Keepalived 没有启动,需要人工操作主从切换和启动 Keepalived。
修改配置的步骤如下:
① 为了避免从节点上的 Keepalived 自动接管 VIP 的情况出现,将从节点的 Keepalived 停止,如果遇到主节点故障,则需要人工干预来进行主从切换。从节点切换为主节点后,重新启动从节点 Keepalived。
systemctl status keepalived
复制代码
② 保留主节点的 Keepalived,保证 MySQL 的连接信息都不需要变。
③ 主节点 node1 停用 MySQL 的同步线程。
STOP SLAVE
复制代码
④ 从节点 node2 设置 MySQL 为只读模式。
# 修改 my.cnf 文件read_only = 1
复制代码
⑤ 移除主节点 node1 同步 node2 MySQL 的权限。
⑥ 从节点 node1 的开机启动项中移除 keepalived 服务自启动。
# 修改启动项配置sudo vim /etc/rc.local# 移除以下脚本systemctl start keepalived
复制代码
四、总结
双主高可用的坑确实比较多,没有 MySQL 的硬核知识真的很难搞定。笔者在这一个月的实践中,深刻体会到了双主同步的难点所在,最后还是选择了一主一从的模式。
另外因为最开始的配置都是双主模式下的,所以要修改一些配置,来改为主从模式。因项目时间比较紧,目前采取的是非高可用的主从模式。
对于高可用的主从模式,因涉及的原理和步骤较多,我会在下篇中进行讲解。各位卷王也请给我一点时间进行探索和实践~
一、背景
为了保证高可用,之前在测试环境部署了一套 MySQL 双主模式,当一个主库服务出现异常,可以将流量切到另外一个主库,两个主库之间相互同步数据。
双主模式
双主模式的原理图如下:
但是经常出现数据冲突的问题,于是我们又把双主模式
改为了主从读写分离模式
。
这样设置解决,所以mysql的逻辑同步不能保证完全一致。适合互联网行业。(解决复制异常的事务。无法解决时就需要手动跳过指定类型的错误,比如通过设置slave_skip_errors=1032,1062。当然这个前提条件是跳过这类错误是无损的。(1062 错误是插入数据时唯一键冲突;1032 错误是删除数据时找不到行))
主库作为读写库,再加上一个从库用来做 I/O 密集型的任务(如大量的数据统计操作)。如下图所示:
另外从库复制的模式采用位点
的方式:指定 binlog 文件和 binlog 位置,这样从库就知道了复制的起始位置。(下文会讲解这种方式)
虽然改为了主从模式,但依旧遇到了些问题:
- 问题 1:从库 B 复制数据时,出现了主键冲突问题,导致同步失败,从库停止复制。猜测因主库配置的 binlog 日志的格式为
mixed
,从库同步时出现不一致的情况。 - 问题 2:从库 B 停止复制后,导致很多数据未同步到从库,出现主从大量数据不一致的情况。
- 问题 3:从库 B 想要恢复复制,必须先解决同步失败的问题才能恢复。排查难度较大,耗时。
- 问题 4:从库 B 恢复时,必须知道同步位点,也就是从哪个 binlog 文件和 binlog 位置断开复制的,且即使找到了位点,也不是精确的。
- 问题 5:从库 B 因同步异常导致停止复制到恢复复制这段期间,主库 A 自动清理了几天前的 binlog 日志,而这些日志从库 B 还未来得及同步,进而导致再次同步失败。
- 问题 6:主从存在同步延迟。
这篇我们来探讨下问题 4 和问题 6。
其中问题 4 是一个比较头疼的问题,我们一般是通过查看从库 B 当前的同步状态拿到同步位点,然后设置同步位点后。但是重新启动同步的时候又会出现同步异常,比如从库 B 可能会出现 Duplicate entry ‘id_of_R’ for key ‘PRIMARY’ 错误,提示出现了主键冲突,然后停止同步。
为了减少位点同步引入的复杂度,我们切换成了 GTID 模式。
对于问题 6,本篇也仅限于探讨如何观察延迟,对于如何减少延迟不在本篇探讨范围之内。
接下来我们来展开看下位点同步的痛点。
二、位点同步的痛点
2.1 通过位点同步的原理图
为了更清晰地理解主从采用位点同步的原理,这里有一个原理图:
1、主库会生成多个 binlog 日志文件。
2、从库的I/O 线程请求指定文件和指定位置的 binlog 日志文件(位点)。
3、主库 dump 线程获取指定位点的 binlog 日志。
4、主库按照从库发送给来的位点信息读取 binlog,然后推送 binlog 给从库。
5、从库将得到的 binlog 写到本地的 relay log (中继日志) 文件中。
6、从库的 SQL 线程读取和解析 relay log 文件。
7、从库的 SQL 线程重放 relay log 中的命令。
当我们使用位点同步的方式时,两种场景下的操作步骤比较复杂。
2.2 痛点
痛点1:首次开启主从复制的步骤复杂
- 第一次开启主从同步时,要求从库和主库是一致的。
- 找到主库的 binlog 位点。
- 设置从库的 binlog 位点。
- 开启从库的复制线程。
痛点2:恢复主从复制的步骤复杂
- 找到从库复制线程停止时的位点。
- 解决复制异常的事务。无法解决时就需要手动跳过指定类型的错误,比如通过设置slave_skip_errors=1032,1062。当然这个前提条件是跳过这类错误是无损的。(1062 错误是插入数据时唯一键冲突;1032 错误是删除数据时找不到行)
不论是首次开启同步时需要找位点和设置位点,还是恢复主从复制时,设置位点和忽略错误,这些步骤都显得过于复杂,而且容易出错。所以 MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。
三、GTID 方案
3.1 GTID 是什么?
GTID 的全称是 Global Transaction Identifier,全局事务 ID,当一个事务提交时,就会生成一个 GTID,相当于事务的唯一标识。
GTID 长这样:
复制
c5d74746-d7ec-11ec-bf8f-0242ac110002:1
- 1.
结构:
复制
GTID=server_uuid:gno
- 1.
server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;
gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。
每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。
3.2 GTID 的优势
- 更简单的实现 failover,不用以前那样在需要找位点(log_file 和 log_pos)。
- 更简单的搭建主从复制。
- 比传统的复制更加安全。
- GTID是连续的没有空洞的,保证数据的一致性,零丢失。
3.3 如何启用 GTID
修改主库和从库的配置文件:
复制
#GTID:
gtid_mode=on
enforce_gtid_cnotallow=on
- 1.
- 2.
- 3.
从库配置同步的参数:
复制
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_positinotallow=1
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
其中 master_auto_position 标识主从关系使用的 GTID 协议。
相比之前的配置,MASTER_LOG_FILE 和 MASTER_LOG_POS 参数已经不需要了。
3.4 GTID 同步方案
GTID 同步的原理图。
GTID 方案:主库计算主库 GTID 集合和从库 GTID 的集合的差集,主库推送差集 binlog 给从库。
当从库设置完同步参数后,主库 A 的GTID 集合记为集合 x,从库 B 的 GTID 集合记为 y。从库同步的逻辑如下:
- 从库 B 指定主库 A,基于主备协议简历连接。
- 从库 B 把集合 y 发给主库 A。
- 主库 A 计算出集合 x 和集合 y 的差集,也就是集合 x 中存在,集合 y 中不存在的 GTID 集合。比如集合 x 是 1~100,集合 y 是 1~90,那么这个差集就是 91~100。这里会判断集合 x 是不是包含有集合 y 的所有 GTID,如果不是则说明主库 A 删除了从库 B 需要的 binlog,主库 A 直接返回错误。
- 主库 A 从自己的 binlog 文件里面,找到第一个不在集合 y 中的事务 GTID,也就是找到了 91。
- 主库 A 从 GTID = 91 的事务开始,往后读 binlog 文件,按顺序取 binlog,然后发给 B。
- 从库 B 的 I/O 线程读取 binlog 文件生成 relay log,SQL 线程解析 relay log,然后执行 SQL 语句。
GTID 同步方案和位点同步的方案区别是:
- 位点同步方案是通过人工在从库上指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
- 而 GTID 方案是通过主库来自动计算位点的,不需要人工去设置位点,对运维人员友好。
四、如何判断主从库是否有延迟
上面提到的问题 6 是主从读写分离后,从库复制存在延迟,接下来我们来探讨下如何观察主从延迟多少的问题。
方案一:判断从库的同步状态参数 seconds_behind_master 是否为 0。(不准确)
方案二:对比位点确保主备无延迟。
方案三:对比 GTID 集合确保主备无延迟。
方案一:查看 seconds_behind_master
可以在从库上执行 slow slave status 命令来看执行结果里面的 seconds_behind_master
参数的值,如下图所示,Seconds_Behind_Master 等于 0
Seconds_Behind_Master 的单位是秒,所以精度不准确。
所以为了保证查询的数据是和主库一致的,就需要先判断 seconds_behind_master 是否已经等于 0,如果不等于 0,就必须等到这个参数变为 0 才能执行查询请求。
方案二:对比位点
可以通过查看从库当前的同步位点来确认从库同步是否有延迟。下图是在从库上执行 show slave status \G
命令后的结果:
Master_Log_File 和 Read_Master_Log_Pos 这两个参数合起来表示的是读到的主库的最新位点,第一参数是代表读取到了哪个文件,第二个是读取到的文件的位置。
Relay_Master_Log_File 和 Exec_Master_Log_Pos,这两个参数合起来表示的是从库执行的最新位点。
如果红色框起来的两个参数:Master_Log_File 和 Relay_Master_Log_File 相等,则说明从库读到的最新文件和主库上生成的文件相同,这里都是 mysql-bin.000934。
如果蓝色框起来的两个参数 Read_Master_Log_Pos 和 Exec_Master_Log_Pos 相等,则说明从库读到的日志文件的位置和从库上执行日志文件的位置相同,这里都是 59521082。
当上面两组参数都相等时,则说明没有延迟。
方案三:对比 GTID 集合
方案三是对比 GTID 集合。首先我们在从库上执行 show slave status \G来查看 GTID 集合。
如下图所示:
Master_UUID 表示当前连接的主库的 ID。
Auto_Position: 1 表示主备使用了 GTID 协议。
Retrieved_Gtid_Set 表示从库收到的所有日志的 GTID 集合。
Executed_Gtid_Set 表示从库已经执行完成的 GTID 集合。
如果 Executed_Gtid_Set 集合是包含 Retrieved_Gtid_Set,则表示从库接收到的日志已经同步完成。
比如上图中 Retrieved_Gtid_Set 值为
复制
c5d74746-d7ec-11ec-bf8f-0242ac110002:1-87323
- 1.
前面一段是主库 id,后面一段 1-87383 是 GTID 范围。而Executed_Gtid_Set 的值有两个集合
复制
7083ae1f-d7ef-11ec-a329-0242ac110002:1-2,
c5d74746-d7ec-11ec-bf8f-0242ac110002:1-87323
- 1.
- 2.
Executed_Gtid_Set 的第二个集合和第一个集合完全一致,第一个集合 id 和 集合范围是上次同步另外一个主库的记录。这里说明从库已经和当前主库同步完成了。
方案二对比位点和方案三的 GTID 比对都要比方案一的seconds_behind_master 更准确。但是还是没有达到精确的程度,需要配合半同步复制(semi-sync replication)才能达到。
小结:本篇通过 GTID 的方式更好地实现了主从节点的同步,以及如何观察主从同步的延迟。
----------------------------------------------------------------------------
事故现场
-
环境:测试环境
-
时间:上午10:30
-
反馈人员:测试群,炸锅了,研发同事初步排查后,发现可能是数据库问题。
然后就开始找原因吧。因为这套集群环境是我部署的,所以我来排查的话轻车熟路。
系统部署图
先说下系统的部署图,方便大家理解。
两个数据库部署在 node55 和 node56 节点上,它们互为主从关系,所以叫做双主。
还有两个 Keepalived 部署在 node55 和 node56 上面,分别监控 MySQL 容器的状态。
报错原因和解决方案
我第一个想法就是,不是有 Keepalived 来保证高可用么?即使 MySQL 挂了,也可以通过 Keepalived 来自动重启才对。即使一台重启不起来,还有另外一台可以用的吧?
那就到服务器上看下 MySQL 容器的状态吧。到 MySQL 的两台服务器上,先看下 MySQL 容器的状态,docker ps 命令,发现两台 MySQL 容器都不在列表中,这代表容器没正常运行。
这不可能。我可是安装了 Keepalived 高可用组件的,难道 Keepalived 也挂了?
赶紧检查一波 Keepalived,发现两台 Keepalived 是正常运行的。通过执行命令查看:systemctl status keepalived。
纳尼?Keepalived 也是正常的。Keepalived 每隔几秒会重启 MySQL,可能我在那一小段空闲时间没看到 MySQL 容器启动?换个命令执行下,docker ps -a,列出所有容器的状态。可以看到 MySQL 启动后又退出了,说明 MySQL 确实是在重启。
那说明 Keepalived 虽然重启了 MySQL 容器,但是 MySQL 自身有问题,那 Keepalived 的高可用也没办法了。
那怎么整?只能看下 MySQL 报什么错了。执行查看容器日志的命令。docker logs <容器 id>。找到最近发生的日志:
提示 mysql-bin.index 文件不存在,这个文件是配置在主从同步那里的,在 my.cnf 配置里面。
这个配置好后,然后执行主从同步的时候,就会在 var/lib/mysql/log 目录下生成多个 mysql-bin.xxx 的文件。还有一个 mysql-bin.index 索引文件,它会标记现在 binlog 日志文件记录到哪里了。
mysql-bin.index 文件里面的内容如下:
/var/lib/mysql/log/mysql-bin.000001
这个 mysql-bin.000001 文件还是带序号的,这里还有坑,后面我再说。
报错信息是提示缺少 mysql-bin.index,那我们就去检查下呗,确实没有啊!先不管这个文件怎么消失的吧,赶紧把这个 log 文件夹先创建出来,然后 mysql 会自动给我们生成这个文件的。
解决方案:执行以下命令创建文件夹和添加权限。
mkdir log
chmod 777 log -R
两台服务器上都有这个 log 目录后,Keepalived 也帮我们自动重启好了 MySQL 容器,再来访问下其中一个节点 node56 的 MySQL 的状态。咦?居然报错了。
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'
可以看到几个关键信息:
-
Slave_IO_Running: NO,当前同步的 I/O 线程没有运行,这个 I/O 线程是从库的,它会去请求主库的 binlog,并将得到的 binlog 写到本地的 relay-log (中继日志)文件中。没有运行,则代表从库同步是没有正常运行。
-
Master_Log_File: mysql-bin.000014,说明当前同步的日志文件为 000014,之前我们看到节点 node56 上 mysql.index 里面写的是 000001,这个 000014 根本就不在 index 文件里面,所以就会报错了。
这里涉及到主从同步的原理,上一张图:
-
从库会生成两个线程,一个 I/O 线程,一个 SQL 线程;
-
I/O线程会去请求主库的 binlog 日志文件,并将得到的 binlog 日志文件 写到本地的 relay-log (中继日志)文件中;
-
主库会生成一个 dump 线程,用来给从库 I/O 线程传 binlog;
-
SQL线程,会读取 relay log 文件中的日志,并解析成 SQL 语句逐一执行。
那好办啊,我们重新指定下同步哪个日志文件,以及同步的位置就好了。
解决方案
看下主库 node55 上日志文件状态。
记下这两个信息:File=mysql-bin.00001,Position=117748。
(这里也有个坑:先要锁表,再看这两个值,从库开始同步后,再解锁表)。
具体执行的命令如下:
FLUSH TABLES WITH READ LOCK;
SHOW MASTER STATUS
UNLOCK TABLES
然后在从库 node56 上重新指定同步的日志文件和位置:
# 停止从库同步
STOP SLAVE;
# 设置同步文件和位置
CHANGE MASTER TO MASTER_HOST='10.2.1.55',
MASTER_PORT=3306,
MASTER_USER='vagrant',
MASTER_PASSWORD='vagrant',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=117748;
# 开启同步
START SLAVE;
再次查看就不报错了,I/O 线程也跑起来了,
然后将 node55 当做从库,node56 当做主库,同样执行上面的几步,状态显示正常了,然后用 navicat 工具连下数据库,都是正常的。在测试群反馈下结果,搞定收工。
好像忘了一个问题,为啥 log 文件夹被干掉了??
为什么会出现问题?
然后问了一波当时有没有人删除这个 /var/lib/mysql/log 目录,也没有人会随便删除这个目录的吧。
但是发现 log 的上级目录 /var/lib/mysql 有很多其他文件夹,比如 xxcloud, xxcenter 等。这不就是我们项目中几个数据库的名字么,只要在这个目录的文件夹,都会显示在 navicat 上,是一一对应的,如下图所示。其中也显示了 log 数据库。
那会不会有人从 navicat 上干掉了 log 数据库?极有可能啊!
果然,有位同事之前在迁移升级的过程中,发现这个 log 数据库在老的系统是没有的,所以就清理了。这就相当于把 log 数据库干掉了,同时也会把 log 文件夹干掉了。好了,终于水落石出了!这个其实也是我前期没有考虑到 log 目录的一个问题。没错,这是我的锅~
改进
其实操作同步数据库的时候,不应该用这种覆盖同步的方式。可以采取单库同步的方式,也就不会干掉 log 数据库了。但是,这个 log 数据库放在这里有点奇怪啊,能不能不要出现在这里呢?
每次新建库都要设置同步
我们只要指定这个 log 目录不在 /var/lib/mysql 目录下就好了。
东哥建议:log 文件和数据库 data 文件进行隔离:
datadir = /var/lib/mysql/data
log_bin = /var/lib/mysql/log
另外一个问题,我们的高可用真的高可用了吗?
至少没有做到及时报警,MySQL 数据库挂了,我是不知道的,都是通过测试同学反馈的。
能不能及时感知到 MySQL 异常呢?
这里可以利用 Keepalived 发送邮件的功能,或者通过日志报警系统。这个是后面需要改进的地方。