MySQL 外部XA及其在分布式事务中的应用分析

  1. XA原理

关于XA,分布式事务处理的原理,可见[3];关于MySQL XA的说明,可见[1][2]。

 

MySQL XA分为两类,内部XA与外部XA;内部XA用于同一实例下跨多个引擎的事务,由大家熟悉的Binlog作为协调者;外部XA用于跨多MySQL实例的分布式事务,需要应用层介入作为协调者(崩溃时的悬挂事务,全局提交还是回滚,需要由应用层决定,对应用层的实现要求较高);

 

本文,假设读者已经知道MySQL外部XA的使用,而将重点放在MySQL如何处理外部XA的crash recover,以及面对不同的crash recover的情形,应用程序如何处理,才能够保证分布式事务的一致性。最后,本文简单分析一下目前MySQL外部XA支持存在的问题,以及可选的解决方案。

 

源代码分析基于MySQL 5.1.49,MySQL 5.5.16。

 

  1. MySQL处理流程

    1. MySQL 外部XA –正常处理流程

MySQL外部XA的正常处理流程,这里不准备介绍,可以参考[1][2][3]。接下来我重点描述一下MySQL外部XA的崩溃恢复流程,毕竟此流程跟应用程序如何正确使用外部XA息息相关。

  1. MySQL外部XA –崩溃恢复流程

若一个运行外部XA事务的MySQL节点发生崩溃,那么其重启之后的崩溃恢复,涉及到外部XA处理的流程如下:

Crash recover:

 

// 1.    读取binlog文件,将文件中的xid存入commit_list hash表

//     顾名思义,所谓的commit_list,就是说此list中对应prepare状态的xid

//    在崩溃恢复过程中均可以被提交,而不在commit_list中的xid,均须回滚

//     binlog中的xid,都是属于内部xid,由MySQL产生,用于内部XA

Log.cc::TC_LOG_BINLOG::recover

 

// 2.    遍历底层所有的事务引擎,收集处于XA_PREPARED状态的所有xid

//    这些xid列表,既包括内部xid,也包括外部xid,存储引擎内部不做区分

Handler.cc::ha_recover(commit_list)

 

// 执行各引擎层面提供的recover方法,收集所有的处于prepared状态的xid

// 根据xid分类:

// 3.    若xid属于内部xid,那么在commit_list中查找此xid,

//    若存在,则提交此xid对应的事务;否则,回滚此事务

// 4.    若xid属于外部xid,那么则将xid插入xid_cache hash表

//    xid_cache中的所有xid,将会通过xa recover命令返回,等待外部程序决策

Handler.cc::xarecover_handlerton

 

    // 5.    收集InnoDB引擎中,处于prepare状态的所有xid,并返回

    got = hton->recover(innobase_xa_recover)

 

    my_xid x = info->list[i].get_my_xid();

    if (!x)

        // 若当前为外部xid,那么将xid插入xid_cache hash表

xid_cache_insert(&xid_cache, x);

    else

        if (x in commit_list)

            // 若当前为内部xid,同时此xid在binlog中存在,则提交

            hton->commit_by_xid();

        else

            // 若当前为内部xid,同时此xid在binlog中不存在,则回滚

            hton->rollback_by_xid();

 

通过以上的分析,可以总结出:

  • MySQL内部,会对xid做区分。内部xid有MySQL自己产生(MySQL内部xid格式,将在本文下面给出),用于多引擎间事务的一致性;外部xid由应用程序给出,用于跨多MySQL实例的分布式事务。但是存储引擎层不做区分(区分在MySQL上层)。

     

  • crash recover时,存储引擎负责将引擎内部,处于prepare状态的事务收集,并返回MySQL上层。

     

  • Binlog作为内部XA的协调者[5],在binlog中出现的内部xid,在crash recover时,由binlog负责提交;在binlog中未出现的xid,由binlog负责回滚。(这是因为,binlog不进行prepare,只进行commit,因此在binlog中出现的内部xid,一定能够保证其在底层各存储引擎中已经完成prepare)。

     

  • 外部XA事务的xid,在crash recover过程中仅仅是插入xid_cache中,而不做其他处理。等到用户发起xa recover命令时,将xid_cache中处于prepare状态的xid返回。

     

  • xa recover命令的流程处理如下。

 

xa recover命令处理流程:

 

