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

最近在处理一个case的时候(版本:5.7.29),通过连续pstack发现存在2个问题导致CPU比较高导致时钟中断比较高,解决其中一个问题后主观描述系统正常了,但是剩下1个问题没有解决,这里集中看看这个问题。

一、问题展示

这个问题大概通过pstack和火焰图以及show engine的mutex等待部分来呈现

1.1 show engine

4dd1565d276f974b2cae029bf534911b.jpeg这里看到TRX_SYS mutex并不是长时间的等待(0秒),而是很短但是可见。

1.2 pstack(pt-pmp格式化)

其中一个pstack展示如下,这里我删除了大部分内容,只留下有价值的部分。84708b0b94c1792cdf87ed3945592ee2.jpeg

1.3 火焰图
311f19209aba551e2a3fb557f3213ed3.jpeg
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呢?

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

  • https://developer.aliyun.com/article/223320

  • https://bugs.mysql.com/bug.php?id=88422

貌似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,大概如下:c3a824a43371cc3c7de4987a4ba695ad.jpeg

而正在做读写事务的只有1个session。这些session可能曾今跑过select但是且留下了read view,那么极限情况下可能有4750个read view 残留,那么循环的代价被放大了很多。purge线程的唤醒也是比较频繁的,具体参考

  • https://www.jianshu.com/p/e6804308b156

但是这个问题无法解决,很是遗憾,除非修改代码,继而查看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,如果用3个read view来画个图大概这个样子,可以看到pruge线程不得不扫描view1和view2两个read view才能找到oldest read view view3。4b582a266b9c84e7ee7783cb300b3a66.jpeg

五、 代码部分

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、建立
    
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、关闭


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

全文完。


《深入浅出MGR》视频课程

戳此小程序即可直达B站

https://www.bilibili.com/medialist/play/1363850082?business=space_collection&business_id=343928&desc=0


文章推荐:


想看更多技术好文,点个“在看”吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值