MySQL:大并发下TRX_SYS mutex案例分析

2023-08-21  本文已影响0人  重庆八怪

最近在处理一个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个链表结构:

随即我找了一下问题,发现有人已经遇到过了如下:

貌似BUG状态并没有关闭,然后顺着文章进行一下分析,说不定可以有更多的见解。

三、read view的分配和select的类型

read view对于select 语句来讲非常的重要,其主要是用于判定数据的可见性,如果不可见还要联动undo,因此对于大查询比如select很久的语句,可能purge线程不能清理undo,导致undo巨大,并且数据不能清理掉,否则无法判定可见性。
在当前版本中read view的分配,并不一定是分配可能是重用,我们将纯读取(select)的事务分为3种:

而对于一个read view分配正常来讲是需要加trx_sys->mutex,至少包含:

  1. 从trx_sys->mvcc的m_free中获取一个空闲的read view 或者直接分配内存建立read view
  2. 获取当前trx中rw 事务的vector数组(trx_sys的rw_trx_ids),用于判定可见性
  3. 获取当前trx中的事务最大和最小trx_id,用于判定可见性
  4. 获取当前事务trx的最老的trx_no,用于purge线程使用
  5. 加入到trx_sys->mvcc的m_views链表的头部

可以看到这一套流程基本上分不开对trx_sys元素的操作,因此需要持有trx_sys->mutex。而前面列举的A/B/C 的情况中:

而其主要方式就是在每次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加锁情况大概为:

而真正当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锁的时间就变得比较大了。可能的情况如下:

然后通过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);
    }
    ...
    
上一篇下一篇

猜你喜欢

热点阅读