MySQL:大并发下TRX_SYS mutex案例分析
最近在处理一个case的时候(版本:5.7.29),通过连续pstack发现存在2个问题导致CPU比较高导致时钟中断比较高,解决其中一个问题后主观描述系统正常了,但是剩下1个问题没有解决,这里集中看看这个问题。
一、问题展示
这个问题大概通过pstack和火焰图以及show engine的mutex等待部分来呈现
1.1 show engine
image.png这里看到TRX_SYS mutex并不是长时间的等待(0秒),而是很短但是可见。
1.2 pstack(pt-pmp格式化)
其中一个pstack展示如下,这里我删除了大部分内容,只留下有价值的部分。
image.png
1.3 火焰图
image.png二、初步分析
很显然从上面的信息可以看出来,purge线程在获取最老的一个read view 用于清理undo和delete flag信息的时候,这个过程耗用了大量的CPU,这个过程是加trx_sys->mutex的,因为trx_sys->mvcc(MVCC) 是当前系统的read view的数据结构,其中包含2个链表结构:
- m_free:read view释放后会优先放到这个链表,可以重用(MVCC::get_view)
- m_views:当前使用中的read view或者auto_commit并且不加锁的只读事务的read view close后放到里面(后面再讨论),对于最老的read view 应该放到其尾部。
而pruge线程需要从m_views的尾部扫描,找到最老的read view,因此需要加trx_sys->mutex,而在分配read view 有些时候需要拿到trx_sys->mutex来维护MVCC的m_views和m_free。
因此出现了堵塞,但是问题是为什么MVCC::get_oldest_view需要这么多的CPU呢?
随即我找了一下问题,发现有人已经遇到过了如下:
貌似BUG状态并没有关闭,然后顺着文章进行一下分析,说不定可以有更多的见解。
三、read view的分配和select的类型
read view对于select 语句来讲非常的重要,其主要是用于判定数据的可见性,如果不可见还要联动undo,因此对于大查询比如select很久的语句,可能purge线程不能清理undo,导致undo巨大,并且数据不能清理掉,否则无法判定可见性。
在当前版本中read view的分配,并不一定是分配可能是重用,我们将纯读取(select)的事务分为3种:
- A:auto_commit且session中两次select没有读写事务
- B:auto_commit且session中两次select有读写事务
- C:非auto_commit的select
而对于一个read view分配正常来讲是需要加trx_sys->mutex,至少包含:
- 从trx_sys->mvcc的m_free中获取一个空闲的read view 或者直接分配内存建立read view
- 获取当前trx中rw 事务的vector数组(trx_sys的rw_trx_ids),用于判定可见性
- 获取当前trx中的事务最大和最小trx_id,用于判定可见性
- 获取当前事务trx的最老的trx_no,用于purge线程使用
- 加入到trx_sys->mvcc的m_views链表的头部
可以看到这一套流程基本上分不开对trx_sys元素的操作,因此需要持有trx_sys->mutex。而前面列举的A/B/C 的情况中:
- A:不需要走任何流程,因为两次select没有读写事务,那么只要重用上一次的read view即可。
- B:需要走 2 3 4 5流程
- C:需要走 1 2 3 4 5 流程
而其主要方式就是在每次select语句结束准备释放read view的时候,先判断这个read view是不是auto_commit的select,如果是就暂时不做维护trx_sys->mvcc链表的操作,让其保存在MVCC的m_views中,只是对read view做一个操作设置其属性m_closed = true,这样就不存在维护trx_sys结构,那么也就不需要trx_sys->mutex。当然如果是非auto_commit的select还是老老实实的释放走加锁释放。这是通过MVCC::view_close函数的第二参数来判定的。
而在分配的时候情况A下,只需要将m_closed设置为false就可以了,继续用这个read view就可以了。而对于情况B还是需要持有trx_sys->mutex的,因为这种情况不能复用了,但是read view存在也就直接初始化一下。对于情况C实打实的关闭read view和重新分配。因此前面列举的A/B/C 的情况中,对于read view的操作trx_sys->mutex加锁情况大概为:
- A:释放,分配都不需要
- B:释放不需要,分配需要
- C:释放,分配都需要
而真正当session断开后A和B的read view 可能才真正释放掉(trx_disconnect_from_mysql)。
因此在A和B的情况下存在一种延迟释放read view的情况,而不同就是A会判断后下一个select 也重用read view,而B会判断后加锁处理重新初始化read view。
四、存在的问题
但是这有一个问题,就是A和B情况下MVCC的m_views链表中read view没有被摘下来,那么在purge线程扫描的时候代码如下:
for (view = UT_LIST_GET_LAST(m_views);
view != NULL;
view = UT_LIST_GET_PREV(m_view_list, view)) {
if (!view->is_closed()) {
break;
}
}
也就是从m_views 链表的尾部开始扫描,如果大量的read view存在其中,且都是不活跃的,那么可能存在扫描大量的read view才找到最老的那个read view,那么持有trx_sys->mutex锁的时间就变得比较大了。可能的情况如下:
- 大并发的小select语句不断的访问,而DML不多那么就可能这样,出现情况A大量的复用read view。
- 大量的session可能跑一个select 就停下来休息一会,那么也会出现情况B而留下的read view,这个时候还没有新分配read view就残留下来了(可能性较大)
然后通过show engine查看本案例中出现过读写事务但是当前没有做读写事务的session,大概如下:
image.png
而正在做读写事务的只有1个session。这些session可能曾今跑过select但是且留下了read view,那么极限情况下可能有4750个read view 残留,那么循环的代价被放大了很多。purge线程的唤醒也是比较频繁的,具体参考
但是这个问题无法解决,很是遗憾,除非修改代码,继而查看8.0的主要代码,貌似也没看到拆分,那么这个问题可能依旧存在,如果遇到可以参考,当然可以限制一下最大session数量(比如1000个session)或者做好读写分离。
如果要测试一下可以随便开几个session,我这里开了4个session,每个做了几个select语句,然后去打印m_views的长度,如下:
(gdb) p trx_sys->mvcc->m_views
$12 = {count = 4, start = 0x33deb78, end = 0x33ded58, node = &ReadView::m_view_list, init = 51966}
如果是4000多个session做过select,可能这里就是4000。
五、 代码部分
class MVCC:
private:
typedef UT_LIST_BASE_NODE_T(ReadView) view_list_t;
/** Free views ready for reuse. */
view_list_t m_free;
/** Active and closed views, the closed views will have the
creator trx id set to TRX_ID_MAX */
view_list_t m_views;
class ReadView:
class ids_t:
/** Memory for the array */
value_type* m_ptr;
/** Number of active elements in the array */
ulint m_size;
/** Size of m_ptr in elements */
ulint m_reserved;
trx_id_t m_low_limit_id;
trx_id_t m_up_limit_id;
trx_id_t m_creator_trx_id;
ids_t m_ids; //当前rw trx_id vector数组
/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
trx_id_t m_low_limit_no;
bool m_closed; //是否关闭
/** This is a view cloned by clone but not by
MVCC::clone_oldest_view. Used to make sure the cloned transaction does
not see its own changes. */
bool m_cloned;
typedef UT_LIST_NODE_T(ReadView) node_t;
byte pad1[64 - sizeof(node_t)];
node_t m_view_list; //在trx_sys上的链表node
MVCC ---> m_free
---> m_views
trx_sys->mvcc->m_views
1、建立
#0 MVCC::view_open (this=0x33acf18, view=@0x7fffee206c20: 0x0, trx=0x7fffee206b10) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/read/read0read.cc:568
#1 0x0000000001baf529 in trx_assign_read_view (trx=0x7fffee206b10) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/trx/trx0trx.cc:2344
#2 0x0000000001b1ccaf in row_search_mvcc (buf=0x7fff40028c50 "\377", mode=PAGE_CUR_G, prebuilt=0x7fff4002b860, match_mode=0, direction=0)
at /opt/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:5092
MVCC::view_open
->if (view != NULL)
如果视图存在
->uintptr_t p = reinterpret_cast<uintptr_t>(view);
view = reinterpret_cast<ReadView*>(p & ~1);
转换指针
->ut_ad(view->m_closed);
断言m_closed为false
->if (trx_is_autocommit_non_locking(trx) && view->empty())
如果事务是autocommit且无锁,并且没有读写事务
->view->m_closed = false;
设置false
->if (view->m_low_limit_id == trx_sys_get_max_trx_id())
如果 上限等于当前最大的max trx id
return;
直接返回
->mutex_enter(&trx_sys->mutex)
加锁
->UT_LIST_REMOVE(m_views, view);
从mvcc m_views中移除这个view
->else
如果视图为空
->mutex_enter(&trx_sys->mutex);
加锁
->view = get_view(MVCC::get_view)
获取一个新的view
->if (UT_LIST_GET_LEN(m_free) > 0)
如果存在空闲的read view
->view = UT_LIST_GET_FIRST(m_free);
从free中分配
->UT_LIST_REMOVE(m_free, view);
从m_free中去掉
->else
如果没有空闲的view
->view = UT_NEW_NOKEY(ReadView());
否则需要初始化了
->if (view != NULL)
这里就拿到了view了
->view->prepare(trx->id);
->ReadView::prepare
确认加锁
->m_creator_trx_id = id
记录建立这个view的trx_id
->m_low_limit_no = m_low_limit_id = trx_sys->max_trx_id;
设置为当前最大的trx id
->if (!trx_sys->rw_trx_ids.empty())
如果当前rw trxid 数组不为空
->copy_trx_ids(trx_sys->rw_trx_ids)
将trx_sys的rw_trx_ids读写事务数组,拷贝到这个view中
->else
如果当前rw trxid为空
m_ids.clear();
->if (UT_LIST_GET_LEN(trx_sys->serialisation_list) > 0)
如果提交中的事务大于0
->trx = UT_LIST_GET_FIRST(trx_sys->serialisation_list);
获取提交事务中的一个事务,头部
->if (trx->no < m_low_limit_no)
如果这个事务的trx_no小于trx_sys->max_trx_id
->m_low_limit_no = trx->no
->MVCC::complete(view->complete())
->m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;
如果m_ids有活跃RW事务,就设置m_up_limit_id为m_ids vector的第一个(最小的一个)
->m_closed = false;
设置为false
->MVCC::view_add(view_add(view))
->ut_ad(trx_sys_mutex_own())
还是先断言加锁
->UT_LIST_ADD_FIRST(m_views, const_cast<ReadView *>(view))
加入到MVCC的m_views链表中
->trx_sys_mutex_exit();
解锁
2、关闭
#0 MVCC::view_close (this=0x33acec8, view=@0x7fffee206c20: 0x33defd8, own_mutex=false) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/read/read0read.cc:809
#1 0x0000000001bae553 in trx_commit_in_memory (trx=0x7fffee206b10, mtr=0x0, serialised=false) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/trx/trx0trx.cc:2004
#2 0x0000000001baf1a7 in trx_commit_low (trx=0x7fffee206b10, mtr=0x0) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/trx/trx0trx.cc:2256
#3 0x0000000001baf24d in trx_commit (trx=0x7fffee206b10) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/trx/trx0trx.cc:2280
#4 0x0000000001bafbc2 in trx_commit_for_mysql (trx=0x7fffee206b10) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/trx/trx0trx.cc:2556
#5 0x0000000001973195 in innobase_commit_low (trx=0x7fffee206b10) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:4733
#6 0x0000000001973a5c in innobase_commit (hton=0x2e66550, thd=0x7fff3c000b90, commit_trx=true) at /opt/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:5022
#7 0x000000000198ac74 in ha_innobase::external_lock (this=0x7fff3c020740, thd=0x7fff3c000b90, lock_type=2)
at /opt/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:16854
#8 0x0000000000f63c2e in handler::ha_external_lock (this=0x7fff3c020740, thd=0x7fff3c000b90, lock_type=2) at /opt/percona-server-locks-detail-5.7.22/sql/handler.cc:8381
#9 0x00000000017217a2 in unlock_external (thd=0x7fff3c000b90, table=0x7fff3c96c3c8, count=1) at /opt/percona-server-locks-detail-5.7.22/sql/lock.cc:667
#10 0x0000000001721043 in mysql_unlock_read_tables (thd=0x7fff3c000b90, sql_lock=0x7fff3c96c3b0) at /opt/percona-server-locks-detail-5.7.22/sql/lock.cc:478
#11 0x00000000015c38c3 in JOIN::join_free (this=0x7fff3c007098) at /opt/percona-server-locks-detail-5.7.22/sql/sql_select.cc:2565
MVCC::view_close 对于auto commit的select第二个参数为false
->p = reinterpret_cast<uintptr_t>(view)
->if (!own_mutex)
从open view来看如果是auto commit且是只读数据,并且如果没有rw事务
这这里可以是own_mutex=false
->ReadView* ptr = reinterpret_cast<ReadView*>(p & ~1);
获取这个指针
ptr->m_closed = true;
ptr->m_cloned = false;
/* Set the view as closed. */
view = reinterpret_cast<ReadView*>(p | 0x1);
整个过程不涉及到MVCC的修改,只是通过本视图进行修改,标记为close
->else
view = reinterpret_cast<ReadView*>(p & ~1);
view->close();
UT_LIST_REMOVE(m_views, view);
从MVCC 链表中去掉
UT_LIST_ADD_LAST(m_free, view);
加入到free中
view = NULL;
清理view指针
trx_disconnect_from_mysql
...
if (trx->read_view != NULL) {
trx_sys->mvcc->view_close(trx->read_view, true);
}
...