欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 幼教 > 海山数据库(He3DB)技术分享:海山MySQL 5.7版本GTID丢失问题分析及解决方案

海山数据库(He3DB)技术分享:海山MySQL 5.7版本GTID丢失问题分析及解决方案

2024/10/24 14:16:05 来源:https://blog.csdn.net/He3DB_ht/article/details/140671491  浏览:    关键词:海山数据库(He3DB)技术分享:海山MySQL 5.7版本GTID丢失问题分析及解决方案

文章目录

  • 一、问题现象
  • 二、问题原因
    • 管控问题
    • 内核源码问题
  • 三、问题分析
    • 问题复现
    • 源码分析
      • binlog创建
      • 初始化server uuid
      • 读取第一个GTID持久化介质
      • 读取第二个持久化介质
      • 持久化Previous-GTIDs
      • 更新executed_gtids变量
      • previous set信息写入新的binlog
    • GTID初始化流程
  • 四、修复方案
    • 修改参数
    • 内核修改
  • 五、问题思考
  • 六、作者

本文将从源码层面分析MySQL 5.7因异常重启导致整个binlong中GTID丢失问题,并逐步梳理出MySQL 5.7中GTID持久化和初始化的过程。本问题的复现步骤、产生的原因、修复方案均以BUG和patch的方式反馈给了社区,percona社区已经在其5.7版本中merge了我们贡献的patch代码。

注:本文以MySQL 5.7.44版本源码进行分析和梳理,且只针对主库。

一、问题现象

在主备实例环境中,主库重启之后,备库同步报错,备库的GTID要比主库多很多。进一步排查发现主库的GTID存在丢失,具体表现如下:

  1. mysql.gtid_executed表的GTID信息没更新

  2. global.gtid_executed没有更新

  3. 倒数第二个binlog日志中没有Previous-GTIDs信息,且大小为123kb

  4. 最新产生的binlog中的Previous-GTIDs和最近一个含有Previous-GTIDs的值相同

  5. 最新产生的binlog中的GTID起始值和最近一个含有GTID EVENT的GTID起始值相同

注:发生上述问题的前置条件为第一次重启前的最后一个binlog中存在GTID EVENT

二、问题原因

管控问题

数据库第一次重启流程没有完成时(准确的讲是在第一次重启时没有在新产生的binlog中写入Previous-GTIDs信息),其他组件将mysqld进程强制kill。

内核源码问题

内核源码中,读取binlog(第二个GTID持久化介质)时,在反向扫描最后一个binlog时,如果此时binlog中不包含任何Previous-GTIDs和GTID EVENT,且参数binlog_gtid_simple_recovery为ON的情况下,则不会再去读取任何其他的binlog文件。

三、问题分析

问题复现

通过GDB设置断点的方式,控制mysqld的启动流程,具体复现步骤如下:

  • 安装MySQL-5.7.44

具体安装步骤略

  • 开启binlog和gtid

在参数文件中配置参数

log-bin=mysql-binserver_id=1enforce_gtid_consistency=ongtid_mode=on
  • 重启数据库
systemctl restart mysqld
  • 手动生成部分GTID EVENT
create database testdb;Use testdb;Create table t1(id int, name varchar(20));Insert into t1 values(1,’test01’);  ---可多条重复执行
  • 关闭数据库
systemctl stop mysql
  • GDB启动mysqld并设置断点

gdb --args /usr/sbin/mysqldb MYSQL_BIN_LOG::open_binlogb MYSQL_BIN_LOG::init_gtid_setsb Gtid_state::saverun

注:在MYSQL_BIN_LOG::open_binlog执行完成之后,在Gtid_state::save完成之前kill掉mysqld和GDB进程

在这里插入图片描述
在这里插入图片描述

  • KILL掉mysqld和GDB进程

此时会产生一个大小123KB且无Previous-GTIDs信息的binlog

在这里插入图片描述
在这里插入图片描述

  • 再次启动数据库

此次重启会再产生一个新的binlog,但该binlog中的Previous-GTIDs和mysql-bin.000004中的一致,且global.gtid_executed变量中的信息亦和mysql-bin.000004中的一致,最终导致的结果是mysql-bin.000004中的GTID EVENT 全部丢失,问题复现。

源码分析

