1. 故事的起源
我们在学习MySQL/InnoDB purge的过程中,使用select name, subsystem, count from information_schema.innodb_metrics where name="trx_rseg_history_len";
查看系统当前回滚段历史链表长度(可以把这个历史链表近似理解为:尚未被清理的Undo物理页面),效果如下:
然而,我们发现即使在无负载时,trx_rseg_history_len也不会降低为0,这一点,网上早有反馈https://bugs.mysql.com/bug.php?id=76750,阿里印风给出了解答https://yq.aliyun.com/articles/400891?spm=a2c4e.11155435.0.0.23f84f18pf0kqh。我们在MySQL8.0.3-rc版本上发现了此问题,所以基于MySQL8.0.3-rc,我们展开讨论。
2.trx_rseg_history_len是什么?
我们可以将trx_rseg_history_len近似地理解为系统中尚未被清理的Undo物理页面数。
查询trx_rseg_history_len,实际查询的是trx_sys.rseg_history_len
,那么trx_sys.rseg_history_len在什么条件下增长、什么条件下缩减呢?
2.1 trx_sys.rseg_history_len的增长
事务提交时,update类型的undo页面将被添加到历史链表,此时trx_sys->rseg_history_len随之+1。
trx_commit-->trx_commit_low-->trx_write_serialisation_history-->trx_undo_update_cleanup-->trx_purge_add_update_undo_to_history-->os_atomic_increment_ulint(&trx_sys->rseg_history_len, 1)
2.2 trx_sys.rseg_history_len的缩减
Purge线程清理Undo页面时,将Undo页从历史链表移除,此时trx_sys->rseg_history_len随之-1。
srv_do_purge-->trx_purge-->trx_purge_truncate-->trx_purge_truncate_history-->trx_purge_truncate_rseg_history-->trx_purge_free_segment-->trx_purge_remove_log_hdr-->os_atomic_decrement_ulint(&trx_sys->rseg_history_len, 1)
3. Purge的工作机制?
既然trx_sys->rseg_history_len不能降回0,那么我们就关注Purge为何不能将其降0,从Purge线程说起。
3.1 Purge协调线程
Purge coordinator线程,其大致逻辑如下:
srv_purge_coordinator_thread //Purge coordinator线程函数主体
srv_do_purge
trx_purge
srv_que_task_enqueue_low //如果需要的话,唤醒一些Purge工作线程
que_run_threads //协调线程本身也purge数据行
trx_purge_truncate //清理Undo表空间,和2.2对应上了!
3.2 Purge工作线程
聚焦trx_purge_truncate上下文,不介绍Purge工作线程
3.3 为什么不能降0?
说回到2.2的函数调用过程,单说下面这部分,只要执行trx_purge_truncate,一定会调用后续函数(期间无分支跳出此调用过程),最终trx_sys->rseg_history_len - 1。
trx_purge_truncate-->trx_purge_truncate_history-->trx_purge_truncate_rseg_history-->trx_purge_free_segment-->trx_purge_remove_log_hdr-->os_atomic_decrement_ulint(&trx_sys->rseg_history_len, 1)
那么,进入trx_purge_truncate的条件是什么?
trx_purge调用trx_purge_truncate:
trx_purge(ulint n_purge_threads, ulint batch_size, bool truncate)
{
...
if (truncate || srv_upgrade_old_undo_found) {
trx_purge_truncate();
}
...
}
srv_do_purge调用trx_purge:
srv_do_purge(ulint n_threads, ulint* n_total_purged)
{
...
n_pages_purged = trx_purge(n_use_threads,
srv_purge_batch_size,
(++count % rseg_truncate_frequency) == 0);
...
}
可以看到,满足(++count % rseg_truncate_frequency) == 0
则进入trx_purge_truncate。
那么count是什么?rseg_truncate_frequency又是什么?
srv_do_purge(ulint n_threads, ulint* n_total_purged)
{
...
static ulint count = 0; //count记录了执行trx_purge的次数
...
ulint rseg_truncate_frequency = ut_min(
static_cast<ulint>(srv_purge_rseg_truncate_frequency),
undo_trunc_freq); //rseg_truncate_frequency是truncate undo表空间的频率,缺省值128
}
count和rseg_truncate_frequency共同实现了:每执行rseg_truncate_frequency次trx_purge,truncate一次undo表空间,清理undo物理页面。
3.4 小结
如果Purge线程执行了n次,n%rseg_truncate_frequency != 0,则n%rseg_truncate_frequency个Undo页面得不到清理,导致:
trx_sys->rseg_history_len = n % rseg_truncate_frequency
比如,trx_purge调用127次即清理完全部Undo信息,则这127个Undo页面,就不被清理。
4. 实验
为验证上述想法,我们修改了srv_do_purge,使得每次执行trx_purge都truncate表空间(这样会带来大量小I/O,影响性能)。
srv_do_purge(ulint n_threads, ulint* n_total_purged)
{
...
n_pages_purged = trx_purge(n_use_threads,
srv_purge_batch_size, 1);
...
}
我们使用Sysbench对此实例压测,产生足量Undo信息后等待Purge线程清理完成,最终可观察到,trx_sys->rseg_history_len = 0,如下示:
5. 最后
5.1 结论
为了减少I/O,Purge线程每执行一定次数进行一次Undo物理页面清理工作,导致了查询trx_rseg_history_len无法归0。
5.2 trx_rseg_history_len !=0 有啥影响吗?
历史数据的purge工作已经完成,保证了数据正确性。只是Undo物理页面滞后清理,后果是无用数据占用磁盘久一点,但是换取了较少的I/O啊!
5.3 为什么MySQL实例启动立即查询,trx_rseg_history_len !=0?
MySQL在启动时,会执行一些语句(具体内容还不清楚,参考scripts目录,该目录内容在实例初始化时会被执行),产生Undo信息。
5.4 暂时没有被清理的Undo页面怎么办?
Purge线程再次激活、trx_purge满rseg_truncate_frequency次时会清理的;
MySQL实例shutdown时,会清理Undo表空间。
网友评论