天天看點

MySQL 8.0 Atomic DDL

背景

MySQL 8.0 DDL 是一個複雜的過程,涉及比較多的子產品,例如:MDL 鎖,表定義緩存,行格式,Row Log,DDL Log,online 屬性,表空間實體檔案操作等。本文主要通過與5.7版本的對比講述原子性相關的實作。

相關WorkLog見:

https://dev.mysql.com/worklog/task/?id=9525

https://dev.mysql.com/worklog/task/?id=9536

在 8.0 之前的版本中使用了Sever層的Frm檔案作為中繼資料儲存的方式,這樣做可以讓多個存儲引擎都使用統一的定義規範。但是也帶來了一些問題,InnoDB引擎本身也做了表定義的存儲,隻給InnoDB引擎使用。那麼Frm實體檔案的操作和 InnoDB事務性表定義的更改之間如果發生Crash,就會造成Server層的中繼資料和InnoDB的資料不一緻。

例如 Alter table 的過程中,需要産生臨時表來存儲新定義的表資料,如果在新舊表定義Rename過程(DDL操作的一個環節)中發生Crash,會造成表的不可通路,因為有可能FRM檔案是舊的定義,但是InnoDB的同名表卻是新的定義。

又例如,如果frm檔案被誤删除了,導緻表無法被打開,如果需要删除表就需要 CDB 的 Drop Table Force功能跳過frm的檢查,直接從InnoDB删除表。

MetaData Before 8.0

MySQL 8.0 的中繼資料結構如下所示:

MySQL 8.0 Atomic DDL

在 8.0 之前 MySQL 的中繼資料分散存儲在三個不同的地方:實體檔案、MyISAM引擎、InnoDB引擎。實體檔案主要存儲 frm,opt,trg 等定義資訊,會存在與InnoDB不一緻的情況,沒有日志保護。系統表 user/proc/events 等資訊存儲在MyISAM引擎中,不支援事務。InnoDB引擎則是存儲SYS_*系統表,例如SYS_TABLES,SYS_INDEXES 等。由于 實體檔案和非事務引擎中繼資料表的存在很難做到DDL的原子性。

詳見:InnoDB INFORMATION_SCHEMA System Tables

https://dev.mysql.com/doc/refman/5.7/en/innodb-information-schema-system-tables.html

例如,ALTER TABLE 過程中涉及到的中繼資料資訊變化如下所示:

DDL 階段 FRM 檔案 IBD 檔案 資訊描述
Start table_test.frm table_test.ibd 原表
DDL Prepare table_test.frm table_test.ibd 原表
#sql-5810_3.frm #sql-ib37-952053511.ibd 新定義的臨時表
DDL Alter 同上 同上 同上

DDL Commit 1

(InnoDB Commit)

同上

InnoDB Commit:

table_test.ibd --> #sql-ib38-952053512.ibd

#sql-ib37-952053511.ibd --> table_test.ibd

将原表ibd和新定義表ibd互換名字

InnoDB Commit:

Drop #sql-ib38-952053512.ibd

删除原定義的表

DDL Commit 2

(Server Commit)

table_test.frm --> #sql-2add_3.frm

and drop

table_test.ibd frm檔案互換名字
#sql-5810_3.frm --> table_test.frm
Finish table_test.frm table_test.ibd 新定義的表

如果 DDL Commit 1 階段之後發生Crash,那麼Server層和InnoDB層的表定義是不同的,表通路會失敗。

MetaData After 8.0

MySQL 8.0 Atomic DDL

在 8.0 中Data Dictionary 通過将系統表存儲在InnoDB引擎中,建構了一套中繼資料存儲和讀取的服務架構,其中包括 DD Client 和 Storage Adaptor。

SQL層的Table Define Cache之前通過讀取FRM檔案來緩存定義,進而Open Table 進行通路,現在需要通過DD Client通路存儲在InnoDB中的中繼資料。

中繼資料系統表有了InnoDB事務系統的支援,MySQL 8.0 将之前版本中多個事務完成的一個DDL操作變成一個 DDL Trx 事務去完成(也有其他輔助事務,但不影響DDL Trx 主導的DDL的原子性)。其實作方式就是改造中繼資料存儲方案,将中繼資料和實體操作統一存儲到了 InnoDB 引擎中,通過 DDL 對中繼資料表操作的事務的原子性,達到DDL操作的原子性。DDL Trx 事務送出則 DDL 完成,如果復原則 DDL 執行的所有操作都可以復原,包括:中繼資料表復原和檔案操作復原。也就是原子 DDL 需要中繼資料操作的原子性和檔案(實體)操作的原子性。

原子保證(一)InnoDB New DD,解決中繼資料操作原子性

8.0 中新的資料字典 Data Dictionary 是基于 InnoDB 存儲引擎的事務表實作的,我們可以通過InnoDB提供的接口看到都有哪些中繼資料表。

通過設定 SET SESSION debug='+d,skip_dd_table_access_check'; 可以通路中繼資料表。

MySQL 8.0 Atomic DDL

