在处理一个故障的时候怀疑大量的删除数据导致了查询比较慢,但是自己对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
唤醒的条件是有事务提交或者回滚
/* 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
但是需要注意的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
十、总结
到这里开头的问题我们基本就了解了,如下:
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的工作线程:
并且purge线程状态处于running状态
- purge的协调线程会在每次事务提交的时候醒来,判断是否有需要清理的事务,如果长期没有事务到来那么会第一次等待10ms,超时过后进入长时间的堵塞等待状态。