mysql-5-7-完美的分布式事务支持,连接断开导致XA事务丢失等。本文不对XA事务做全面介绍…" /> mysql-5-7-完美的分布式事务支持,连接断开导致XA事务丢失等。本文不对XA事务做全面介绍…" />
绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
MySQL 5.7版本XA事务若干bug分析
2018-06-22 17:32:21

MySQL 5.7实现了对XA事务的完美支持,通过将XA PREPARE操作记录在Binlog文件来解决prepare事务的持久化问题。MySQL 5.7 XA事务目前已经有多篇文章进行了介绍,可参考 mysql-5-7-完美的分布式事务支持连接断开导致XA事务丢失等。本文不对XA事务做全面介绍,而是分析MySQL 5.7版本存在的bug,相信对大家使用MySQL XA事务会有所帮助。


group commit相关问题

首先是一个和组提交相关的bug,该bug在官方新的MySQL 5.7.22版本仍存在未修复的XA bug bug#88534,网易内部的兄弟部门也遇到过(详见复现链接)。该bug的定位过程也能够了解MySQL的XA事务PREPARE和COMMIT的具体实现。

下面是对bug#88534的描述,针对所述这两点,我们将一一详细展开分析::

1 ha_prepare function first prepare the binlog, then the engines.

If there's error or mysqld crashed when the engine prepare, the engine prepared info would lost. But the slave would receive the XA binlog.

2. As ha_prepare function call binlog_prepare first, the thd->durability_property would be HA_IGNORE_DURABILITY. Then the engine's prepare would not flush logs. Event though the client receive success after issue 'XA prepare', the prepared transaction may also lost after server crash. And this would also cause slave replication not consistent.

xa prepare crash问题

点的意思是说XA Prepare的时候会调用函数ha_prepare,在该函数中会先进行Binlog prepare,然后再进行引擎层的prepare,如果Binlog prepare成功后,在执行引擎层prepare时出错或mysqld crash了,引擎层的prepare信息会丢失,但Slave端仍会收到该XA事务的Prepare Binlog信息。这段话直接那代码来说明,描述比较模糊。我们看下ha_prepare,很容易可以发现其所述的代码段如下图:

依次调用该事务所涉及的handlerton的prepare函数,那么Binlog和引擎层(InnoDB)prepare函数对象是什么呢,通过分析可知分别为binlog_prepare和innodb_xa_prepare,通过在执行XA Prepare时加gdb断点确认确实如此:

所以,点就变为:ha_prepare函数会先调用binlog_prepare进行XA事务的Prepare操作,然后再调用引擎层的XA Prepare函数innodb_xa_prepare,假设在binlog_prepare成功返回后,执行innodb_xa_prepare时出错或mysqld crash了,引擎层的prepare信息会丢失,但Slave端仍会收到该XA事务的Prepare Binlog信息。从后半句的描述可以猜测,在binlog_prepare函数中会将Binlog信息写到Binlog文件中,其实熟悉MySQL group commit实现的同学知道,binlog_prepare函数在咨询每条DML操作时都会调用,目的是为了更新事务的last_committed信息。另外我们也知道,MySQL 5.7的XA事务在做prepare的时候,会记Binlog,那么很显然,对于XA Prepare操作,binlog_prepare函数还会写Binlog文件。正常来说,(还是与MySQL group commit实现有关)事务的Binlog是在MYSQL_BIN_LOG::ordered_commit函数中写入Binlog文件的(group commit三部曲:flush,sync,commit)。是不是进行XA Prepare的时候binlog_prepare调用了ordered_commit函数,我们再加个断点进行调试:

进一步看binlog_prepare的代码发现,在参数all为true,且是XA Prepare时,会调用MYSQL_BIN_LOG::commit:

在commit函数中,会在事务的Binlog Cache中写入XA PREPARE信息:

接着调用MYSQL_BIN_LOG::ordered_commit,注意,此时的传入参数为(this=this@entry=0x1fb23c0, thd=thd@entry=0x9b56b70, all=all@entry=true, skip_commit=skip_commit@entry=true),skip_commit被赋予thd->get_transaction()->m_flags.commit_low(= !skip_commit),这也就是意味着,跟普通事务的ordered_commit不一样,由XA Prepare触发的该流程仅完成Binlog的持久化(并更新Binlog文件位置信息,从而dump线程可拉取新的Binlog发送给Slave,详见“semi-sync下是否会有此Bug”小节分析),不会进行引擎层的事务提交,另外,前面已经说了,XA Prepare是先进行Binlog prepare再InnoDB prepare,没有进行prepare的事务也不应该直接进行commit。

binlog_prepare函数返回后,就以为这Binlog已经持久化,再继续调用引擎层的prepare函数,终会调用InnoDB事务系统的trx_prepare_low进行事务的prepare,调用栈如下:

如果进行innodb_xa_prepare时执行出错,那么会调用ha_rollback_trans回滚该事务,但由于binlog已经prepare了,在主从复制场景下,该Binlog会通过dump线程传到Slave端。那么就会出现主上正确得处理了执行出错的prepare,但Slave上如果万一这个错误的prepare执行成功了,那么就会导致Slave端有个prepare事务,该事务会一直处于prepare状态;如果执行innodb_xa_prepare时mysqld crash了,reboot后,由于InnoDB存储引擎层,该事务未处于Prepare状态,InnoDB在recv的undo处理阶段,会将此活跃事务回滚掉,此时也会出现前述的情况,及Slave端该事务正常Prepare了,Master端回滚了。两种情况都会导致主从数据不一致。不过由于Slave端事务处于Prepare阶段,对用户是不可见的,所以该情况造成的影响较小。

xa prepare数据未持久化

我们再来看下第二点:

2. As ha_prepare function call binlog_prepare first, the thd->durability_property would be HA_IGNORE_DURABILITY. Then the engine's prepare would not flush logs. Event though the client receive success after issue 'XA prepare', the prepared transaction may also lost after server crash. And this would also cause slave replication not consistent.

意思是由于binlog_prepare调用MYSQL_BIN_LOG::ordered_commit时,将thd->durability_property设置为HA_IGNORE_DURABILITY(如下图),导致InnoDB在进行prepare的时候不会持久化redo log(通过在trx_flush_log_if_needed_low设置断点发现并没有触发)。也就是说,XA Prepare返回时,虽然引擎层已经将事务状态设置为Prepared,但是并没有将相应的日志落盘,此时如果mysqld所在的服务器crash了(由于redo采用buffer IO的方式,即会写到page cache,所以mysqld宕机不会造成对应的日志丢失),reboot后InnoDB recv时也可能会回滚对应的事务,即问题一的情况。


xa commit crash问题

除了bug中所述的这两点问题,我们还发现了另一个问题,那就是如果在XA Commit时mysqld crash了,虽然此时XA Commit对应的Binlog已经写了,但不会像普通事务一样,在mysqld recovery阶段通过Binlog和InnoDB中处于Prepared状态的事务进行比对来提交对应的事务,即InnoDB中已经prepare的事务,如果该事务的Binlog已记录在Binlog中,则MySQL Server层会执行Commit提交这些事务。通过查看代码发现了其中的原因,Server层调用MYSQL_BIN_LOG::recover函数来执行前面所说的操作,它会遍历后一个Binlog文件,收集记录了提交日志的事务信息,然后调用InnoDB注册的函数ha_recover完成这些事务在引擎层的提交。

细心的同学已经发现了,遍历Binlog文件时,只会收集XID_EVENT记录的事务信息。而对在QUERY_EVENT中的提交信息并没有进行相应的处理。因为后者是有非事务引擎提交的事务。如下图:


而XA COMMIT也是记录在QUERY_EVENT中的,导致扫描的时候过滤掉了对应的XA事务。



这就是XA COMMIT无法crash safe的原因。

(2020-3-16 更新,这里分析的xa commit和xa rollback无法crash-safe问题,有人给Oracle又提了个bug并提供了patch,我们将进一步跟进)

PS:近在看淘宝的MySQL内核月报发现,有篇文章也建了本文所述的问题。mysql.taobao.org/monthl

修复xa prepare问题

至此,XA事务相关的问题都已经阐述了,应该说这些问题并不会对线上应用产生大的影响。而且出现的概率也比较小。这也是网易杭研截止目前没有发现的原因。那么如果要进行修复,应该怎么做呢,bug链接中也附上了对应的说明:

相对来说,第二种方案更加友好。简单说来就是在XA PREPARE时,调整prepare的顺序,先让InnoDB进行prepare,然后再Binlog prepare。但如果在Binlog完成prepare前,mysqld crash了,由于此时还没有记录Binlog,所以,mysqld reboot后需要将引擎层已经prepare的事务回滚掉。所以,需要基于Binlog文件中是否记录XA PREPARE日志来决定回滚掉引擎层已prepare的事务。但是Binlog文件的rotate操作不会受XA事务是否已提交的影响,也就是说,系统中还有处于prepare状态的XA事务时,Binlog文件能够正常rotate。这样的话就无法仅扫描后一个Binlog文件来获取XA事务是否已经记录了PREPARE Binlog信息,否则可能会导致XA事务被错误回滚。所以,在方案中,引入了一个新的系统表在Binlog rotate时记录处于prepare的XA事务。

