数据库内核月报

数据库内核月报 - 2022 / 03

MySQL · 源码阅读 · Purge sys介绍

Author: 沧何

Purge sys

参考代码8.0.23,purge系统的目的是将不被任何read可见的旧版本数据(undo数据)进行清理。它包含两类处理线程:一类是一个purge coordinator thread,另一类是innodb_purge_threads-1个purge worker threads。

mysql在srv_sys->sys_threads数组中维护内部线程,用于暂停或重启内部线程,purge coordinator thread存入SRV_PURGE_SLOT(1)位置,purge worker threads存入2 ~ srv_sys->n_sys_threads位置。

purge sys中有个purge_queue,它是个按Rollback segements的txn_no排序的最小堆。每次通过该purge_queue获取最早提交的事务所产生的undo log,然后进行清理。

Purge coordinator thread流程

purge coordinator thread在正常清理阶段,分批清理undo数据,每批最多清理300个undo log(具体的清理工作由purge worker thread执行)。由于清理期间依然有新事务在产生新的undo数据,清理一批后根据剩余待清理的undo log个数,来判断是要增加还是减少执行清理的worker线程个数。由于有innodb_purge_threads-1个purge worker threads,不执行清理的worker线程都处于sleep状态,要执行清理的worker线程会被purge coordinator thread唤醒,唤醒就是通过srv_sys->sys_threads数组中保存的slot->event进行的。

事务提交会将旧版本的数据放入undo segment中,其中普通表的旧版本数据放入redo rollback segment,临时表的旧版本数据放入unredo rollback segment,并将这两个rollback segment存入purge sys的purge_queue中。purge coordinator thread清理undo时,借助purge sys的purge_queue,就能确保取出的rollback segment是按照事务提交的顺序(txn_no)。

purge coordinator thread在每批清理前会先将事务系统中缓存的ReadView保存在purge_sys->view中,再按事务提交的顺序从purge sys->purge_queue中取出所有待清理的undo log,再确保待清理的undo log的trx_no不能超过purge_sys->view的m_low_limit_no(小于该值的undo log不再被该ReadView需要),然后按table_id进行分组存入map中,直到在map中存满300个undo log,如果当前rollback segment中不够,则会继续从purge sys的purge_queue中取出下一个rollback segment,重复该过程。

继续将map分组后的undo logs轮转存入清理任务purge_sys->query->thrs[i]中,例如map中包含3组undo logs,分别为table1、table2、table3,将table1的undo logs存入清理任务purge_sys->query->thrs[1],将table2的undo logs存入清理任务purge_sys->query->thrs[2],将table3的undo logs存入清理任务purge_sys->query->thrs[3]。再将其中前n_use_threads个清理任务存入srv_sys->tasks,并唤醒n_use_threads-1个purge worker thread,剩余一个清理任务交由purge coordinator thread来处理。

由于每批清理都只清理300个undo log,而一个rollback segment会包含多个undo log,所以需要在purge sys中记录上次清理的位置,也就是purge_sys->hdr_page_no和purge_sys->hdr_offset,这样下次清理时可以继续从上次上次清理的位置继续清理。

srv_start_purge_threads
  v
  v