sql_parse.cc::mysql_execute_command

    case SQLCOM_XA_RECOVER:

        mysql_xa_recover();

            // 遍历xid_cache,找出其中的状态处于XA_PREPARED的事务,发送客户端

            while (xs = hash_element(&xid_cache,))

                if (xs->xa_state == XA_PREPARED)

                    protocol->write();

 

根据xa recover命令收集到的各MySQL实例返回的xid列表,然后再对比应用程序端日志,决定这些xid,哪些全局commit,哪些rollback。

 

由于测试中只有一个MySQL实例,因此此时可以直接选择commit处于prepare状态的xid。

 

  1. MySQL内部xid格式

上面提到,MySQL有外部XA与内部XA,内部XA对应的xid由MySQL内部产生,有特定的格式:

  • MySQL内部xid格式:    MYSQL_XID_PREFIX + server_id + my_xid

    MYSQL_XID_PREFIX:    MySQLXid(源码写死)        8 bytes

    server_id:                MySQL实例的id,ulong,        4 bytes

    my_xid:                内部自增序列,ulonglong,    8 bytes

     

    MySQL内部xid由以上3部分组成,总长度为20。

    判断是否为内部xid的代码如下:

    gtrid_length == MYSQL_XID_GTRID_LEN

    &&bqual_length == 0

    &&!memcmp(data, MYSQL_XID_PREFIX, MYSQL_XID_PREFIX_LEN)

     

    其中:MYSQL_XID_GTRID_LEN = 20;MYSQL_XID_PREFIX_LEN = 8;

     

     

    例如:“MySQLXid 0004“

    server_id = ”;my_xid = 4

     

    因此,使用时应该注意,不要在外部构造这种形式的xid,否则MySQL就会将内部xid与外部xid混淆。

     

    在测试中,我构造了一个外部xid = ‘MySQLXidxxxx00100000′,长度为20 bytes,前八个字符为‘MySQLXid’。在事务完成xa prepare之后,关闭MySQL数据库。MySQL在crash recover时,直接将此xid认为是内部xid,并在内部由Binlog直接rollback此事务,导致使用xa recover命令无法看到任何prepare状态的xa事务。

     

    但是,反过来考虑,若是应用程序本身不想处理悬挂事务,那么将外部xid构造成内部的形式不失为一种较好的策略,由binlog来负责处理悬挂事务的提交与回滚。付出的代价则是:崩溃时,未提交事务在各个MySQL实例上的状态可能不一致(部分节点提交;部分节点回滚)。

 

  1. MySQL 崩溃恢复& Binlog

前面提到了MySQL外部XA的崩溃恢复流程。在本小节我们简单分析一下崩溃恢复过程中的Binlog文件的读取问题。

 

通过跟踪TC_LOG_BINLOG::open函数,发现在crash recover过程中,MySQL全量读取最后一个Binlog文件,这与MariaDB WorkLog#164:Extend crash recovery to recover non-prepared transactions from binlog[6]中的说法一致:...The existing scan always scans the full last binlog file, and we should keep this...

 

但是这样就带来一个疑问:

为什么仅仅全量读取最后一个Binlog文件就可以呢?如果最后一个binlog文件很短,如何保证底层引擎处于prepare状态的事务不会出现在前一个Binlog文件之中?

 

回答这个疑问,需要从目前MySQL写Binlog与底层存储引擎(InnoDB)写redo log的方式分析:

  1. 同一事务只能写到同一个Binlog文件中,不能跨文件。
  2. 为了保证底层引擎Commit顺序与Binlog顺序一致,目前MySQL+InnoDB不支持group commit(新版的Precona,MariaDB除外),同一时间只有一个事务可以进行提交(内部的XA事务,二阶段提交):InnoDB prepare + Binlog flush + InnoDB commit这一系列操作。因此下一个事务开始进行InnoDB prepare时,前一个事务的系列动作一定结束,事务已经提交。意味着crash recovery时,最多只有一个InnoDB事务处于prepare状态。
  3. 结合1,2可得,最后一个prepare事务一定位于最后的Binlog文件中。

 

上面说到,由于MySQL+InnoDB不支持group commit,因此只读最后一个Binlog是可行的,那么如果是最新版的Percona/MariaDB,已经支持group commit (关于group commit的具体实现,可以参考我的另外一篇短文:MariaDB&PerconaXtraDB Group Commit实现简要分析[7]),那么仍旧读取最后一个Binlog文件是否一样可行呢?

 

