有那么一坨代码,他虽然在那里,我们却很少用到。。那就是MySQL的多XA事务引擎特性支持。。本周我们来探讨下TC LOG MMAP的代码实现。由于工作的关系,这块很少涉及,正好趁着周末补补漏。
本文分析的代码基于支持Tokudb的MySQL5.6.16 和MySQL 5.7.5;原因是官方MySQL还不包含多个事务引擎,因此代码压根走不到TC LOG MMAP. 但是MySQL5.7又Fix掉了相关的XA BUG (bug#47134),因此本文贴出的代码部分以MySQL5.7.5为主,调试部分以我们支持TOKUDB的MySQL5.6.16内部分支为主。
1.准备工作
为了能够使用到TC LOG MMAP,我们需要禁止binlog和gtid。 如果仅仅关掉binlog,但gtid没有关闭,也会阻止实例启动
2.实例启动时的初始化
在实例启动时,会选择使用哪种XA方式,默认的就是BINLOG和ENGINE做XA,如果BINLOG禁止了,则使用引擎本身做XA
相关代码(sql/mysqld.cc: init_server_components):
4011 if (total_ha_2pc > 1 || (1 == total_ha_2pc && opt_bin_log))
4012 {
4013 if (opt_bin_log)
4014 tc_log= &mysql_bin_log;
4015 else
4016 tc_log= &tc_log_mmap;
4017 }
4018 else
4019 tc_log= &tc_log_dummy
其中total_ha_2pc代表了支持了二阶段提交(2PC)的引擎数目,在初始化各个存储接口时(函数ha_initialize_handlerton),如果发现引擎的prepare接口函数被定义了,就会给total_ha_2pc++; 注意binlog模块也算是支持2PC的引擎
#不存在支持2PC的引擎,或者只有1个支持2PC的引擎且没有打开Binlog时,使用tc_log_dummy做协调者;对应类TC_LOG_DUMMY
#打开binlog时,使用mysql_bin_log做2PC协调者; 对应类MYSQL_BIN_LOG
#没有打开binlog,且存在超过两个支持2PC的引擎时,使用tc_log_mmap做协调者;对应类TC_LOG_MMAP
这三个类对象皆继承自TC_LOG,关系图如下:
对于TC_LOG_DUMMY,直接调用引擎接口做PREPARE/COMMIT/ROLLBACK,基本不做任何协调;
对于MYSQL_BIN_LOG,这算是我们最熟悉的部分了,实际上binlog接口不做prepare,只做commit,也就是写BINLOG到文件中。所有写入Binlog的事务,在崩溃恢复时,都应该能够提交。
本文的重点是TC_LOG_MMAP,因此其他两类不在这里展开阐述。
注意下文所有的讨论前提,都是基于BINLOG已经被彻底关闭!!!
3. TC LOG初始化
在确定了使用tc_log_mmap后,就会调用TC_LOG_MMAP::open 打开/初始化日志文件,文件名默认为tc.log,存放在$DATA目录下,文件主要用来持久化维护事务的XID信息
当tc.log文件不存在时,就创建该文件,并将文件大小初始化到24KB
当tc.log文件存在时,表示上次使用的是tc_log_mmap,那么就需要走崩溃恢复逻辑。
对tc.log的操作,使用mmap的方式映射到内存中:
data= (uchar *)my_mmap(0, (size_t)file_length, PROT_READ|PROT_WRITE,
MAP_NOSYNC|MAP_SHARED, fd, 0);
tc.log 以PAGE来进行划分,每个PAGE大小为8K,至少需要3个PAGE,初始化的文件大小也为3个PAGE(TC_LOG_MIN_SIZE),每个Page对应的结构体对象为st_page,因此需要根据page数,完成文件对应的内存控制对象的初始化。
在完成st_page初始化后,如果需要进入崩溃恢复逻辑(启动实例时tc.log文件已经存在),则调用TC_LOG_MMAP::recover进入崩溃恢复处理 (后文讨论)
初始化第一个page的header,写入magic number以及当前的2PC引擎数(也就是total_ha_2pc)
下图描述了tc.log的文件结构:
4.事务Prepare/Commit
假定我们做了如下操作序列,其中sbtest1为Innodb表,sbtest2为tokudb表;
BEGIN;
update sbtest1 set k =k+1 where id = 2;
update sbtest2 set k =k+1 where id = 2;
commit;
当遇到第一个UPDATE时,会去注册XA,设置XID。backtrace如下:
mysql_update
|–>lock_table –>mysql_lock_tables–>lock_external–>handler::ha_external_lock–>ha_innobase::external_lock–>innobase_register_trx–>trans_register_ha
if (thd->transaction.xid_state.xid.is_null())
thd->transaction.xid_state.xid.set(thd->query_id);
XID根据会话当前的query_id来设置,由于query id是递增的,因此能保证xid唯一性。
(如果是先UPDATE TOKUDB表,则是另外一个trace:lock_external–>handler::ha_external_lock–>ha_tokudb::external_lock–>ha_tokudb::create_txn-->trans_register_ha)
总之,不管先更新哪个表,xid只注册一次,在事务完成前都不会变化了。
当事务提交COMMIT时,实际上是分两步走的,第一步是Prepare,第二步是Commit;
mysql_execute_command–>trans_commit–>ha_commit_trans:
if (!trn_ctx->no_2pc(trx_scope) && (trn_ctx->rw_ha_count(trx_scope) > 1))
error= tc_log->prepare(thd, all);
当事务中使用超过两个XA事务引擎时,就会走到XA PREPARE的逻辑,如果只使用一个引擎,是无需PREPARE的。
TC_LOG_MMAP::prepare的实现很简单,就是直接调用引擎的PREPARE接口函数(ha_prepare_low)
通常事务引擎都会在引擎层写一个PREPARE的REDO 日志,例如TOKUDB和INNODB。
当完成Prepare后,就进入Commit阶段,这里会稍微复杂点。XA事务COMMIT的入口为TC_LOG_MMAP::commit, 分为以下几个步骤:
Step 1: 获取XID
my_xid xid= thd->transaction.xid_state.xid.get_my_xid();
在遇到第一个UPDATE时XID已经设置好了。对应unsigned longlong的query id
Step2: 记录XID到tc.log中
if (all && xid)
if (!(cookie= log_xid(thd, xid)))
DBUG_RETURN(RESULT_ABORTED); // Failed to log the transaction
为了将XID写入到TC日志中,首先需要选择一个PAGE。
TC_LOG_MMAP对PAGE的管理,采用一种POOL结构,在刚启动时的初始化时,active指针指向第一个Page, pool指针指向第二个page, pool_last_ptr指向最后一个page结尾.
通过active、pool、pool_last_ptr三个指针实现了PAGE POOL的管理。
记录XID的流程如下:
1)持有LOCK_tc锁
2)获取active page
–#如果当前active的page中没有空闲空间,则condition wait (COND_active)
–#如果没有active的page,则从POOL中取一个PAGE(TC_LOG_MMAP::get_active_from_pool),根据注释描述,有两种策略:
–# take the first from the pool
–# if there’re waiters – take the one with the most free space.
–# if there’re waiters – take the one with the most free space.
然后将active指针指向选择的page
3)将xid写入到active page中 (store_xid_in_empty_slot),Page状态被设置成PS_DIRTY。
同时返回一个cookie:
cookie= (ulong)((uchar *)p->ptr – data_arg). 通过cookie,我们可以快速定位到写入的page位置
4)如果syncing指针被设置,表示有别的线程正在sync文件,等待直到其完成(wait_sync_completion)
注意这里可能别的线程sync的正是当前page,因此再次检查st_page::state是否为PS_DIRTY。如果不是,就无需去sync 当前page了,直接返回;
5)将active的page指针赋值给syncing,同时设置active为NULL.
6)释放LOCK_tc锁。
7)写syncing的page,刷到磁盘,调用函数TC_LOG_MMAP::sync
完成sync后,持有LOCK_tc,将该page丢到POOL末尾,唤醒可能在step4等待的线程。syncing被重置为NULL.
可以看到,在写一个XID的过程中,PAGE经历了,从ACTIVE ==> SYNCING ==> POOL的三种状态转变。
Step3: 到引擎层提交事务(ha_commit_low)
不赘述,各个引擎各自实现。
Step4: 重置XID.
if (cookie)
if (unlog(cookie, xid))
DBUG_RETURN(RESULT_INCONSISTENT); // Transaction logged, committed, but not unlogged.
调用函数TC_LOG_MMAP::unlog,前面提到的cookie这里就起到作用了,我们可以直接定位到PAGE的内存位置:
PAGE *p= pages + (cookie / tc_log_page_size);my_xid *x= (my_xid *)(data + cookie);
将在记录的XID设置为0,同时递增st_page::free,表示已经释放了一个空闲的slot。
5.崩溃恢复
崩溃恢复时,我们需要所有已经记录的XID都要COMMIT掉。
函数TC_LOG_MMAP::recover
和BINLOG做XA Recover类似,TC_LOG_MMAP也是在崩溃恢复时,读取记录在tc.log文件中的XID;所有已经记录的XID对应的事务,在引擎层都需要COMMIT