New Data Dictionary 代替了之前分散在不同地方的中繼資料,用于儲存系統中繼資料,這些表會伴随着DDL的進行而進行各種操作,例如:建立一個表的時候,會向tables系統表中插入一行,會向 indexes 系統表中插入該table id以及其索引資訊,多個索引就插入多個行,也會向column系統表中插入table id以及對應的列資訊。值得注意的是,所有這些修改都是通過同一個DDL Trx進行的,如果事務送出則系統表的修改送出,如果DDL復原,這些修改也會通過UNDO LOG進行復原。Data Dictioanry 系統表解決的是之前版本Server層和InnoDB層定義不一緻的問題,現在的DD tables通過InnoDB事務系統做到了原子性。

DD通過統一的接口設計提供給外層調用,其實作如下圖所示:

MySQL 8.0 Atomic DDL

8.0 Data Dictionary 的設計分為三層:Client 層,接口轉換層,存儲層。

  • Client層:主要負責對外提供統一通路接口以及緩存管理,SQL層的Table Define Cache就是通過DD Client 接口去擷取那些之前需要從FRM檔案中讀的内容。同樣,InnoDB層的Dict Cache也是通過DD Client讀取的。
  • 轉換層:負責将Client的請求封裝成對應系統表的通路方法
  • 存儲層:就是InnoDB表的存儲和通路方法,和使用者表一樣。

一個典型的調用堆棧如下圖所示:

MySQL 8.0 Atomic DDL

原子保證(二)DDL Log 解決實體表空間檔案操作原子性

DDL 操作會涉及到實體檔案的操作,例如Btree的建立和釋放,表空間檔案ibd的建立和删除等,這樣的實體操作也需要能做到可復原,以保證DDL的原子操作。

DDL Log 被引入進來以解決實體操作的原子性,Create Table、Alter Table、Drop Table、Rename Table、Create Index 等操作都會涉及DDL Log表的修改。

DDL Log 系統表的定義如下:

mysql> show create table mysql.innodb_ddl_log \G
*************************** 1. row ***************************
       Table: innodb_ddl_log
Create Table: CREATE TABLE `innodb_ddl_log` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `thread_id` bigint unsigned NOT NULL,
  `type` int unsigned NOT NULL,
  `space_id` int unsigned DEFAULT NULL,
  `page_no` int unsigned DEFAULT NULL,
  `index_id` bigint unsigned DEFAULT NULL,
  `table_id` bigint unsigned DEFAULT NULL,
  `old_file_path` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `new_file_path` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `thread_id` (`thread_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC;           

DDL Log 用以記錄一個DDL事務所做的檔案實體更改,有兩個方面的作用:

  1. 復原的時候,為了保證DDL事務的實體檔案新增操作可復原,例如建立的ibd要删除,建立的實體索引樹要釋放。類似“UNDO LOG”的復原作用。
  2. 送出之後,為了保證DDL事務的實體檔案删除操作可復原,DDL事務過程中删除操作不能立刻執行,因為一旦真正删除就不能復原了,是以将其記錄到DDL Log中。放到Commit之後再執行。

此外,Rename Log 重命名操作,Alter Table,Rename Table 會使用。

下面舉例說明:

Create Table 的 DDL Log 操作(作用1):

[MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=7, thread_id=8, space_id=4, old_file_path=./test/test.ibd]
[MY-012478] [InnoDB] DDL log delete : 7
[MY-012477] [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=8, thread_id=8, table_id=1066, new_file_path=test/test]
[MY-012478] [InnoDB] DDL log delete : 8
[MY-012472] [InnoDB] DDL log insert : [DDL record: FREE, id=9, thread_id=8, space_id=4, index_id=156, page_no=4]
[MY-012478] [InnoDB] DDL log delete : 9
[MY-012485] [InnoDB] DDL log post ddl : begin for thread id : 8
[MY-012486] [InnoDB] DDL log post ddl : end for thread id : 8           

從日志看有三種 ddl log type 的日志,日志其實描述了一個逆向操作,DDL 建立的實體檔案或者索引樹,這些實體操作怎麼復原,那麼就寫入了一個實體操作的逆向操作。

建立了表空間檔案就寫DELETE SPACE,建立了索引樹就寫 FREE TREE,記憶體中保留這個表的定義就寫清除表定義。

值得關注的是,其中還有 DDL log delete 操作,這個其實是删除剛剛寫入的 ddl log 日志。因為這些日志需要在DDL事務送出的時候全部删除,不能夠保留到COMMIT之後,因為成功送出之後是不能删除這些檔案和索引樹的,那麼這裡DDL就用了DDL Trx之外的事務做 ddl log 日志的insert操作,該insert事務立刻送出,DDL trx 讀取這個 ddl log record并将其标記删除,如果DDL Trx 成功Commit了,那麼删除生效,ddl log 被清理。如果DDL Trx失敗復原了,那麼 ddl log 日志保留下來了,按照日志的操作復原即可。

Drop Table 的 DDL Log 操作(作用2):

[InnoDB] DDL log insert : [DDL record: DROP, id=10, thread_id=8, table_id=1066]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=11, thread_id=8, space_id=4, old_file_path=./test/test.ibd]
[InnoDB] DDL log post ddl : begin for thread id : 8
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=11, thread_id=8, space_id=4, old_file_path=./test/test.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=10, thread_id=8, table_id=1066]
[InnoDB] DDL log post ddl : end for thread id : 8           

如上所述,Drop Table 操作 DDL Log 記錄需要在 DDL Trx Commit成功後需要删除的實體操作。Drop Table需要删除獨立表空間檔案,就寫DELETE SPACE并給出路徑。和Create Table不同,這裡沒有delete ddl log操作,因為這些日志是需要留給Commit之後的Post DDL階段做實體删除操作。

如果 Post DDL 階段沒有來得及做就Crash了,重新開機之後的會繼續讀取 DDL Log 表按照日志類型做相應的操作。

Alter Table 的 DDL Log 操作

// ======================Prepare=============================================
[MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=15, thread_id=8, space_id=6, old_file_path=./test/#sql-ib1067-850981604.ibd]
[MY-012478] [InnoDB] DDL log delete : 15
[MY-012477] [InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=16, thread_id=8, table_id=1068, new_file_path=test/#sql-ib1067-850981604]
[MY-012478] [InnoDB] DDL log delete : 16
[MY-012472] [InnoDB] DDL log insert : [DDL record: FREE, id=17, thread_id=8, space_id=6, index_id=158, page_no=4]
[MY-012478] [InnoDB] DDL log delete : 17
// ======================Alter=============================================
[MY-000000] [InnoDB] TXSQL: parallel_read_threads with 1 threads, parallel_sort_threads with 1 threads, sql: alter table test add id1 int, algorithm = inplace, sample_step: 1, max_sample_cnt: 0, skip_pk_sort: 1
// ======================Commit=============================================
[MY-012475] [InnoDB] DDL log insert : [DDL record: DROP, id=18, thread_id=8, table_id=1067]
[MY-012474] [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=19, thread_id=8, space_id=5, old_file_path=./test/#sql-ib1068-850981605.ibd, new_file_path=./test/test.ibd]
[MY-012478] [InnoDB] DDL log delete : 19
[MY-012476] [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=20, thread_id=8, table_id=1067, old_file_path=test/#sql-ib1068-850981605, new_file_path=test/test]
[MY-012478] [InnoDB] DDL log delete : 20
[MY-012474] [InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=21, thread_id=8, space_id=6, old_file_path=./test/test.ibd, new_file_path=./test/#sql-ib1067-850981604.ibd]
[MY-012478] [InnoDB] DDL log delete : 21
[MY-012476] [InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=22, thread_id=8, table_id=1068, old_file_path=test/test, new_file_path=test/#sql-ib1067-850981604]
[MY-012478] [InnoDB] DDL log delete : 22
[MY-012475] [InnoDB] DDL log insert : [DDL record: DROP, id=23, thread_id=8, table_id=1067]
[MY-012473] [InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=24, thread_id=8, space_id=5, old_file_path=./test/#sql-ib1068-850981605.ibd]
[MY-012485] [InnoDB] DDL log post ddl : begin for thread id : 8
[MY-012479] [InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=24, thread_id=8, space_id=5, old_file_path=./test/#sql-ib1068-850981605.ibd]
[MY-012479] [InnoDB] DDL log replay : [DDL record: DROP, id=23, thread_id=8, table_id=1067]
[MY-012479] [InnoDB] DDL log replay : [DDL record: DROP, id=18, thread_id=8, table_id=1067]
[MY-012486] [InnoDB] DDL log post ddl : end for thread id : 8           

這個是 Alter DDL 的三階段産生的 DDL Log操作:

  • Prepare 階段:建立臨時名字的表用于存放新的定義的表資料,類似 Create Table。
  • Alter 階段就是資料的轉移,沒有使用DDL Log。
  • Commit 階段:InnoDB 的Alter Commit需要将InnoDB表做重命名,是以有 RENAME SPACE日志。此時的DDL Log日志仍是逆向操作的日志,代表復原的時候要重新RENAME回來。
  • 最後Commit還需要将原表删掉,此時的原表已經是一個臨時的名字了。這部分日志交給 Post DDL 階段處理。

參考:

  1. Atomic DDL 官方介紹:
  2. https://dev.mysql.com/doc/refman/8.0/en/atomic-ddl.html
  3. InnoDB_New_DD:
  4. Support crash-safe DDL https://dev.mysql.com/worklog/task/?id=9536
  5. Bootstrap code for new DD:
  6. https://dev.mysql.com/worklog/task/?id=6394
  7. MySQL 8.0 Data Dictionary:
  8. Background and Motivation https://dev.mysql.com/blog-archive/mysql-8-0-data-dictionary-background-and-motivation/

來源:微信公衆号:騰訊資料庫技術

出處:https://mp.weixin.qq.com/s/ofp0e4HYjDFyZsBXYR-ueg

繼續閱讀