答案是肯定的,因为目前Percona/MariaDB的最新版本实现中,仍旧采用的是全量读取最后一个Binlog文件的策略,那么此时又是如何保证前一个Binlog文件中所有的日志对应的事务,其在底层InnoDB引擎中已经完成提交动作了呢?

经过阅读MariaDB 5.3.4的代码,我找到了答案:

  1. 同一事务只能写在同一Binlog文件中,不能跨文件,这个要求仍旧保留。
  2. Binlog在进行group commit时,需要统计参与本次group commit的所有内部XA事务的数量(prepared_xids,何用?)。
  3. 若当前Binlog文件已经超出指定的大小,需要切换,那么在切换之前,必须等待当前Binlog文件对应的prepared_xids归零(换句话说,也就是要保证当前Binlog文件中的所有内部XA事务,在存储引擎中全部提交,完成commit & fsync)。如此一来,就能够保证切换到新的Binlog文件之后,老的Binlog文件对应的所以事务,都已经确定提交。
  4. prepared_xids归零前提?要让prepared_xids归零,首先必须将新的Binlog group commit暂停,通过对LOCK_log mutex加锁即可实现(LOCK_log mutex功能可见[7],新的binlog group commit开始前,必须获得此mutex)。
  5. prepared_xids归零操作?Binlog模块(TC_LOG_BINLOG)提供一个unlog方法,该方法每调用一次,对prepared_xids –,直到prepared_xids归零,即可进行binlog文件的切换操作。每个事务,在完成所有的commit步骤(包括底层的存储引擎commit),返回用户之前,调用此方法;若binlog group commit中有事务失败,同样调用此方法。因此,只要binlog中的事务对应的底层引擎全部完成commit,prepared_xids一定为0,也意味着可以切换Binlog文件。
  6. 总结:group commit下的crash recovery,同样只需要遍历最后一个Binlog文件即可。MariaDB在实现group commit的过程中,已经改动binlog的实现,用于支持此方法。

 

同样还是在MariaDB WL#164[6]中,提到了遍历binlog的一种优化,目前,InnoDB redo log在commit日志中已经记录了对应的Binlog日志的(文件名,位置)信息。只要将此信息返回,就可以从指定位置开始遍历Binlog,如此一来,使用更大的Binlog文件,也不会影响crash recovery时,读取Binlog文件的性能。

  1. MySQL 外部XA分析

    1. 作用分析

MySQL外部XA可以用在分布式数据库代理层,例如开源的代理工具:ameoba[4],网易的DDB,淘宝的TDDL,B2B的Cobar等等。

 

通过MySQL外部XA,这些工具可以提供跨库的分布式事务。当然,这些工具也就成了外部XA事务的协调者角色。在crash recover时控制悬挂事务是全局commit,或者rollback。

 

