1. 故事的起源
我們在學(xué)習(xí)MySQL/InnoDB purge的過程中,使用select name, subsystem, count from information_schema.innodb_metrics where name="trx_rseg_history_len";查看系統(tǒng)當(dāng)前回滾段歷史鏈表長度(可以把這個歷史鏈表近似理解為:尚未被清理的Undo物理頁面),效果如下:

然而,我們發(fā)現(xiàn)即使在無負載時,trx_rseg_history_len也不會降低為0,這一點,網(wǎng)上早有反饋https://bugs.mysql.com/bug.php?id=76750,阿里印風(fēng)給出了解答https://yq.aliyun.com/articles/400891?spm=a2c4e.11155435.0.0.23f84f18pf0kqh。我們在MySQL8.0.3-rc版本上發(fā)現(xiàn)了此問題,所以基于MySQL8.0.3-rc,我們展開討論。
2.trx_rseg_history_len是什么?
我們可以將trx_rseg_history_len近似地理解為系統(tǒng)中尚未被清理的Undo物理頁面數(shù)。
查詢trx_rseg_history_len,實際查詢的是trx_sys.rseg_history_len,那么trx_sys.rseg_history_len在什么條件下增長、什么條件下縮減呢?
2.1 trx_sys.rseg_history_len的增長
事務(wù)提交時,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,那么我們就關(guān)注Purge為何不能將其降0,從Purge線程說起。
3.1 Purge協(xié)調(diào)線程
Purge coordinator線程,其大致邏輯如下:
srv_purge_coordinator_thread //Purge coordinator線程函數(shù)主體
srv_do_purge
trx_purge
srv_que_task_enqueue_low //如果需要的話,喚醒一些Purge工作線程
que_run_threads //協(xié)調(diào)線程本身也purge數(shù)據(jù)行
trx_purge_truncate //清理Undo表空間,和2.2對應(yīng)上了!
3.2 Purge工作線程
聚焦trx_purge_truncate上下文,不介紹Purge工作線程
3.3 為什么不能降0?
說回到2.2的函數(shù)調(diào)用過程,單說下面這部分,只要執(zhí)行trx_purge_truncate,一定會調(diào)用后續(xù)函數(shù)(期間無分支跳出此調(diào)用過程),最終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調(diào)用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調(diào)用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記錄了執(zhí)行trx_purge的次數(shù)
...
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共同實現(xiàn)了:每執(zhí)行rseg_truncate_frequency次trx_purge,truncate一次undo表空間,清理undo物理頁面。
3.4 小結(jié)
如果Purge線程執(zhí)行了n次,n%rseg_truncate_frequency != 0,則n%rseg_truncate_frequency個Undo頁面得不到清理,導(dǎo)致:
trx_sys->rseg_history_len = n % rseg_truncate_frequency
比如,trx_purge調(diào)用127次即清理完全部Undo信息,則這127個Undo頁面,就不被清理。
4. 實驗
為驗證上述想法,我們修改了srv_do_purge,使得每次執(zhí)行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對此實例壓測,產(chǎn)生足量Undo信息后等待Purge線程清理完成,最終可觀察到,trx_sys->rseg_history_len = 0,如下示:

5. 最后
5.1 結(jié)論
為了減少I/O,Purge線程每執(zhí)行一定次數(shù)進行一次Undo物理頁面清理工作,導(dǎo)致了查詢trx_rseg_history_len無法歸0。
5.2 trx_rseg_history_len !=0 有啥影響嗎?
歷史數(shù)據(jù)的purge工作已經(jīng)完成,保證了數(shù)據(jù)正確性。只是Undo物理頁面滯后清理,后果是無用數(shù)據(jù)占用磁盤久一點,但是換取了較少的I/O??!
5.3 為什么MySQL實例啟動立即查詢,trx_rseg_history_len !=0?
MySQL在啟動時,會執(zhí)行一些語句(具體內(nèi)容還不清楚,參考scripts目錄,該目錄內(nèi)容在實例初始化時會被執(zhí)行),產(chǎn)生Undo信息。
5.4 暫時沒有被清理的Undo頁面怎么辦?
Purge線程再次激活、trx_purge滿rseg_truncate_frequency次時會清理的;
MySQL實例shutdown時,會清理Undo表空間。