這個(gè)問(wèn)題實(shí)際上已經(jīng)網(wǎng)友遇到過(guò)了,我們?cè)賮?lái)簡(jiǎn)單的分析一下,因?yàn)槲乙灿龅搅艘淮巍?/p>
一、 xtrbackup的報(bào)錯(cuò)
其實(shí)就是xtrbackup報(bào)下面的錯(cuò):
ER_IB_MSG_1077 eng "Undo tablespace number %lu was being truncated when mysqld quit."
ER_IB_MSG_1078 eng "Cannot recover a truncated undo tablespace in read-only mode"
拋錯(cuò)點(diǎn)
if (!undo::is_active_truncate_log_present(space_num)) { //檢測(cè)是否存在 trunc.log文件
return (DB_SUCCESS);
}
ib::info(ER_IB_MSG_1077, ulong{space_num});//如果存在報(bào)錯(cuò) ER_IB_MSG_1077
if (srv_read_only_mode) {
ib::error(ER_IB_MSG_1078); //xtrbackup始終未read only模式報(bào)錯(cuò) ER_IB_MSG_1078
return (DB_READ_ONLY);
}
其中l(wèi)og文件的名字后綴為undo_trunc.log
size_t size = strlen(srv_log_group_home_dir) + 22 + 1 /* NUL */
+ strlen(undo::s_log_prefix) + strlen(undo::s_log_ext)
/** Truncate Log file Prefix. */
const char *const s_log_prefix = "undo_";
/** Truncate Log file Extension. */
const char *const s_log_ext = "trunc.log";
二、 undo的segment初始化和使用
undo segment一共128個(gè),前面32位temp segment供臨時(shí)表使用,后面的segment輪訓(xùn)的分配到當(dāng)前的各個(gè)undo tablespace
初始化的時(shí)候會(huì)計(jì)算undo space id,然后根據(jù)不同undo space id初始化undo segment,5.7分配代碼:
trx_sys_create_rsegs
for (i = 0; i < new_rsegs; ++i) { //對(duì)每個(gè)rollback segment進(jìn)行初始化
ulint space_id;
space_id = (n_spaces == 0) ? 0
: (srv_undo_space_id_start + i % n_spaces);
//獲取 undo space_id 采用取模的方式循環(huán)
//如果是2個(gè)undo tablespace,則space 為1,2,1,2
ut_ad(n_spaces == 0
|| srv_is_undo_tablespace(space_id));
if (trx_rseg_create(space_id, 0) != NULL) {
++n_used;
++n_redo_active;
使用的時(shí)候也是進(jìn)行輪詢使用每個(gè)segment,5.7使用segment部分
get_next_redo_rseg:
static ulint redo_rseg_slot = 0; //此處是靜態(tài)變量
slot = redo_rseg_slot++; //靜態(tài)變量,不斷輪訓(xùn)使用
slot = slot % max_undo_logs;//取模輪詢使用
...
else if (rseg->skip_allocation) {
//skip_allocation為purge線程做truncate undo tablespace的時(shí)候設(shè)置
ut_ad(n_tablespaces > 1);
//如果是skip_allocation 很明顯不能是單個(gè)undo tablespace,需要斷言大于1
ut_ad(max_undo_logs
>= (1 + srv_tmp_undo_logs + 2));
continue;
這里如果undo segment處于skip_allocation狀態(tài)不能分配,這個(gè)值則由我們的purge線程做truncate undo tablespace的時(shí)候設(shè)置。
查看數(shù)據(jù)庫(kù)的undo目錄下有類似文件(測(cè)試)

三、 purge線程truncate undo tablespace
undo的truncate 由purge線程在做完undo的回收后(物理空間不變)后,回收的時(shí)候當(dāng)前也需要判斷當(dāng)前的undo是否需要才可以回收。 然后通過(guò)設(shè)置的參數(shù)和當(dāng)前undo物理文件的大小進(jìn)行循環(huán)判斷,并且如果有活躍事務(wù)持有本undo segment則不能truncate。
trx_purge_truncate_history末尾:
for (i = 0; i < nchances; i++) {
//每次循環(huán)判斷一個(gè) undo tablespace
trx_purge_mark_undo_for_truncate(&purge_sys->undo_trunc);
//判斷本undo tablespace是否可以truncate,
//如果需要清理標(biāo)記為rseg->skip_allocation = true,不在分配本undo segment。
trx_purge_initiate_truncate(limit, &purge_sys->undo_trunc);
//進(jìn)行undo tablespace的清理 。
}
以下為判斷是否查過(guò)參數(shù)innodb_max_undo_log_size設(shè)置的邏輯
if (fil_space_get_size(space_id)
> (srv_max_undo_log_size / srv_page_size)) {
//srv_max_undo_log_size為參數(shù)innodb_max_undo_log_size的設(shè)置大小
/* Tablespace qualifies for truncate. */
undo_trunc->mark(space_id);
//標(biāo)記為需要 truncate
undo::Truncate::add_space_to_trunc_list(space_id);
//插入到truncate 列表
break;
}
truncate的步驟比較多。在清理的最后會(huì)做如下步驟:
a. log-checkpoint
b. Write the DDL log to protect truncate action from CRASH
c. Remove rseg instance if added to purge queue before we
initiate truncate.
d. Execute actual truncate
e. Remove the DDL log.
而我們的undo_trunc.log正是DDL log這樣一個(gè)文件,因此只要它存在則說(shuō)明undo tablespace的tunracte沒(méi)有正常結(jié)束。
四、 如何模擬
這個(gè)問(wèn)題已經(jīng)有朋友提交了bug
https://bugs.mysql.com/bug.php?id=104573
我的測(cè)試方法如下,選擇版本MySQL8.0.27,也就是最新版本,我們發(fā)現(xiàn)BUG依舊存在。
- 選擇MGR的一個(gè)備份節(jié)點(diǎn),首先需要設(shè)置參數(shù)
set global innodb_max_undo_log_size=11534336;
set global innodb_purge_rseg_truncate_frequency=1;
其中innodb_purge_rseg_truncate_frequency默認(rèn)為128。如果調(diào)為1會(huì)加大undo清理和truncate判斷的頻率。
參數(shù)影響如下:
這個(gè)和參數(shù)innodb_purge_rseg_truncate_frequency的設(shè)置有關(guān),默認(rèn)為128,如果滿負(fù)荷計(jì)算為 :
300(undo log pages)*128(truncate frequency ) = 38,400
38400個(gè)undo log pages處理完成后會(huì)進(jìn)行一次undo history清理。
根據(jù)參數(shù)賦值
set_rseg_truncate_frequency(
static_cast<ulint>(srv_purge_rseg_truncate_frequency));
參數(shù)判斷
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次進(jìn)行一次清理
判斷是否進(jìn)入truncate流程
if (truncate || srv_upgrade_old_undo_found) { //truncate就是根據(jù)(++count % rseg_truncate_frequency)計(jì)算而來(lái)
trx_purge_truncate();
}
但是需要注意的count是一個(gè)static局部變量,因此每次調(diào)入函數(shù)會(huì)繼續(xù)上次的取值繼續(xù)計(jì)數(shù)。如果壓力很小那么undo可能不能及時(shí)清理:
小事務(wù)
如果都是小事務(wù)那么每個(gè)事務(wù)修改的undo page數(shù)可能達(dá)不到300個(gè),那么必然需要等待128個(gè)事務(wù)才能進(jìn)行一次清理。
大事務(wù)
如果事務(wù)比較大,有許多undo page,那么超過(guò)了300*128 那么就會(huì)進(jìn)行清理。
這不是說(shuō)del flag記錄不清理,而是說(shuō)undo history鏈表不清理。因此我們經(jīng)??吹紿istory list length不為0的情況。
注意線上不要設(shè)置innodb_purge_rseg_truncate_frequency=1,否則會(huì)導(dǎo)致purge線程的CPU負(fù)載飆升
- 在備節(jié)點(diǎn)選擇一張表,做一個(gè)大查詢
select sleep(10000) from test;
這樣做的目的是為了積壓UNDO不會(huì)及時(shí)清理掉。
使用sysbench加壓主節(jié)點(diǎn)
觀測(cè)從節(jié)點(diǎn)undo空間大小
一旦從節(jié)點(diǎn)的undo 空間增大,就可以中止結(jié)束大查詢,這樣就是放了read view,undo可以清理對(duì)清理接口進(jìn)行DEBUG
五、問(wèn)題觸發(fā)點(diǎn)和相關(guān)BUG
我們發(fā)現(xiàn)出問(wèn)題的棧如下,實(shí)際上是在修改tablespace_files和tablespaces兩個(gè)字典的時(shí)候,檢測(cè)發(fā)現(xiàn)為read only,因此出現(xiàn)了問(wèn)題。
#0 lock_tables_check (thd=0x7fff44001110, tables=0x7fff06ffb600, count=2, flags=18434) at /newdata/mysql-8.0.27/sql/lock.cc:214
#1 0x000000000385bee7 in mysql_lock_tables (thd=0x7fff44001110, tables=0x7fff06ffb600, count=2, flags=18434) at /newdata/mysql-8.0.27/sql/lock.cc:325
#2 0x00000000031b1e20 in lock_dictionary_tables (thd=0x7fff44001110, tables=0x7fff44098328, count=2, flags=18434) at /newdata/mysql-8.0.27/sql/sql_base.cc:7050
#3 0x000000000478e882 in dd::Open_dictionary_tables_ctx::open_tables (this=0x7fff06ffb750) at /newdata/mysql-8.0.27/sql/dd/impl/transaction_impl.cc:124
#4 0x000000000469dc95 in dd::cache::Storage_adapter::store<dd::Tablespace> (thd=0x7fff44001110, object=0x7fff4405f3e0) at /newdata/mysql-8.0.27/sql/dd/impl/cache/storage_adapter.cc:334
#5 0x00000000045acc35 in dd::cache::Dictionary_client::update<dd::Tablespace> (this=0x7fff44000bf0, new_object=0x7fff4405f3e0)
at /newdata/mysql-8.0.27/sql/dd/impl/cache/dictionary_client.cc:2653
#6 0x0000000004565cfd in dd::commit_or_rollback_tablespace_change (thd=0x7fff44001110, space=0x7fff4405f3e0, error=false, release_mdl_on_commit_only=false)
at /newdata/mysql-8.0.27/sql/dd/impl/dictionary_impl.cc:721
#7 0x0000000004d9a492 in dd_tablespace_set_id_and_state (space_name=0x7fff44058a88 "innodb_undo_001", space_id=4294966898, state=DD_SPACE_STATE_ACTIVE)
at /newdata/mysql-8.0.27/storage/innobase/dict/dict0dd.cc:5997
#8 0x0000000004bceea2 in trx_purge_truncate_marked_undo_low (space_num=1, space_name=...) at /newdata/mysql-8.0.27/storage/innobase/trx/trx0purge.cc:1496
#9 0x0000000004bcf262 in trx_purge_truncate_marked_undo () at /newdata/mysql-8.0.27/storage/innobase/trx/trx0purge.cc:1560
#10 0x0000000004bcf942 in trx_purge_truncate_undo_spaces () at /newdata/mysql-8.0.27/storage/innobase/trx/trx0purge.cc:1691
#11 0x0000000004bd1535 in trx_purge_truncate () at /newdata/mysql-8.0.27/storage/innobase/trx/trx0purge.cc:2395
#12 0x0000000004bd19f1 in trx_purge (n_purge_threads=4, batch_size=300, truncate=true) at /newdata/mysql-8.0.27/storage/innobase/trx/trx0purge.cc:2501
#13 0x0000000004b7d539 in srv_do_purge (n_total_purged=0x7fff06ffca70) at /newdata/mysql-8.0.27/storage/innobase/srv/srv0srv.cc:2924
lock_tables_check 會(huì)根據(jù)read only進(jìn)行判斷,如下:
/*
Prevent modifications to base tables if READ_ONLY is activated.
In any case, read only does not apply to temporary tables and
performance_schema tables.
*/
if (!(flags & MYSQL_LOCK_IGNORE_GLOBAL_READ_ONLY) && !t->s->tmp_table &&
!is_perfschema_db(t->s->db.str, t->s->db.length)) { //flags沒(méi)有忽略read only
if (t->reginfo.lock_type >= TL_WRITE_ALLOW_WRITE &&
(check_readonly(thd, true) ||
check_schema_readonly(thd, t->s->db.str, t->s))) {
return 1;//這里返回1
}
}
這里flags為:0100100000000010 (18434)
MYSQL_LOCK_IGNORE_GLOBAL_READ_ONLY為:1000
因此條件flags & MYSQL_LOCK_IGNORE_GLOBAL_READ_ONLY條件返回為false,取反后為true,觸發(fā)了readonly檢測(cè)。接下來(lái)分析出來(lái) dd::commit_or_rollback_tablespace_change在這種場(chǎng)景下沒(méi)有忽略read only 去做的提交如下:
trans_commit(THD *thd, bool ignore_global_read_lock)
因此從整個(gè)問(wèn)題來(lái)看,還是在這種特殊的場(chǎng)景下沒(méi)有考慮到read only的存在,主要存在于2點(diǎn):
- 修改數(shù)據(jù)字典 (dd::Open_dictionary_tables_ctx::open_tables)
- 修改字典的事務(wù)提交(dd::commit_or_rollback_tablespace_change)
因此在truncate tablespace的最后導(dǎo)致了錯(cuò)誤,但是實(shí)際的truncate任務(wù)(重建文件)已經(jīng)完成了。但是由于報(bào)錯(cuò)導(dǎo)致沒(méi)有跑到刪除DDL log的流程。而我們的xtrbackup剛好是檢測(cè)了DDL log這個(gè)地方,來(lái)確定undo是否處于重建狀態(tài)。這里已經(jīng)有朋友提交了BUG如下:
https://bugs.mysql.com/bug.php?id=104573
六、 規(guī)避方法和其他
當(dāng)然這個(gè)已經(jīng)有朋友提到了:
- 加大undo tablespace的大小,這樣自然不會(huì)觸發(fā)truncate條件,如果不觸發(fā)truncate那么自然沒(méi)有DDL log。
- 減少大事務(wù),減少大查詢,這樣undo segment在回收完會(huì)重新使用,不會(huì)繼續(xù)擴(kuò)張undo tablespace大小。
- 等待官方修復(fù)
其實(shí)在整個(gè)初始化和分配過(guò)程中,我們已經(jīng)發(fā)現(xiàn)事務(wù)的undo實(shí)際上是分布在不同的undo tablespace中的,并不是先用一個(gè)undo tablespace,等到需要切換的時(shí)候才進(jìn)行使用另外一個(gè),因此遇到大查詢(長(zhǎng)時(shí)間的查詢)這種情況,各個(gè)undo的大小也是比較均勻的增長(zhǎng)的。
其次因?yàn)檫@種輪詢分配的方式,讓purge線程有機(jī)會(huì)進(jìn)行truncate操作,只是在做truncate之前標(biāo)記本undo tablespace上的undo segment不可用就好了。