前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >故障分析 | MySQL 5.7 连续 Crash 引发 GTID 丢失

故障分析 | MySQL 5.7 连续 Crash 引发 GTID 丢失

作者头像
爱可生开源社区
发布2024-09-14 18:48:27
发布2024-09-14 18:48:27
15400
代码可运行
举报
运行总次数:0
代码可运行

作者:xuty。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

本文约 1400 字,预计阅读需要 5 分钟。


1问题现象

生产环境中 MySQL 5.7.26 版本下,当主库短时间内连续遇到 2 次 Crash 的特殊场景时,会导致备库重新建立复制时会抛出错误 Slave has more GTIDs than the master has,IO 线程复制报错。

2023-12-11T10:32:43.433707+08:00 1457551 [ERROR] Error reading packet from server for channel '': Slave has more GTIDs than the master has, using the master's SERVER_UUID. This may indicate that the end of the binary log was truncated or that the last binary log file was lost, e.g., after a power or disk failure when sync_binlog != 1. The master may or may not have rolled back transactions that were already replicated to the slave. Suggest to replicate any transactions that master has rolled back from slave to master, and/or commit empty transactions on master to account for transactions that have been (server_errno=1236)

如下图所示,备库比主库多了几十万个 GTID,且解析对应 Binlog 都是业务操作,更像是主库把这部分 GTID “丢掉了”。

  • 主库:1-1080678246;
  • 备库:1-1081067155;

2问题分析

先来复习下 MySQL 5.7 GTID 持久化原理:

  • gtid_executed 变量:它是一个处于内存中的 GTID SET,表示数据库中执行了那些 GTID,会实时更新,但是一旦重启就会丢失。show slave statusExecuted_Gtid_Setshow master status 中的 Executed_Gtid_Set 都来自于这个变量。
  • mysql.gtid_executed 表:GTID 持久化的介质,只有在 binlog 切换时才会触发更新。将该 binlog 中的 GTID SET 记录到表中,所以该表中会记录所有历史 binlog 中的 GTID SET

📌当 MySQL 启动时,会初始化 gtid_executed 变量。通过读取 mysql.gtid_executed 表的持久化记录(已持久化的 GTID),再加上扫描最后一个 binlog 的 GTID(未持久化的 GTID)合并后完成初始化,新的 GTID 会基于 gtid_executed 变量递增产生。

基于上述 GTID 持久化的原理,我们就有理由怀疑是主库没有持久化最后 1 个 binlog 中的所有 GTID,导致备库比主库多了很多的 GTID。

通过对比 Crash 前后的 binlog 中 GTID,发现主库确实并没有持久化到 mysql-bin.001499 中的 binlog,导致后续新产生的 GTID 反而比之前的还小。(正常情况下 GTID 肯定是随着 binlog 越来越大的)。

3GDB 调试复现

基于 MySQL 5.7.26 版本,通过 GDB 调试模拟复现了上述问题现象,主库连续崩溃恢复后会丢失最后 1 个 binlog 中的 GTID,引发备库 GTID 大于主库。

先讲下原因:

  1. 在 MySQL 第一次崩溃恢复过程中,会先创建新的 binlog,再将崩溃前最后 1 个 binlog 中的 GTID 持久化到表中;
  2. 如果在这个间隙,再次发生崩溃,就可能会导致 MySQL 已经产生了新的 binlog,但是还未将第一次崩溃前最后 1 个 binlog 持久化到表中;
  3. MySQL 再次启动时,就不会再读取第一次崩溃前最后 1 个binlog 做持久化了,而是读取新产生的 binlog 作持久化,那么就会丢失第一次崩溃前最后 1 个 binlog 中的 GTID ;
代码语言:javascript
代码运行次数:0
复制
#mysql5.7.26 crash启动流程
|main
 |mysqld_main
  |ha_recover         #mysqld.cc:4256 恢复数据流程
  |open_binlog        #mysqld.cc:4282 生成新的binlog
  |Gtid_state::save   #mysqld.cc:4870 读取最后1个binlog写入mysql.gtid_executed
   |Gtid_table_persistor::save
    |Gtid_table_persistor::write_row

本地 GDB 模拟复现过程

  1. 使用 MySQL 5.7.26 mysqld-debug 版本启动 MySQL。
  1. 连入 MySQL,创建 test库,生成 1 个GTID。当前 binlog 为 mysql-bin.000001mysql.gtid_executed 表为空,GTID 还未持久化。
  1. kill -9 mysql 进程模拟 OOM。
  1. 使用 GDB 启动 MySQL,并打了2个断点,分别是MYSQL_BIN_LOG::open_binlog(创建新的binlog) 和 Gtid_state::save(崩溃启动过程中持久化最后1个binlog的GTID);
  1. RUN开始,会停在第一个断点 MYSQL_BIN_LOG::open_binlog 处,此时可以看到还未产生新的 binlog。
  1. continue 继续跑,会卡在第二个断点 Gtid_state::save,此时已经生成了新的 binlog,但是还未将 mysql-bin.000001 中的 GTID 持久化。
  1. 正常情况下 Gtid_state::save 走完,就会把 mysql-bin.000001 中的 GTID 持久化到表中,这里模拟把 mysql 给 kill 了,此时产生了新的 binlog 但是还未做完 GTID 持久化。
  1. 再次正常启动 MySQL,就会发现“丢掉了”mysql-bin.000001 中的 GTID,此次启动并不会再次读取 mysql-bin.000001 中的 GTID 做持久化;所以备库会比主库多整整 1 个 binlog 的 GTID。

同样的测试在 MySQL v5.7.26、v5.7.36、v5.7.44 均可以复现,说明 MySQL 5.7 中都存在该现象。而 MySQL 8.0 因为 GTID 持久化做了优化,所以不会有此类问题。

4总结

在 MySQL 5.7 版本下,因为 GTID 持久化机制的原因,当 MySQL 处于崩溃恢复阶段时,如果再次遇到 Crash,就可能会丢失最后 1 个 binlog 中的 GTID。

本文关键字:#MySQL# #主从复制# #GTID#

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-08-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爱可生开源社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1问题现象
  • 2问题分析
  • 3GDB 调试复现
    • 本地 GDB 模拟复现过程
  • 4总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档