修复xa commit问题

对于XA COMMIT无法crash safe的问题,只需要修改XA COMMIT Binlog类型或者故障恢复时识别QUERY_EVENT并匹配“XA COMMIT”,解析其中记录的XID信息。咨询了MySQL官方开发,这个问题会在后续版本修改。


xtrabackup与XA相关的bug和选项

这是一个很容易被忽视的场景。DBA在对MySQL实例进行运维时发现使用xtrabackup备份恢复从库,跟主库建立复制关系时报复制出错,提示xid不存在。这个问题个感觉是不是因为前述的bug(xa prepare命令返回后redo未落盘)导致:xa prepare返回后刚好开始进行xtrabackup备份,由于此时redo可能还未落盘,也就是说引擎层事务并不是prepare状态,导致事务被回滚,从而出现xid找不到的情况,从Binlog文件和备份保留的gtid信息也能够确定xa prepare在gtid集合中,而xa commit是在起复制之后。但再仔细一想,xtrabackup进行备份的时候,是会调用“FLUSH NO_WRITE_TO_BINLOG ENGINE LOGS”刷redo的,所以,在xtrabackup场景下xa prepare返回redo未落盘不是问题。那么问题有出在哪里呢?就在陷入绝境的时候,谷歌助攻了一把,腾讯有个兄弟反馈xtrabackup的时候xa prepare真会出问题,简单说来就是xtrabackup加的全局锁无法阻止xa prepare,也就是说xtrabackup进行备份的时候,xa prepare可以正常执行。那么就很容易理解了。详细分析可参考该文献qkxue.net/info/207119/M。反馈给DBA,并详细解释了下,确实让人信服。但我还是不放心,于是用InnoSQL 5.7.20版本试了下,结果却是执行“FLUSH NO_WRITE_TO_BINLOG ENGINE LOGS”后,xa prepare卡住了!!!!基于链接中的patch看了5.7.20源码,发现这个bug已经修复了。

所以,问题又重新回到原点。但终在DBA的助攻下找到了真正的原因。这其实不是什么bug,而是跟xtrabackup的redo-log选项有关,默认该选项为false,意味着xtrabackup在恢复的时候会把已经完成xa prepare的事务也回滚掉,意不意外??但确实就是这样的,详见日志记录:

后,对比了下xtrabackup和MySQL的代码,官方的代码如下:

xtrabackup代码如下:

也就是说,xtrabackup修改了InnoDB引擎的故障恢复逻辑,扫描undo的时候,对于prepared状态的事务,会将其设置为active状态。所以,对于使用xtrabackup进行MySQL从库重建时一定要留意redo-log这个参数。

其他相关问题分析

group commit prepare优化

本文所述的个XA事务bug,与MySQL的group commit(组提交)机制有密切的关系。在分析了XA事务的bug后,还有一个关于组提交机制的疑问:上面提到,在prepare阶段设置了HA_IGNORE_DURABILITY,不会刷redo到磁盘上,那么这会不会导致普通事务在Commit时写了Binlog,由于redo没有刷盘导致数据丢失呢?回答显然是不会。这就是MySQL 5.7的group commit prepare优化。即在prepare的时候不刷redo,减少一次独自刷盘操作(与上面的有所不同,这里的HA_IGNORE_DURABILITY是在MYSQL_BIN_LOG::prepare设置的),而是选择在group comit的Binlog flush stage进行redo持久化,即在Binlog持久化前完成redo持久化(此时刷盘,可以将多个事务的redo一起刷掉,大大提高了效率),这样就确保了数据不丢失,写了Binlog就一定能够通过redo恢复事务数据,同时又进一步提高了group commit的性能。对应的调用栈信息如下所示:


semi-sync下是否会有此Bug?有

上面我们已经了解在传统的异步复制模式下,该Bug可能会导致主从的XA事务状态不一致。那么在semi-sync场景下,该Bug是否仍存在。这个问题的本质是与semi-sync的Binlog传输和ACK机制有关。