srv_purge_coordinator_thread
  |
  |-> (while !srv_purge_should_exit)      正常清理阶段
  |     v
  |   srv_do_purge    把多余的线程当做一个池子,只有purge跟不上更新的时候,才会去调度这些线程
  |     v
  |   (while !srv_purge_should_exit)
  |     |
  |     |-> trx_sys->rseg_history_len相比上次purge有增长时,或者超过了innodb_max_purge_lag,则 ++n_use_threads
  |     |
  |     |-> 否则说明数据库处于不活跃状态(srv_check_activity),  -n_use_threads
  |     |
  |     |-> trx_purge
  |           |
  |           |-> trx_sys->mvcc->m_views中获取oldestReadView,拷贝到purge_sys->view,所有在该ReadView开启前提交的事务所产生的undo都被认为是可以清理的
  |           |
  |           |-> trx_purge_attach_undo_recs
  |           |     |
  |           |     |-> (for 300个待获取的undo log)
  |           |     |     |
  |           |     |     |-> trx_purge_fetch_next_rec      获取undo log
  |           |     |     |     |
  |           |     |     |     |-> (purge_sys中未缓存上次清理的undo log)
  |           |     |     |     |     v
  |           |     |     |     |   trx_purge_choose_next_log       获取最早提交的事务所产生的undo log,然后进行清理
  |           |     |     |     |     |
  |           |     |     |     |     |-> TrxUndoRsegsIterator::set_next    purge_sys->rseg_iter移到下一个有效的rseg
  |           |     |     |     |     |     |
  |           |     |     |     |     |     |-> purge_sys->purge_queue中取出trx_no最小的所有rsegs,存入purge_sys->rseg_iter->m_trx_undo_rsegs
  |           |     |     |     |     |     |   包含redo rollback segment(普通表)noredo rollback segment(临时表)
  |           |     |     |     |     |     |
  |           |     |     |     |     |     |-> purge_sys->rseg指向m_trx_undo_rsegs中第一个rseg
  |           |     |     |     |     |     |
  |           |     |     |     |     |     |-> purge_sys->rseg_iter->m_iter 指向m_trx_undo_rsegs中下一个rseg
  |           |     |     |     |     |     |
  |           |     |     |     |     |     |-> 更新purge_sys->iter.trx_no & hdr_offset & hdr_page_no指向当前rseg上次purge到的位置
  |           |     |     |     |     |     |
  |           |     |     |     |     |     |-> purge_sys->next_stored = true
  |           |     |     |     |     |
  |           |     |     |     |     |-> trx_purge_read_undo_rec
  |           |     |     |     |           |
  |           |     |     |     |           |-> (!purge_sys->rseg->last_del_marks)    已读到该rseg的结尾
  |           |     |     |     |           |
  |           |     |     |     |           |-> (purge_sys->rseg->last_del_marks)
  |           |     |     |     |                 |
  |           |     |     |     |                 |-> trx_undo_get_first_rec          读取当前的undo log记录
  |           |     |     |     |                 |
  |           |     |     |     |                 |-> 更新purge_sys->offset & page_no & iter.undo_no iter.undo_rseg_space 指向当前的undo log
  |           |     |     |     |
  |           |     |     |     |-> purge_sys->iter.trx_no >= purge_sys->view.m_low_limit_no,则后续undo log不再清理
  |           |     |     |     |
  |           |     |     |     |-> trx_undo_build_roll_ptr      获取undo log的回滚段指针roll_ptr
  |           |     |     |     |
  |           |     |     |     |-> 获取undo log的对应事务的modifier_trx_id
  |           |     |     |     |
  |           |     |     |     |-> trx_purge_get_next_rec
  |           |     |     |           |
  |           |     |     |           |-> (若已读到该rseg的结尾)    获取下一个最早提交的事务所产生的undo log,然后进行清理
  |           |     |     |           |     |
  |           |     |     |           |     |-> trx_purge_rseg_get_next_history_log
  |           |     |     |           |     |
  |           |     |     |           |     |-> trx_purge_choose_next_log -> xxx
  |           |     |     |           |
  |           |     |     |           |-> (否则) trx_undo_page_get_next_rec    获取下一个undo log,并缓存在purge_sys
  |           |     |     |
  |           |     |     |-> undo logtable_id进行分组存入group_by map
  |           |     |
  |           |     |-> group_by map中,将table_id相同的一组undo log轮转存入清理任务purge_sys->query->thrs[i]
  |           |
  |           |-> (for n_use_threads-1 worker线程)
  |           |     |
  |           |     |-> que_fork_scheduler_round_robin    purge_sys->query->thrs取出一个清理任务
  |           |     |     v
  |           |     |   thr->state = QUE_THR_RUNNING
  |           |     |
  |           |     |-> srv_que_task_enqueue_low     将该清理任务存入srv_sys->tasks
  |           |           v
  |           |         srv_release_threads          唤醒一个worker线程,它会从srv_sys->tasks中取出一个清理任务
  |           |
  |           |-> que_fork_scheduler_round_robin
  |           |
  |           |-> trx_purge_truncate          128purge, truncate一次history list,所以每隔一会,才看到history list长度变小
  |                 v
  |               trx_purge_truncate_history
  |                 |
  |                 |-> (for rseg in 所有undo space中的rsegs)
  |                       v
  |                     trx_purge_truncate_rseg_history
  |                       v
  |                     trx_purge_remove_log_hdr      rseghistory list中删除该undo log
  |
  |-> (while srv_fast_shutdown == 0 && n_pages_purged > 0)  slow shutdown要避免在退出上述循环后,有新的记录加入
  |     v
  |   trx_purge -> xxx
  |
  |-> trx_purge(truncate=true) -> xxx     最后对history list做一次truncate,并确保所有worker线程退出

Purge worker thread流程

purge worker thread被唤醒后,会从srv_sys->tasks中取出一个清理任务,清理掉其中的undo logs(具有相同的table id)。对于每个待清理的undo log,会先解析undo log内容,检查该undo log是否能进行清理,若能清理,先删除二级索引再删除clustered索引。

srv_worker_thread
  v
(while purge_sys->state != PURGE_STATE_EXIT)    worker线程总是在coordinator线程退出之后再退出
  v
srv_task_execute
  |
  |-> 持有srv_sys->tasks_mutex锁,从srv_sys->tasks中取出一个thr,然后释放tasks_mutex
  |
  |-> que_run_threads
        |
        |-> que_run_threads_low
              |
              |-> que_thr_node_step
              |
              |-> row_purge_step      从清理任务thr中取出undo logs(具有相同的table id),进行清理
                    |
                    |-> row_purge
                    |     v
                    |   (for undo_rec in 清理任务thr中的所有undo logs)
                    |     |
                    |     |-> row_purge_parse_undo_rec    解析undo log,检查该undo log是否能进行清理
                    |     |
                    |     |-> row_purge_record            清理undo log
                    |           v
                    |         row_purge_del_mark          先删除二级索引再删除clustered索引
                    |           |
                    |           |-> row_purge_remove_multi_sec_if_poss
                    |           |
                    |           |-> row_purge_remove_clust_if_poss
                    |
                    |-> row_purge_end

参考:
InnoDB事务系统
InnoDB的read view,回滚段和purge过程简介