前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MySQL:大并发下TRX_SYS mutex案例分析

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

作者头像
老叶茶馆
发布2023-09-14 15:09:24
2490
发布2023-09-14 15:09:24
举报

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

一、问题展示

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

1.1 show engine

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

1.2 pstack(pt-pmp格式化)

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

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呢?

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

  • 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线程扫描的时候代码如下:

代码语言:javascript
复制
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,大概如下:

而正在做读写事务的只有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的长度,如下:

代码语言:javascript
复制
(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。

五、 代码部分

代码语言:javascript
复制
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);
 }
 ...
    

全文完。

本文参与?腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-07 07:00,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 老叶茶馆 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体同步曝光计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、问题展示
    • 1.1 show engine
      • 1.2 pstack(pt-pmp格式化)
        • 1.3 火焰图
        • 二、初步分析
        • 四、存在的问题
        • 五、 代码部分
        相关产品与服务
        云数据库 MySQL
        腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
        http://www.vxiaotou.com