在crash recover之后,外部应用程序可能会遇到以下几种情况:

 

  • 情况一:分布式事务对应的MySQL实例,部分完成prepare,部分未完成prepare。此时直接回滚完成prepare的实例即可。n_prepared <Total Nodes (处于prepare状态的节点数量要小于参与分布式事务的所有节点总数)。

     

  • 情况二:分布式事务对应的MySQL实例,全部完成prepare,未开始进行commit。此时即可提交此事务,也可回滚此事务(根据分布式事务原理,所有节点都完成prepare,应该提交)。n_prepared = Total Nodes。

     

  • 情况三:分布式事务对应的MySQL实例,全部完成prepare,并且部分节点已经完成commit。此时应该提交该事务处于prepare状态的节点。n_prepared < Total Nodes。对比情况三与情况一,仅仅通过prepare节点的数量无法区分,因此应用程序需要在prepare完成之后记录日志(此时,应用程序起着事务协调者(Transcaction Coordinator)的角色,而根据MariaDB WorkLog#132[5]的说法,TC角色是可以进行”middle engine”优化的,不需要prepare过程,所有MySQL节点xa prepare返回之后,应用程序直接写commit标识即可,然后再对每个MySQL节点进行xa commit操作。),从而用于区分情况一与情况三。

     

  • 情况四:分布式事务对应的MySQL实例,全部完成commit。此时事务已经提交成功,xid不会出现在执行xa recover的任一个节点。不需要特殊处理。

 

  • 情况五:未记录任何prepare日志。那么所有的事务,在各个存储引擎的crash recover时,都会被回滚,不需要外部特殊处理。
  1. MySQL外部XA不足

通过前面的分析,可知应用程序配合MySQL的XA事务功能,能够较好的支持分布式环境下的事务。但是,这个支持并不完美,根据我的分析,有可能会出现以下几个问题:

 

  • 问题一:主备库数据不一致。

    MySQL的主备库的同步,通过Binlog的复制完成。而Binlog是MySQL内部XA事务的协调者,并且MySQL为binlog做了优化——binlog不写prepare日志,只写commit日志。

     

    考虑前面提到的情况二,所有的参与节点prepare完成,在进行xa commit前crash。crash recover如果选择commit此事务。由于binlog在prepare阶段未写,因此主库中看来,此分布式事务最终提交了,但是此事务的操作并未写到binlog中,因此也就未能成功复制到备库,从而导致主备库数据不一致的情况出现。

     

    在MySQL 5.5.16版本中做过测试,这个问题实际存在。crash recover之后,对xa recover返回的事务运行xa commit,对应事务提交,但是操作并未写入binlog,因此无法复制到备库。

     

    那么是否回滚所有prepare的事务,就可以避免此问题呢?结论是仍旧不行,不仅不能解决问题一,甚至可能引起问题二。

 

  • 问题二:同一事务,在各参与节点,最终状态不一致(部分提交,部分回滚)。

    若回滚所有prepare状态的分布式事务,会产生问题二。考虑情况三(所有节点完成prepare,部分节点完成commit),该分布式事务对应的节点,部分已经提交,无法回滚,而部分节点回滚。最终导致同一分布式事务,在各参与节点,最终状态不一致。

 

  • 问题三:源码级别问题。MySQL 5.1.49源码对于外部XA事务处理存在bug,在MySQL 5.5.16版本中,此bug已经被fix。经过验证发现,在我已下载的MySQL 5.1.61与之后的所有版本,此bug均已经被fix。

    在MySQL 5.1.49中,所有xa recover返回的外部xid,都不能被提交。原因如下:

     

    当运行xa commit ‘xid_name’命令时,MySQL会判断当前xid_name的错误信息,若存在错误信息,那么就在内部将xa commit命令强制转换为xa rollback。xid_name的状态存于xid_cache中,在crash recover阶段,由函数Handler.cc::xarecover_handlerton调用xid_cache_insert(&xid_cache, x)函数完成插入。MySQL 5.1.49在实现xid_cache_insert函数有bug。

                …

    xs->xa_state=xa_state;

            xs->xid.set(xid);

            xs->in_thd=0;

            xs->rm_error=0;

                res=my_hash_insert(&xid_cache, (uchar*)xs);

                    …

    MySQL 5.1.49中,缺少了xs->rm_error =0这一行,未初始化rm_error,导致xa commit时判断出错,无法commit。MySQL 5.5.16已经fix此bug,加上了黑色这一行的初始化,应用程序可以xa commit。

  1. 不足的解决方案

从MySQL外部XA不足的分析可以看出,除了实现bug之外,产生其余两个问题的最大原因,还是在于MySQL针对binlog做的”middle engine”优化,binlog的prepare不写日志。在MySQL内部XA事务中,这个优化是可行的,因为Binlog本身的角色就是事务协调者(Transaction Coordinator),事务协调者可以不进行prepare [5]。

但是对于MySQL外部XA事务,Binlog已经不是事务协调者的角色,其也是一个参与者,或者说是Resource Manager。因此Binlog的prepare日志是不可省略的。

为了解决MySQL外部XA事务crash recover过程中出现的问题,我觉得只能修改binlog模块。使binlog模块在正常运行过程中也区分内部XA事务与外部XA事务。内部XA事务可以仍旧沿用现在的方案;而外部XA事务,需要增加写prepare日志的功能,已经crash recover时处理prepare日志的功能。

  1. 参考资料

[1] Sergei Golubchik.Distributed Transaction Processing with MySQL XA

[2] http://dev.mysql.com/doc/refman/5.1/en/xa.html

[3] X/Open.Distributed TP: The XA Specification

[4] 陈思儒. Amoeba

[5] MariaDB WorkLog#132: Transaction coordinator plugin

[6] MariaDB WorkLog#164: Extend crash recovery to recover non-prepared transactions from binlog

[7] 何登成. MariaDB&PerconaXtraDB Group Commit实现简要分析

 

转载来自:http://hedengcheng.com/?p=136

 

 

 

 

 

《高性能MySQL(第二版)》关于分布式XA事务的说明

5.11  分布式(XA)事务

Distributed(XA) Transactions

存储引擎事务在存储引擎内部被赋予了ACID(译注1)属性,分布式(XA)事务是一种高层次事务,它可以利用两段提交的方式将ACID属性扩展到存储引擎外部,甚至数据库外部。MySQL 5.0及其以上的版本部分支持XA事务。

XA事务需要事务协调员,它会通知所有的参与者准备提交事务(阶段一)。当协调员从所有参与者那里收到"就绪(Ready)"信号时,它会通知所有参与者进行真正的提交(阶段二)。MySQL可以是XA事务的参与者,但不能是协调员。

MySQL内部其实有两种XA事务。MySQL服务器能参与由外部管理的分布式事务,但它内部使用了XA事务来协调存储引擎和二进制日志。

 

5.11.1  内部XA事务

Internal XA Transactions

MySQL内部使用XA事务的原因是服务器和存储引擎之间是隔离的。存储引擎之间是完全独立的,彼此不知道对方的存在,所以任何跨引擎的事务本质上都是分布的,并且要求第三方来进行协调。MySQL就是第三方。假如没有XA事务,跨引擎事务提交需要顺序地要求每个引擎进行提交。这样就会引入一种可能,就是在某个引擎提交之后发生了崩溃,但是另外一个引擎还未提交。这就打破了事务的原则。

如果把记录事件的二进制日志看成一个"存储引擎",那么就能理解为什么即使是单个事务性引擎也需要XA事务。存储引擎把事件提交给二进制日志时,需要和服务器进行同步,因为是服务器,而不是存储引擎处理二进制日志。

当前的XA在性能上有些进退两难。它打破了InnoDB从MySQL 5.0以来的对群体提交(Group Commit)(一种使用单次I/O提交多个事务的技术)的支持,所以会导致了很多fsync()调用。如果二进制日志处于激活状态,那么每个事务都会需要等待日志同步,并且每次事务提交都要求两次日志重写,而不是一次。换句话说,如果想让事务和二进制日志安全地同步,就会要求至少三次fsync()调用。防止其发生的唯一办法就是禁用二进制日志并把innodb_support_xa设置为0。

这样设置无法兼容复制。复制需要二进制日志和XA支持,并且为了尽可能地安全,还须要把sync_binlog设置成1,这样设置就能对存储引擎和二进制日志进行同步。(否则的话,XA支持就没有必要了,因为二进制日志不会被提交到磁盘上)。这是强烈推荐使用带有备用电池的写入缓存的磁盘阵列控制器的一个原因,它能加快fsync()调用并且恢复性能。

下一章将会详细讲解如何配置事务日志及二进制日志。

 

 

5.11.2  外部XA事务

External XA Transactions

MySQL可以参与,但不能管理外部分布式事务。它不支持完整的XA规范。例如,XA规范允许连接运行单个事务中的连接,但是MySQL现在还不能做到这一点。

外部XA事务的开销比内部XA事务更高,这是因为延迟会增加,并且参与者失败的可能性更大。在WAN、甚至是因特网上使用XA,一个常见的问题就是网络性能不可预测。当有不可预测的部分,比如较慢的网络或一个有可能很久都不点击"保存"按钮的用户,最好的选择就是避免XA事务。任何耽搁提交的因素都会有很高的代价,因为它导致的不是单个系统延迟,而是许多系统。

可以用另外的方式设计分布式事务。例如,可以在本地把数据插入队列,然后把它自动地分布成小而快的事务。也可以使用MySQL复制把数据从一个地方搬运到另外一个地方。我们也发现某些使用了分布式事务的应用程序其实根本没必要使用事务。

总的说来,XA事务是一种在服务器之间同步数据的有用的方式。如果因为某些原因,比如不能使用复制或数据更新的性能并不是关键因素,它的效果会不错。

 

 

综上所述,分布式XA事务还不好用,问题多,性能也不好,想想使用其他替代方案吧。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。