此部分只针对数据库启动的代码流程,且只关注主库的GTID初始化和各个模块中GTID更新的时机,因为GTID的更新在很多场景下都会进行,比如:purge binary、flush logs等,而备库的更新流程则还跟参数log_slave_updates有关。

binlog创建

MySQL开启binlog,在每次重启过程中,都会执行MYSQL_BIN_LOG::open_binlog函数创建一个新的binlog文件,并初始化standard header信息,但不包括Previous-GTIDs信息。在mysqld.cc中通过if (opt_bin_log)进入,关键步骤如下:

(1) 初始化新binlog的文件名,如:mysql-bin.000010

init_and_set_log_file_name(log_name, new_name)

(2) 在binlog目录下生成新的binlog文件,此时在文件系统上就可以看到生成的文件

if (open(#ifdef HAVE_PSI_INTERFACEm_key_file_log,#endiflog_name, new_name)){#ifdef HAVE_REPLICATIONclose_purge_index_file();#endifDBUG_RETURN(1);                            /* all warnings issued */}

(3) 将standard header信息写入到binlog文件,但不包含Previous-GTIDs信息

if (flush_io_cache(&log_file) ||mysql_file_sync(log_file.file, MYF(MY_WME)))goto err;

(4) 将新生成的binlog文件名写入到mysql-bin.index

add_log_to_index((uchar*) log_file_name, strlen(log_file_name),need_lock_index))

注:该函数完成之后,在binlog存放的目录下可以看到生成了一个不含Previous-GTIDs信息的binlog,大小为123kb。如果在此函数执行的过程中kill掉mysqld进程,则不会出现我们复现的故障,因为此时会有一个mysql-bin.rec的文件保证原子性,此时重启,则不会生成新的binlog,会复用本次生成的binlog文件名( log_file_name );如果在该函数执行完成后,立即kill掉mysqld进程,则会复现故障。

初始化server uuid

通过init_server_auto_options函数初始化server uuid信息,此函数比较简单,且和后面逻辑关联不强,但uuid是GTID的重要组成部分。

读取第一个GTID持久化介质

该步比较重要,读取第一个GTID持久化介质,即mysql.gtid_executed表,通过gtid_state->read_gtid_executed_from_table函数完成,其真正调用的是gtid_table_persistor->fetch_gtids函数,主要的逻辑是一行一行的读取mysql.gtid_executed表中的数据,并加入gtid_executed变量

  while(!(err= table->file->ha_rnd_next(table->record[0]))){/* Store the gtid into the gtid_set *//**@todo:- take only global_sid_lock->rdlock(), and takegtid_state->sid_lock for each iteration.- Add wrapper around Gtid_set::add_gno_interval and call thatinstead.*/global_sid_lock->wrlock();if (gtid_set->add_gtid_text(encode_gtid_text(table).c_str()) !=RETURN_STATUS_OK){global_sid_lock->unlock();break;}global_sid_lock->unlock();}

注:因主库的mysql.gtid_executed表信息是不正确的,即不是实时更新的,是旧的(不包括最近异常binlog中的GTID EVENT信息),所以在正常情况下,就算mysql.gtid_executed表的数据库被清空了,也不会影响数据库重启之后GTID的正确性,在接下来的读取第二个GTID持久化介质(binary log)才是重点。

读取第二个持久化介质

MYSQL_BIN_LOG::init_gtid_sets函数是我们本次问题的核心函数,第一次在mysql.gtid_executed表中读取到不正确GTID的逻辑下,进行了第二个持久化介质(binary log)的读取。读取中包含两个最核心的逻辑。

  1. 先循环读取本地的binlog日志并将其加入到filename_list中
for (error= find_log_pos(&linfo, NULL, false/*need_lock_index=false*/); !error;error= find_next_log(&linfo, false/*need_lock_index=false*/)){DBUG_PRINT("info", ("read log filename '%s'", linfo.log_file_name));filename_list.push_back(string(linfo.log_file_name));            }
  1. 如果是重启,在该步前会生成一个新的binlog,该binlog不是需要去读取的最后一个binlog,所以要在filename_list中去掉
if (is_server_starting && !is_relay_log && !filename_list.empty())filename_list.pop_back();

在这里插入图片描述

注:该步导致的结果是,上一次重启产生的既没有 Previous-GTIDs、也没有GTID EVENT的binlog就成为了下一步反向扫描中的最后一个binlog

  1. 先反向扫描,获取最后一个binlog中包含的最新GTID EVENT和Previous-GTIDs
rit= filename_list.rbegin();bool can_stop_reading= false;reached_first_file= (rit == filename_list.rend());DBUG_PRINT("info", ("filename='%s' reached_first_file=%d",reached_first_file ? "" : rit->c_str(),reached_first_file));while (!can_stop_reading && !reached_first_file){const char *filename= rit->c_str();assert(rit != filename_list.rend());rit++;reached_first_file= (rit == filename_list.rend());DBUG_PRINT("info", ("filename='%s' can_stop_reading=%d ""reached_first_file=%d, ",filename, can_stop_reading, reached_first_file));switch (read_gtids_from_binlog(filename, all_gtids,reached_first_file ? lost_gtids : NULL,NULL/* first_gtid */,sid_map, verify_checksum, is_relay_log)){case ERROR:{error= 1;goto end;}case GOT_GTIDS:{can_stop_reading= true;break;}case GOT_PREVIOUS_GTIDS:{/*If this is a binlog file, it is enough to have GOT\_PREVIOUS\_GTIDS.If this is a relaylog file, we need to find at least one GTID tostart parsing the relay log to add GTID of transactions that mighthave spanned in distinct relaylog files.*/if (!is_relay_log)can_stop_reading= true;break;}case NO_GTIDS:    //出现问题的核心逻辑{/*Mysql server iterates backwards through binary logs, looking forthe last binary log that contains a Previous_gtids_log_event forgathering the set of gtid\_executed on server start. This may takevery long time if it has many binary logs and almost all of themare out of filesystem cache. So if the binlog_gtid_simple_recoveryis enabled, and the last binary log does not contain any GTIDevent, do not read any more binary logs, GLOBAL.GTID_EXECUTED andGLOBAL.GTID\_PURGED should be empty in the case.*/if (binlog_gtid_simple_recovery && is_server_starting &&!is_relay_log){assert(all_gtids->is_empty());assert(lost_gtids->is_empty());goto end;        //直接跳出了while循环,不再继续读取上一个binlog}/*FALLTHROUGH*/}case TRUNCATED:{break;}}}

注:如果反向读取的最后一个binlog中既没有Previous-GTIDs、也没有GTID EVENT,不再继续反向读取前一个binlog,最终导致更新到global.gtid_executed变量的信息是第一次读取GTID持久化介质mysql.gtid_execute表中陈旧的信息。

  1. 再正向扫描,获取第一个binary log中的lost gtid(即Previous-GTID),该步和本次线上问题无关,不做过多阐述,但和解决方案的选取有关
if (lost_gtids != NULL && !reached_first_file)       //正向查找获得purged_gtids_from_binlog{/*This branch is only reacheable by a binary log. The relay logdon't need to get lost_gtids information.A 5.6 server sets GTID_PURGED by rotating the binary log.A 5.6 server that had recently enabled GTIDs and set GTID_PURGEDwould have a sequence of binary logs like:master-bin.N  : No PREVIOUS_GTIDS (GTID wasn't enabled)master-bin.N+1: Has an empty PREVIOUS_GTIDS and a ROTATE(GTID was enabled on startup)master-bin.N+2: Has a PREVIOUS_GTIDS with the content set by aSET @@GLOBAL.GTID_PURGED + has GTIDs of sometransactions.If this 5.6 server be upgraded to 5.7 keeping its binary log files,this routine will have to find the first binary log that contains aPREVIOUS_GTIDS + a GTID event to ensure that the content of theGTID_PURGED will be correctly set (assuming binlog_gtid_simple_recoveryis not enabled).*/DBUG_PRINT("info", ("Iterating forwards through binary logs, looking for ""the first binary log that contains both a ""Previous_gtids_log_event and a Gtid_log_event."));assert(!is_relay_log);for (it= filename_list.begin(); it != filename_list.end(); it++){/*We should pass a first_gtid to read_gtids_from_binlog whenbinlog_gtid_simple_recovery is disabled, or else it will returnright after reading the PREVIOUS_GTIDS event to avoid stall onreading the whole binary log.*/Gtid first_gtid= {0, 0};const char *filename= it->c_str();DBUG_PRINT("info", ("filename='%s'", filename));switch (read_gtids_from_binlog(filename, NULL, lost_gtids,binlog_gtid_simple_recovery ? NULL :&first_gtid,sid_map, verify_checksum, is_relay_log)){case ERROR:{error= 1;/*FALLTHROUGH*/}case GOT_GTIDS:{goto end;}case NO_GTIDS:case GOT_PREVIOUS_GTIDS:{/*Mysql server iterates forwards through binary logs, looking forthe first binary log that contains both Previous_gtids_log_eventand gtid_log_event for gathering the set of gtid_purged on serverstart. It also iterates forwards through binary logs, looking forthe first binary log that contains both Previous_gtids_log_eventand gtid_log_event for gathering the set of gtid_purged whenpurging binary logs. This may take very long time if it has manybinary logs and almost all of them are out of filesystem cache.So if the binlog_gtid_simple_recovery is enabled, we justinitialize GLOBAL.GTID_PURGED from the first binary log, do notread any more binary logs.*/if (binlog_gtid_simple_recovery)goto end;/*FALLTHROUGH*/}case TRUNCATED:{break;}}}}

持久化Previous-GTIDs

通过gtid_state->save函数将不在mysql.executed_gtids表中,但在binlog中的gtid信息保存到mysql.executed_gtids中,即上述步骤中得到的gtids_in_binlog_not_in_table信息,实际调用的函数为:gtid_table_persistor->save

注:此步的作用是将最后一个binlog中的Previous-GTIDs信息持久化到mysql.executed_gtids表中。

更新executed_gtids变量

通过executed_gtids->add_gtid_set函数在executed_gtids变量中加入不在mysql.executed_gtids表中,但在binlog中的gtid信息,至此,executed_gtids变量的信息才是准确的信息。

previous set信息写入新的binlog

最后通过flush_io_cache(mysql_bin_log.get_log_file()) || mysql_file_sync()函数将previous set信息写入到新的binlog中。

在这里插入图片描述

注:至此,重启过程中的GTID初始化结束。

GTID初始化流程

根据上面源码的分析,可以梳理出MySQL 5.7中GTID初始化的流程图。

在这里插入图片描述

注:在MySQL 5.7中,gtid_executed表是GTID持久化的一个介质,其更新(持久化)时机是在binary log切换时。所以在数据库启动读取的信息不是最新的、不准确的。

四、修复方案

根据上述逻辑,只需要修改MYSQL_BIN_LOG::init_gtid_sets中出现NO_GTIDS时,不跳出反向读取binlog的逻辑即可,可通过如下两种方案进行修复。

修改参数

该修改方式有一定的缺陷,比如当做purge binary logs操作时会存在一定的性能问题

binlog_gtid_simple_recovery=OFF;

内核修改

修改MYSQL_BIN_LOG::init_gtid_sets中出现NO_GTIDS时的逻辑,使其继续向上扫描binlog,直到扫描的binlog中存在Previous-GTIDs信息。

原始逻辑:

if (binlog_gtid_simple_recovery && is_server_starting &&!is_relay_log){assert(all_gtids->is_empty());assert(lost_gtids->is_empty());goto end;}

修改为:

if (binlog_gtid_simple_recovery && is_server_starting &&!is_relay_log && reached_first_file){assert(all_gtids->is_empty());assert(lost_gtids->is_empty());goto end;}

五、问题思考

该问题只在MySQL 5.7中出现,且根据上面的逻辑,可以继续探讨如下遗留问题。

  1. GTID第一个持久化介质(即mysql.gtid_executed表)中GTID信息既然不是实时更新的,那为什么启动的时候要去读呢?为什么不直接读取第二个持久化介质(即binary log)中的GTID信息呢?

  2. GTID第一个持久化介质(即mysql.gtid_executed表)的作用是什么?如果可以直接读取第二个持久化介质,那mysql.gtid_executed表可以直接去掉么?

  3. 该问题只在MySQL 5.7中出现,那MySQL 8.0中GTID持久化又是怎么处理的?

六、作者

戴浩 - 中国移动云能力中心数据库产品部 - 数据库内核研发工程师

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com