5

浅析InnoDB purge线程

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MjM5NzAzMTY4NQ%3D%3D&%3Bmid=2653935168&%3Bidx=1&%3Bsn=e2c4e2b0f206140ba2133561d9137fed
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:八怪(高鹏) 中亦科技数据库专家

水平有限,如有错误请谅解。源码版本8.0.21。

在处理一个故障的时候怀疑大量的删除数据导致了查询比较慢,但是自己对purge线程的工作流程一直不太清楚,本文不做深入解析,只做工作流程解析,待着如下问题进行:

  • del flag记录是否能够及时清理

  • 为什么History list length持续不为0,是否代表del flag记录没有清理

  • purge线程触发的规则是什么

一、purge线程综述

一般来讲我们理解的purge线程可以做如下的工作:

  • 清理del flag标签的记录

  • 清理undo的历史版本

  • 如果需要进行undo tablespace截断。

其包含一个协调线程和多个工作线程由如下参数设置:

innodb_purge_threads=4

这代表1个协调线程和3个工作线程。协调线程也会充当一个工作线程角色。

二、协调线程循环检测变化

如下调入:
srv_purge_coordinator_thread
 ->srv_purge_coordinator_suspend
判断如下:
(rseg_history_len <= trx_sys->rseg_history_len) { 
//如果当前history_len大于等于上一次循环的的history_len
      ret =os_event_wait_time_low(slot->event, SRV_PURGE_MAX_TIMEOUT, sig_count); 
//等待10毫秒后进行处理或者等待被唤醒

唤醒的条件是有事务提交或者回滚

    /* Tell server some activity has happened, since the trx
    does changes something. Background utility threads like
    master thread, purge thread or page_cleaner thread might
    have some work to do. */
    srv_active_wake_master_thread();

但是需要注意的是如果长期没有新的事务进行提交,那么可能进入永久堵塞状态而不是每10毫秒醒来,直到唤醒

if (ret == OS_SYNC_TIME_EXCEEDED) { //如果是等待超时
      if (rseg_history_len == trx_sys->rseg_history_len &&
          trx_sys->rseg_history_len < 5000) { //如果上次的history_len和本次history_len相同且小于5000那么需要等待唤醒
        stop = true; //设置为true,进行无限期等待,直到唤醒
      }

三、克隆最老的read view

这一步没什么好说的,因为清理undo需要根据当前最老的read view来清理,否则可能清理到正在读取需要的undo。

如下调入:
srv_purge_coordinator_thread
 ->srv_do_purge
  ->trx_purge
操作如下:
trx_sys->mvcc->clone_oldest_view(&purge_sys->view); //克隆老的 read view srv_do_purge

四、从可能需要清理的purge_queue中取出undo segment(简单理解为事务)

调入如下:
srv_purge_coordinator_thread
 ->srv_do_purge
  ->trx_purge
   ->trx_purge_attach_undo_recs
    ->trx_purge_fetch_next_rec
     ->TrxUndoRsegsIterator::set_next
操作如下:
const page_size_t &page_size = purge_sys->rseg_iter->set_next();

注意这里是一个迭代器,迭代的就是purge_sys->purge_queue,这是std::priority_queue实现的优先队列。具体迭代的代码如下:

while (!m_purge_sys->purge_queue->empty()) { //如果有事务需要清理
      if (m_trx_undo_rsegs.get_trx_no() == UINT64_UNDEFINED) {
        m_trx_undo_rsegs = purge_sys->purge_queue->top();
      } else if (purge_sys->purge_queue->top().get_trx_no() ==
                 m_trx_undo_rsegs.get_trx_no()) {
        m_trx_undo_rsegs.append(purge_sys->purge_queue->top()); //弹出一个
      } else {
        break;
      }

而事务进入purge_queue是在事务commit的时候调用trx_serialisation_number_get

purge_sys->purge_queue->push(elem);

因此到这里我们知道事务提交的时候可能会唤醒purge协调线程进行工作,并且会加入可能需要purge的事务队列purge_queue中。

五、判断是否符合清理规则

调入如下:
srv_purge_coordinator_thread
 ->srv_do_purge
  ->trx_purge
   ->trx_purge_attach_undo_recs
    ->trx_purge_fetch_next_rec
判断如下:
  if (purge_sys->iter.trx_no >= purge_sys->view.low_limit_no()) {
    return (nullptr);
  }

这里就是判断是否需要清理事务的trx no是否大于了oldest read view的low limit no,如果不满足则返回为nullptr,如果符合那么返回需要清理的page数量,并且指向下一个需要清理的undo segment。

六、每次清理默认为300个page

这个值由参数innodb_purge_batch_size进行控制,默认为300

调入如下:
srv_purge_coordinator_thread
 ->srv_do_purge
  ->trx_purge
   ->trx_purge_attach_undo_recs
生效如下:
for (ulint i = 0; n_pages_handled < batch_size; ++i)

清理流程会一致持续到没有page需要清理为止

调入如下:
srv_purge_coordinator_thread
 ->srv_do_purge
判断如下:
 (!srv_purge_should_exit(n_pages_purged) && n_pages_purged > 0 &&
           purge_sys->state == PURGE_STATE_RUN); 
//清理完成后n_pages_purged > 0 将不会满足
return (rseg_history_len); //返回 rseg_history_len

七、工作线程处理

分发给工作线程后进入如下调用,进行del flag的清理,没有仔细的看这部分,调用比较复杂。但是可以肯定是其构造row_purge_parse_undo_rec)和删除过程可能需要大量的循环和数据定位(btr_cur_search_to_nth_level)操作。

srv_worker_thread
 ->srv_task_execute
  ->que_run_threads
   ->que_run_threads_low
    ->que_thr_step
     ->row_purge_step
      ->row_purge
       ->row_purge_record_func

八、默认每128次batch undo清理会进行undo history清理

这个和参数innodb_purge_rseg_truncate_frequency的设置有关,默认为128,如果满负荷计算为 :

  • 300(undo log pages)*128(truncate frequency ) = 38,400

38400个undo log pages处理完成后会进行一次undo history清理。

根据参数赋值
set_rseg_truncate_frequency(
        static_cast<ulint>(srv_purge_rseg_truncate_frequency));

参数判断
    ulint rseg_truncate_frequency = ut_min(
        static_cast<ulint>(srv_purge_rseg_truncate_frequency), undo_trunc_freq); //128

    n_pages_purged = trx_purge(n_use_threads, srv_purge_batch_size,
                               (++count % rseg_truncate_frequency) == 0);//每128次进行一次清理

判断是否进入truncate流程
  if (truncate || srv_upgrade_old_undo_found) { //truncate就是根据(++count % rseg_truncate_frequency)计算而来
    trx_purge_truncate();
  }

但是需要注意的count是一个static局部变量,因此每次调入函数会继续上次的取值继续计数。如果压力很小那么undo可能不能及时清理:

  • 小事务 如果都是小事务那么每个事务修改的undo page数可能达不到300个,那么必然需要等待128个事务才能进行一次清理。

  • 大事务 如果事务比较大,有许多undo page,那么超过了300*128 那么就会进行清理。

这不是说del flag记录不清理,而是说undo history链表不清理。因此我们经常看到History list length不为0的情况。

九、清理undo history和undo空间

这里简单记录其工作的流程。不做深入函数描述(能力有限)

清理undo history

调入如下:
srv_purge_coordinator_thread
 ->srv_do_purge
  ->trx_purge
   ->trx_purge_truncate
    ->trx_purge_truncate_history
     ->trx_purge_truncate_rseg_history

清理的方式如下:

清理的起点:
hdr_addr = trx_purge_get_log_from_hist(
      flst_get_last(rseg_hdr + TRX_RSEG_HISTORY, &mtr));
向上扫描:
 hdr_addr = prev_hdr_addr;
结束条件:
  if (undo_trx_no >= limit->trx_no) { //这里代表结束了
    /* limit space_id should match the rollback segment
    space id to avoid freeing if the page belongs to a
    different rollback segment for the same trx_no. */
    if (undo_trx_no == limit->trx_no &&
        rseg->space_id == limit->undo_rseg_space) {
      trx_undo_truncate_start(rseg, hdr_addr.page, hdr_addr.boffset,
                              limit->undo_no);
    }

    rseg->unlatch();
    mtr_commit(&mtr);

    return;
  }

值得注意的是这个清理过程不能大于oldest read view的 trx no,否则清理结束。

truncate undo流程

调入如下:
srv_purge_coordinator_thread
 ->srv_do_purge
  ->trx_purge
   ->trx_purge_truncate
    ->trx_purge_truncate_history
     ->trx_purge_truncate_marked_undo

这之前有一个判定是否清理的过程

trx_purge_mark_undo_for_truncate
 ->Tablespace::needs_truncation

Tablespace::needs_truncation会判断是否进行undo truncate,这里涉及到两个参数

  • 参数innodb_undo_log_truncate的作用

  if (!srv_undo_log_truncate || m_rsegs == nullptr || m_rsegs->is_empty() ||
      m_rsegs->is_init()) {
    m_rsegs->s_unlock();
    return (false); //如果没有开启undo truncate则不进行清理
  }
  • 参数innodb_max_undo_log_size的作用

page_no_t trunc_size = ut_max(
      static_cast<page_no_t>(srv_max_undo_tablespace_size / srv_page_size),
      static_cast<page_no_t>(SRV_UNDO_TABLESPACE_SIZE_IN_PAGES)); //10MB

  if (fil_space_get_size(id()) > trunc_size) { //如果undo tablespace大小大于了innodb_max_undo_log_size
    return (true); //则进行清理
  }

十、总结

到这里开头的问题我们基本就了解了,如下:

  • del flag在事务提交后,由协调线程判定是否能够进行清理,如果可以清理会分发给工作线程进行清理,这是一个异步的过程,如果修改数据比较多,那么这个过程可能比较慢,并且可以看到purge的相关线程压力较大,但是还算及时。

  • purge线程总会积压一段时间才会进行History list length的清理,如果是小事务(每次修改的page小于innodb_purge_batch_size的设置),那么需要128个这种小事务才清理一次,如果是大事务那么修改两超过了(innodb_purge_batch_size*innodb_purge_rseg_truncate_frequency)的设置则进行一次清理,但是不管如何这个指标持续不为0是正常。如果较大那么可能意味着要么有大查询,要么purge的各个线程满负荷工作。如下,9281为一个purge的工作线程:

2YFVvea.png!mobile

并且purge线程状态处于running状态

ZNv26fF.png!mobile

  • purge的协调线程会在每次事务提交的时候醒来,判断是否有需要清理的事务,如果长期没有事务到来那么会第一次等待10ms,超时过后进入长时间的堵塞等待状态。

全文完。

Enjoy MySQL :)

扫码添加作者微信

Q3YBbeU.png!mobile

叶老师的「MySQL核心优化」大课已升级到MySQL 8.0,扫码开启MySQL 8.0修行之旅吧

7riIJ3u.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK