MySQL·8.0.0新特性·持久化自增列值

zhaiwx_yinfeng發表於2016-09-22

Worklog: WL#6204

這是MySQL8.0修復的上古bug之一,在2003年由Percona的CEO(當時應該還沒Percona吧)提出的bug#199,光看這bug號就撲面而來一股上古時代的滄桑氣息。

問題的本質在於InnoDB初始化AUTO_INCREMENT的方式,在每次重啟時,總是算出表上最大的自增值作為最大值,下一次分配從該值開始。這意味著如果在btree右側葉節點大量刪除記錄,重啟後,自增值可能被重用。這在很多場景下可能導致問題,包括但不限於:主備切換、歷史資料遷移等場景。在bug#199下面一大堆的回覆裡,可以看到大量的同行抱怨。

很早阿里的MySQL版本就解決了這個問題,主要思路是取btree根page的一個未用的長整數字段(page header的PAGE_MAX_TRX_ID),然後將當前表上的auto-increment的值持久化到其中 (還好目前innodb還不支援多個自增列),由於一般表的root頁都是駐留在記憶體的,純記憶體操作對效能帶來的影響幾乎可以忽略。

官方的修復就比較優雅了,不改變任何現有的儲存,而是通過redo log來進行恢復。該補丁基於WL#7816的框架實現的,要想搞懂這個補丁,得先看看WL#7816做了哪些改動

根據Worklog的描述,當InnoDB發現某個索引損壞時,它會設定flag其標記成corruption狀態, 並持久化到內部資料詞典及持久化儲存中。但是新的全域性資料詞典(data directory,簡稱DD)置於儲存引擎上層,而從底層引擎去更新資料詞典可能會導致死鎖。而將corruption資訊層層傳遞到上層,看起來也比較詭異.

為了解決這個問題,InnoDB使用一個引擎私有的系統表+特殊redo log的方式,在引擎內部自己解決corruption標記持久化的問題。其大概思路為:

  1. 當發現索引損壞時,寫入一條redo log,但不更新資料詞典
  2. 引入一個innodb引擎私有的系統表,稱為DD Buffer Table,每次checkpoint之前會將索引corruption bit存入其中。
  3. 在崩潰恢復時,同時從redo log和DD Buffer Table中讀取索引 corruption bit, 合併結果,並標記記憶體中的表和索引物件。

在該worklog中解決的是corruption bit的持久化問題,但實現的框架也適用於其他目的,例如update_time, auto_inc, count(*)等,因此對程式碼做了通用性的抽象。

初始化Persister

目前Persister的型別僅有兩種,一個用於corruption bit的持久化,一個用於自增列的持久化,對應的類為:

Persister:
    |-- CorruptedIndexPersister
    |-- AutoIncPersister

Persister對應全域性物件dict_persist_t::persisters,可以通過型別persistent_type_t來找到對應的Persister,目前僅有PM_INDEX_CORRUPTED及PM_TABLE_AUTO_INC,但從註釋來看,未來肯定會做更多的擴充套件

Persister在啟動時呼叫函式dict_persist_init進行初始化。

新的系統表

新的系統表名為SYS_TABLE_INFO_BUFFER,對應管理類為DDTableBuffer,指標儲存在dict_persist->table_buffer中。

Table id為DICT_TBL_BUFFER_ID,值為0xFFFFFFFFFF000000ULL, ROOT PAGE是ibdata的第8個page(FSP_TBL_BUFFER_TREE_ROOT_PAGE_NO)

系統表包含兩個列:TABLE_ID及BLOB型別的METADATA(ref DDTableBuffer::init),METADATA列包含了所有需要持久化的後設資料。

更新Metata

當發現索引損壞時,呼叫dict_set_corrupted標記索引損壞,並進行日誌寫入(Persister::write_log):

  • 寫入的內容包含space id 和index id
  • 日誌格式為:
| 1byte: type = MLOG_TABLE_DYNAMIC_META
| Table ID
| 1byte: SUB-TYPE: PM_INDEX_CORRUPTED
| 1byte: Num: Number of corrupted indexs
| 4bytes: space id
| 8bytes: index id

## 這個結構有點奇怪,理論上同一個表的索引應該存在於同一個space中,這裡只需要記錄一個space id就可以了

