美文网首页
MySQL:大并发下TRX_SYS mutex案例分析

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

作者: 重庆八怪 | 来源:发表于2023-08-21 19:01 被阅读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个链表结构:

    • 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,至少包含:

    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 的情况中:

    • 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);
        }
        ...
        
    

    相关文章

      网友评论

          本文标题:MySQL:大并发下TRX_SYS mutex案例分析

          本文链接:https://www.haomeiwen.com/subject/pdkymdtx.html