标签:des http ar io 使用 sp for strong on
关于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。
MySQL外部XA的正常处理流程,这里不准备介绍,可以参考[1][2][3]。接下来我重点描述一下MySQL外部XA的崩溃恢复流程,毕竟此流程跟应用程序如何正确使用外部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();
通过以上的分析,可以总结出:
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。
上面提到,MySQL有外部XA与内部XA,内部XA对应的xid由MySQL内部产生,有特定的格式:
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实例上的状态可能不一致(部分节点提交;部分节点回滚)。
前面提到了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的方式分析:
上面说到,由于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的代码,我找到了答案:
同样还是在MariaDB WL#164[6]中,提到了遍历binlog的一种优化,目前,InnoDB redo log在commit日志中已经记录了对应的Binlog日志的(文件名,位置)信息。只要将此信息返回,就可以从指定位置开始遍历Binlog,如此一来,使用更大的Binlog文件,也不会影响crash recovery时,读取Binlog文件的性能。
MySQL外部XA可以用在分布式数据库代理层,例如开源的代理工具:ameoba[4],网易的DDB,淘宝的TDDL,B2B的Cobar等等。
通过MySQL外部XA,这些工具可以提供跨库的分布式事务。当然,这些工具也就成了外部XA事务的协调者角色。在crash recover时控制悬挂事务是全局commit,或者rollback。
在crash recover之后,外部应用程序可能会遇到以下几种情况:
通过前面的分析,可知应用程序配合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 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。
从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] 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事务还不好用,问题多,性能也不好,想想使用其他替代方案吧。
标签:des http ar io 使用 sp for strong on
原文地址:http://www.cnblogs.com/gxldan/p/4170261.html