首先,需要了解的是主上的dump线程何时将事务的Binlog发送给Slave。dump线程通过Binlog_sender::send_binlog(IO_CACHE *log_cache, my_off_t start_pos)来执行从start_pos开始的Binlog dump操作。其中会调用Binlog_sender::get_binlog_end_pos(IO_CACHE *log_cache)来循环等待Binlog文件的位置被更新,终是通过信号量mysql_cond_t update_cond来实现。而在进行事务组提交MYSQL_BIN_LOG::ordered_commit中,有2处调用了update_binlog_end_pos(my_off_t pos)用来唤醒在update_cond上的线程。这两处分别在flush stage和sync stage,具体在哪个stage唤醒等待位置更新的线程由参数sync_binlog决定,如果为1,则表示每完成一次事务组提交都需要进行Binlog持久化,对应得是在sync stage阶段完成Binlog持久化后更新Binlog位置为本次组事务提交写入偏移,从而唤醒dump线程向Slave传输本次事务组提交产生的Binlog。若sync_binlog不为1,则在flush stage阶段,将参与组提交的事务的Binlog写入到Binlog文件后,即更新Binlog位置,也就是说,Binlog无需先持久化再传给Slave。对于所讨论的这个Bug,由于在binlog_prepare中会调用MYSQL_BIN_LOG::ordered_commit,所以在完成binlog_prepare后,不管sync_binlog设置为何值,此时Binlog均可能已经传给Slave。crash只会影响主上的Binlog文件的数据一致性。

其次,再来了解semi-sync的ACK机制。简单来说,若主从均开启了semi-sync,那么在Slave连上主库复制Binlog时,主库能够通过通过连接信息获取该Slave是否开启了semi-sync,进而在dump线程给Slave发送Binlog的某些场景(比如事务提交时。注意,并不是每次Binlog传输都需要Slave提供ACK)下设置ACK回复的状态。若主库需要Slave回复ACK,则可能在sync stage或者commit stage来等待Slave的ACK信息,具体在哪个stage等待由参数rpl_semi_sync_master_wait_point决定。如果设置为AFTER_SYNC,则在sync stage完成Binlog持久化后等待ACK。

若是AFTER_COMMIT,则在完成引擎层提交后再等待Slave的ACK。对于所讨论的Bug,由于在binlog_prepare中会调用MYSQL_BIN_LOG::ordered_commit,innobase_xa_prepare在binlog_prepare之后,且redo log持久化标志为HA_IGNORE_DURABILITY,所以,即使设置为AFTER_COMMIT模式,仍然会有问题。

MGR下是否会有此Bug?

不管异步复制还是semi-sync,Binlog都是由主上的dump线程发送,Slave的io线程接收的。但MGR模式下并非如此,本地事务的Binlog是通过before_commit这个hook发送到底层的xcom/paxos协议上。

也就是说,before_commit的时候,事务的Binlog还没有写入到Binlog文件,更谈不上是持久化。所以,MGR下不会出现事务的Binlog信息已经发送给Slave,但引擎层还未Prepare的情况。这也就是避免了在进行XA PREPARE时完成了binlog_prepare在innobase_xa_prepare的时候crash,reboot后出现的主从XA事务状态不一致问题,因为前面已经提到,此时该XA PREPARE操作会在引擎层被回滚掉。由于在binlog_prepare时,Binlog已经走了paxos协议,所以其他节点会正常执行XA PREPARE,本节点在MGR recv阶段,会基于gtid_executed信息,从其他节点拉取本节点还未执行的事务Binlog信息进行catchup,从而补上因为crash而回滚的XA PREPARE。


总结

本文分析了MySQL 5.7上发现的XA事务相关bug,有跟组提交相关的,也有跟xtrabackup备份恢复相关的,通过详细分析,相信读者在遇到类似问题的时候可以很快找到原因所在。了解了问题的起因后,也能够在使用XA事务的时候进行规避。本文还介绍了MySQL组提交优化和 5.7 XA事务prepare和commit的代码实现,对MySQL源码感兴趣的同学欢迎交流探讨。

分享好友

分享这个小栈给你的朋友们,一起进步吧。

数据库内核开发
创建时间:2019-12-11 16:43:06
网易数据库内核技术专家 8年多数据库和存储系统开发经验,《MySQL内核:InnoDB存储引擎 卷1》作者之一,申请技术专利10+,已授权5+。曾主导了网易公有云RDS、MongoDB等数据库云服务建设 现负责网易MySQL分支InnoSQL开发和维护。专注于数据库内核技术和分布式系统架构,擅长分析解决疑难问题。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • 温正湖
    栈主

小栈成员

查看更多
  • xzh1980
  • else
  • Jack2k
  • at_1
戳我,来吐槽~