寫完這條日誌後,會進行一次log flush,將日誌持久化到磁碟。由於index corruption屬於低概率事件,不會引起效能問題。

然後再設定表的狀態為髒 (dict_table_mark_dirty),這裡為表定義了三種狀態:

dict_table_t::dirty_status

METADATA_CLEAN: 在DDTableBuffer表中沒有任何快取資料
METADATA_BUFFERED: 在DDTableBuffer系統表中存在至少一行資料,未來需要回寫到DD中
METADATA_DIRTY: 一些持久化後設資料在記憶體中被修改,需要回寫到DDTableBuffer中

如果當前表狀態為METADATA_CLEAN,則需要將物件加到全域性連結串列dict_persist_t::dirty_dict_tables中,這個連結串列用於維護狀態為METADATA_DIRTY或者METADATA_BUFFERED的表物件

dirty_status在呼叫dict_table_mark_dirty後被設定成METADATA_DIRTY,並確保在dict_persist_t::dirty_dict_tables連結串列上

而對於AUTOINC列的持久化發生在插入或者更新時,注意對於臨時表無需做持久化。

在插入聚集索引記錄前(row_ins_clust_index_entry_low), 會先從entry中把counter拿出來,並記入日誌

在更新記錄時(row_upd_clust_rec),如果表上有autoinc列並且被更新成更大的值(row_upd_check_autoinc_counter),也會去嘗試記錄寫日誌。

持久化AUTOINC的日誌寫入函式為AutoIncLogMtr::log,當新的counter大於已經持久化的dict_table_t::autoinc_persisted時,將autoinc_persisted更新為新的counter,並將表的diry_status置為dirty(如果需要的話)

記錄的日誌格式為

| 1byte: type = MLOG_TABLE_DYNAMIC_META
| Table ID
| 1byte: Sub-type: PM_TABLE_AUTO_INC
| Autoinc Counter

注意這裡在寫入日誌後,出於效能考慮並沒有做flush log操作,因此如果crash了,已分配的autoinc不能保證不被重用,但從使用者的角度來看(事務級別),autoinc是不會重用的。

回寫DDTableBuffer

有幾種情況會將記憶體修改回寫到DDTableBuffer中:

  1. 在做checkpoint(log_checkpoint)之前,所有在dirty_dict_tables連結串列上的表物件,對應persist metadata都需要回寫到DDTableBuffer中(dict_persist_to_dd_table_buffer)
  2. 從記憶體中驅逐一個表物件時(dict_table_remove_from_cache_low),如果需要的話也會去嘗試回寫。

  3. 在對包含自增列的表做DDL後,需要持久化counter,在如下函式中,會呼叫dict_table_set_and_persist_autoinc:
ha_innobase::commit_inplace_alter_table
create_table_info_t::initialize_autoinc()
// for example: alter table..auto_increment = ??
row_rename_table_for_mysql;
// rename from temporary table to normal table

回寫的過程也比較簡單(dict_table_persist_to_dd_table_buffer_low):

  1. 通過表物件初始化需要回寫的Metadata資料: corrupt index及autoinc值(dict_init_dynamic_metadata)
  2. 構建記錄值,插入DDTableBuffer系統表(DDTableBuffer::replace(), 如果記錄存在的話,則進行悲觀更新操作
  3. 表物件的diry_status修改成 METADATA_BUFFERED,表示有buffer的後設資料

Recovery and Startup

在崩潰恢復時,當解析到日誌MLOG_TABLE_DYNAMIC_META時(MetadataRecover::parseMetadataLog),會進行解析並將解析得到的資料儲存到集合中(MetadataRecover::m_tables),如果存在相同table-id的項,就進行替換,確保總是最新的。

在完成recovery後,蒐集到的meta資訊暫時儲存到srv_dict_metadata中, 隨後進行apply(srv_dict_recover_on_restart), apply的過程也比較簡單,載入表物件,然後對錶物件進行更新(MetadataRecover::apply),例如對於autoinc列,就總是選擇更大的那個值。

最後

詳細參閱程式碼 commit dcb8792b371601dc5fc4e9f42fb9c479532fc7c2

這個bug已經掛了相當長的時間,不排除把這個bug當作InnoDB的“特性”的同學,一定要注意到這個改動…


相關文章