内存泄漏问题
解决了slave 回放xa 事务的问题,业务顺利部署MyRocks,但上线不久后发现有内存泄漏。
线下也很容易模拟出来。rocksdb_block_cache_size=4G,利用sysbench_xa 32线程跑update_index测试,top抓取的内存消耗:
show engine rocksdb status显示rocksdb_block_cache和memtable的总大小才1G多,内存泄露明显。show engine innodb status 显示有大量处于not started状态的trx_t对象存在,线上都是rocksdb表,为什么存在这么多innodb的事务对象?
同时DBA在利用mysqladmin shutdown slave的时候反映无法shutdown。我们线下利用sysbench_xa跑了一会后执行shutdown,MySQL的error.log有下面的输出:
(有关无法shutdown的问题在下一节详细介绍。)
内存泄漏与这么多遗留的trx_t有很大的关系,为什么会有这么多innodb trx_t的对象?在操作rocksdb表的过程中难道涉及到了某些innodb表?如果是涉及到了某些innodb表,又是什么操作导致了trx_t泄漏呢?调试代码发现这些trx与系统表更新有关,主要是slave复制过程中更新下面这些表:
gtid_executed
slave_relay_log_info
这些表都是innodb表,打开这些表时会创建trx_t对象,但这些对象没有立刻释放,从innodb引擎角度看,trx_t对象是可以复用的,每个连接复用一个trx_t对象,等连接断开后再回收trx。这些trx_t对象为什么会被泄漏(在无法shutdown问题这一节中会详细介绍)。这是因为xa start的时候是所有引擎参与deattach自己的事务对象,xa prepare的时候只有实际参与的引擎reattach回自己的事务对象,这样的逻辑会导致没有innodb参与prepare时innodb trx无法reattach,从而导致对象被泄漏。代码中增加了xa-prepare-memleak.patch(patch在文章末尾给出)内存泄漏得以解决,利用sysbench_xa测试发现内存消耗平稳:
解决了slave 无法回放xa事务问题,内存泄漏问题也缓解了,延迟从库稳得以稳定运行。延迟从库稳定运行了一段时间后,在日常的巡检中发现个别实例内存消耗有点高,这又让人很困惑,内存泄漏问题如果没有个大概的方向线下模拟很难复现,还是从现场找原因。排查了一圈,发现这些实例上存在业务的innodb表,这也正常,业务方有时候会临时创建innodb表,难道混合引擎会导致内存泄漏。为了验证这个猜测,线下利用sysbench_xa模拟,在没有xa事务的情况下没有内存泄漏,在有xa事务的情况下,内存泄漏明显,看起来又是与xa事务有关。
xa-prepare-memleak.patch不能解决这种内存泄漏。混合引擎场景导致的内存泄漏通过线下模拟也是与innodb trx_t对象有关系,实验中发现存在大量的处于not started 状态的 trx_t对象,shutdown也是存在大量的innodb活跃事务,可以确认又是trx_t对象泄漏,这次的trx_t泄漏又是什么操作导致的呢?
上文介绍过innodb 的 trx_t对象是在每个连接中复用的,调试发现每次在打开表时会判断trx_t是否存在,如果不存在则创建trx_t对象,如果存在就会复用存在的trx_t,代码如下图所示:
trx_t对象的回收是在连接断开后执行的,innodb_close_connection函数执行下图函数回收trx_t对象。
innodb 引擎xa 事务 的 trx_t的生命周期如下:
(1)在start slave后因为会读取slave_worker_info表信息会创建trx(标记为trx1),利用gdb调试发现Innodb引擎在xa start detach 的trx也就是这个trx1。
(2)xa start 之后innodb执行sql时,由于trx1已被detach,所以会再分配一个trx(标记为trx2),xa prepare的时候调用ha_prepare后,trx的状态变为TRX_STATE_PREPARED,接着会调用innodb_replace_trx_in_thd, 先把trx2与thd
解除关联,并且从trx_sys->mysql_trx_list中移除,trx2还在trx_sys→rw_trx_list中,trx2处理完之后,reattach回trx1,trx1重新与thd保持关联。
(3)xa commit xid的时候,遍历trx_sys->rw_trx_list中的trx→xid查找与该提交的xid相同的trx也就是trx2,找到后进行提交,提交结束后释放该trx(trx2)。
上述步骤在只操作innodb表时逻辑没有问题,但是在混合引擎的场景下,可能就会出问题。假设有下面这种场景:
- rocksdb 表的 xa事务;
- innodb表的dml普通事务;
- rocksdb 表的xa事务;
之前介绍过了,MySQL解决xa prepare与xa commit分离的方法是所有引擎在xa start 的时候都deattach掉自己的事务对象,xa prepare的时候只有实际参与的引擎去re-attach 自己的事务对象。按照这种解决方法我们走一遍上面的场景:
rocksdb表的xa事务开始执行,xa start的时候innodb引擎也会deattach trx,但是在xa prepare的时候只有rocksdb引擎re-attach自己的事务对象,innodb的trx泄漏。
innodb表执行普通的dml,因为上一步trx已经泄漏了,需要再重新分配一个trx,并且在事务提交后该trx没有马上回收,同一个连接会复用。
又来了一个rocksdb表的xa 事务,跟步一样,第二步中分配的innodb trx又被泄漏。
这种场景也是线上存在的场景,线下利用sysbench_xa模拟也是这种场景。同时线下实验也验证过如果全是xa事务不存在内存泄漏。
xa-prepare-memleak.patch如下:
diff --git a/mysql-5.7.20/sql/rpl_gtid_persist.cc b/mysql-5.7.20/sql/rpl_gtid_persist.cc
index df5457e..5c74f04 100644
--- a/mysql-5.7.20/sql/rpl_gtid_persist.cc
+++ b/mysql-5.7.20/sql/rpl_gtid_persist.cc
@@ -166,22 +166,23 @@ bool Gtid_table_access_context::init(THD **thd, TABLE **table, bool is_write)
m_tmp_disable_binlog__save_options= (*thd)->variables.option_bits;
(*thd)->variables.option_bits&= ~OPTION_BIN_LOG;
}
-
+
if (!(*thd)->get_transaction()->xid_state()->has_state(XID_STATE::XA_NOTR))
{
- /*
- This type of caller of Attachable_trx_rw is deadlock-free with
- the main transaction thanks to rejection to update
- 'mysql.gtid_executed' by XA main transaction.
- */
+
DBUG_ASSERT((*thd)->get_transaction()->xid_state()->
has_state(XID_STATE::XA_IDLE) ||
(*thd)->get_transaction()->xid_state()->
has_state(XID_STATE::XA_PREPARED));
-
- (*thd)->begin_attachable_rw_transaction();
}
+ /*
+ This type of caller of Attachable_trx_rw is deadlock-free with
+ the main transaction thanks to rejection to update
+ 'mysql.gtid_executed' by XA main transaction.
+ */
+ (*thd)->begin_attachable_rw_transaction();
+
(*thd)->is_operating_gtid_table_implicitly= true;
bool ret= this->open_table(*thd, DB_NAME, TABLE_NAME,
Gtid_table_persistor::number_fields,
diff --git a/mysql-5.7.20/sql/rpl_info_table.cc b/mysql-5.7.20/sql/rpl_info_table.cc
index 204dd69..f1029db 100644
--- a/mysql-5.7.20/sql/rpl_info_table.cc
+++ b/mysql-5.7.20/sql/rpl_info_table.cc
@@ -172,6 +172,8 @@ int Rpl_info_table::do_flush_info(const bool force)
tmp_disable_binlog(thd);
thd->is_operating_substatement_implicitly= true;
+ thd->begin_attachable_rw_transaction();
+
/*
Opens and locks the rpl_info table before accessing it.
*/
@@ -255,6 +257,17 @@ end:
Unlocks and closes the rpl_info table.
*/
access->close_table(thd, table, &backup, error);
+ if (thd->is_attachable_rw_transaction_active())
+ thd->end_attachable_transaction();
+
thd->is_operating_substatement_implicitly= false;
reenable_binlog(thd);
thd->variables.sql_mode= saved_mode;