天天看點

深入解讀MySQL8.0 新特性 :Crash Safe DDL

前言

在MySQL8.0之前的版本中,由于架構的原因,mysql在server層使用統一的frm檔案來存儲表中繼資料資訊,這個資訊能夠被不同的存儲引擎識别。而實際上innodb本身也存儲有中繼資料資訊。這給ddl帶來了一定的挑戰,因為這種架構無法做到ddl的原子化,我們線上上經常能夠看到資料目錄下遺留的臨時檔案,或者類似server層和innodb層列個數不一緻之類的錯誤。甚至某些ddl可能還遺留中繼資料在innodb内,而丢失了frm,導緻無法重建表…..(我們為了解決這個問題,實作了一個叫drop table force的功能,去強制做清理….)

(以下所有的讨論都假定使用InnoDB存儲引擎)

到了8.0版本,我們知道所有的中繼資料已經統一用InnoDB來進行管理,這就給實作原子ddl帶來了可能,幾乎所有的對innodb表,存儲過程,觸發器,視圖或者UDF的操作,都能做到原子化:

- 中繼資料修改,binlog以及innodb的操作都放在一個事務中
- 增加了一個内部隐藏的系統表`mysql.innodb_ddl_log`,ddl操作被記錄到這個表中,注意對該表的操作産生的redo會fsync到磁盤上,而不會考慮innodb_flush_log_at_trx_commit的配置。當崩潰重新開機時,會根據事務是否送出來決定通過這張表的記錄去復原或者執行ddl操作
- 增加了一個post-ddl的階段,這也是ddl的最後一個階段,會去:1. 真正的實體删除或重命名檔案; 2. 删除innodb_ddl_log中的記錄項; 3.對于一些ddl操作還會去更新其動态中繼資料資訊(存儲在`mysql.innodb_dynamic_metadata`,例如corrupt  flag, auto_inc值等)
- 一個正常運作的ddl結束後,其ddl log也應該被清理,如果這中間崩潰了,重新開機時會去嘗試重放:1.如果已經走到最後一個ddl階段的(commit之後),就replay ddl log,把ddl完成掉;2. 如果處于某個中間态,則復原ddl
           

由于引入了atomic ddl, 有些ddl操作的行為也發生了變化:

- DROP TABLE: 在之前的版本中,一個drop table語句中如果要删多個表,比如t1,t2, t2不存在時,t1會被删除。但在8.0中,t1和t2都不會被删除,而是抛出錯誤。是以要注意5.7->8.0的複制問題 (DROP VIEW, CREATE USER也有類似的問題)
- DROP DATABASE: 修改中繼資料和ddl_log先送出事務,而真正的實體删除資料檔案放在最後,是以如果在删除檔案時崩潰,重新開機時會根據ddl_log繼續執行drop database
           

測試:

MySQL很貼心的加了一個選項

innodb_print_ddl_logs

,打開後我們可以從錯誤日志看到對應的ddl log,下面我們通過這個來看下一些典型ddl的過程

root@(none) 11:12:19>SET GLOBAL innodb_print_ddl_logs = 1;                                                                                                                                    
Query OK, 0 rows affected (0.00 sec)

root@(none) 11:12:22>SET GLOBAL log_error_verbosity = 3;                                                                                                                                                       
Query OK, 0 rows affected (0.00 sec)           

CREATE DATABASE

mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.02 sec)
           

建立資料庫語句沒有寫log_ddl,可能覺得這不是高頻操作,如果建立database的過程中失敗了,重新開機後可能需要手動删除目錄。

CREATE TABLE

mysql> USE test;
Database changed
mysql> CREATE TABLE t1 (a INT PRIMARY KEY, b INT);
Query OK, 0 rows affected (0.06 sec)

[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=428, thread_id=7, space_id=76, old_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 428
[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=429, thread_id=7, table_id=1102, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 429
[InnoDB] DDL log insert : [DDL record: FREE, id=430, thread_id=7, space_id=76, index_id=190, page_no=4]
[InnoDB] DDL log delete : by id 430
[InnoDB] DDL log post ddl : begin for thread id : 7
InnoDB] DDL log post ddl : end for thread id : 7           

從日志來看有三類操作,實際上描述了如果操作失敗需要進行的三項逆向操作:删除資料檔案,釋放記憶體中的資料詞典資訊,删除索引btree。在建立表之前,這些資料被寫入到ddl_log中,在建立完表并commit後,再從ddl log中删除這些記錄。

另外上述日志中還有

DDL log delete

日志,其實在每次寫入ddl log時是單獨事務送出的,但在送出之後,會使用目前事務執行一條delete操作,直到操作結束了才會送出。

加列(instant)

mysql> ALTER TABLE t1 ADD COLUMN c INT;
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0


[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log post ddl : end for thread id : 7
           

注意這裡執行的是Instant ddl, 這是8.0.13新支援的特性,加列操作可以隻修改中繼資料,是以從ddl log中無需記錄資料

删列

mysql> ALTER  TABLE t1 DROP COLUMN c;
Query OK, 0 rows affected (2.77 sec)
Records: 0  Duplicates: 0  Warnings: 0


[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=487, thread_id=7, space_id=83, old_file_path=./test/#sql-ib1108-1917598001.ibd]
[InnoDB] DDL log delete : by id 487
[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=488, thread_id=7, table_id=1109, new_file_path=test/#sql-ib1108-1917598001]
[InnoDB] DDL log delete : by id 488
[InnoDB] DDL log insert : [DDL record: FREE, id=489, thread_id=7, space_id=83, index_id=200, page_no=4]
[InnoDB] DDL log delete : by id 489

[InnoDB] DDL log insert : [DDL record: DROP, id=490, thread_id=7, table_id=1108]
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=491, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd, new_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 491
[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=492, thread_id=7, table_id=1108, old_file_path=test/#sql-ib1109-1917598002, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 492
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=493, thread_id=7, space_id=83, old_file_path=./test/t1.ibd, new_file_path=./test/#sql-ib1108-1917598001.ibd]
[InnoDB] DDL log delete : by id 493
[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=494, thread_id=7, table_id=1109, old_file_path=test/t1, new_file_path=test/#sql-ib1108-1917598001]
[InnoDB] DDL log delete : by id 494
[InnoDB] DDL log insert : [DDL record: DROP, id=495, thread_id=7, table_id=1108]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd]

[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=495, thread_id=7, table_id=1108]
[InnoDB] DDL log replay : [DDL record: DROP, id=490, thread_id=7, table_id=1108]
[InnoDB] DDL log post ddl : end for thread id : 7           

這是個典型的三階段ddl的過程:分為prepare, perform 以及commit三個階段:

  • Prepare: 這個階段會修改中繼資料,建立臨時ibd檔案#sql-ib1108-1917598001.ibd, 如果發生異常崩潰,我們需要能把這個臨時檔案删除掉, 是以和create table類似,也為這個idb寫了三條日志:delete space, remove cache,以及free btree
  • Perform: 執行操作,将資料拷貝到上述ibd檔案中,(同時處理online dmllog), 這部分不涉及log ddl操作
  • Commit: 更新資料詞典資訊并送出事務, 這裡會寫幾條日志:
    • DROP : table_id=1108
    • RENAME SPACE: #sql-ib1109-1917598002.ibd檔案被rename成t1.ibd
    • RENAME TABLE: #sql-ib1109-1917598002被rename成t1
    • RENAME SPACE: t1.ibd 被rename成#sql-ib1108-1917598001.ibd
    • RENAME TABLE: t1表被rename成#sql-ib1108-1917598001
    • DROP TABLE: table_id=1108
    • DELETE SPACE: 删除#sql-ib1109-1917598002.ibd

實際上這一步寫的ddl log描述了commit階段操作的逆向過程:将t1.ibd rename成#sql-ib1109-1917598002, 并将sql-ib1108-1917598001 rename成t1表,最後删除舊表。其中删除舊表的操作這裡不執行,而是到post-ddl階段執行

  • Post-ddl: 在事務送出後,執行最後的操作:replay ddl log, 删除舊檔案,清理mysql.innodb_dynamic_metadata中相關資訊
    • DELETE SPACE: #sql-ib1109-1917598002.ibd
    • DROP: table_id=1108

加索引

mysql> ALTER TABLE t1 ADD KEY(b);
Query OK, 0 rows affected (0.14 sec)
Records: 0  Duplicates: 0  Warnings: 0


[InnoDB] DDL log insert : [DDL record: FREE, id=431, thread_id=7, space_id=76, index_id=191, page_no=5]
[InnoDB] DDL log delete : by id 431

[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log post ddl : end for thread id : 7           

建立索引采用inplace建立的方式,沒有臨時檔案,但如果異常發生的話,依然需要在發生異常時清理臨時索引, 是以增加了一條FREE log,用于異常發生時能夠删除臨時索引.

TRUNCATE TABLE

mysql> TRUNCATE TABLE t1;
Query OK, 0 rows affected (0.13 sec)


[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=439, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd, new_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 439
[InnoDB] DDL log insert : [DDL record: DROP, id=440, thread_id=7, table_id=1103]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=442, thread_id=7, space_id=78, old_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 442
[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=443, thread_id=7, table_id=1104, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 443
[InnoDB] DDL log insert : [DDL record: FREE, id=444, thread_id=7, space_id=78, index_id=194, page_no=4]
[InnoDB] DDL log delete : by id 444
[InnoDB] DDL log insert : [DDL record: FREE, id=445, thread_id=7, space_id=78, index_id=195, page_no=5]
[InnoDB] DDL log delete : by id 445

[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=440, thread_id=7, table_id=1103]
[InnoDB] DDL log post ddl : end for thread id : 7           

Truncate table是個比較有意思的話題,在早期5.6及之前的版本中, 是通過删除舊表建立新表的方式來進行的,5.7之後為了保證原子性,改成了原地truncate檔案,同時增加了一個truncate log檔案,如果在truncate過程中崩潰,可以通過這個檔案在崩潰恢複時重新truncate。到了8.0版本,又恢複成了删除舊表,建立新表的方式,與之前不同的是,8.0版本在崩潰時可以復原到舊資料,而不是再次執行。以上述為例,主要包括幾個步驟:

  • 将表t1.ibd rename成#sql-ib1103-1917597994.ibd
  • 建立新檔案t1.ibd
  • post-ddl: 将老檔案#sql-ib1103-1917597994.ibd删除

RENAME TABLE

mysql> RENAME TABLE t1 TO t2;
Query OK, 0 rows affected (0.06 sec)
           

DDL LOG:

[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=450, thread_id=7, space_id=78, old_file_path=./test/t2.ibd, new_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 450
[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=451, thread_id=7, table_id=1104, old_file_path=test/t2, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 451

[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log post ddl : end for thread id : 7           

這個就比較簡單了,隻需要記錄rename space 和rename table的逆操作即可. post-ddl不需要做實際的操作

DROP TABLE

DROP TABLE t2           
[InnoDB] DDL log insert : [DDL record: DROP, id=595, thread_id=7, table_id=1119]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd]

[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=595, thread_id=7, table_id=1119]
[InnoDB] DDL log post ddl : end for thread id : 7           

先在ddl log中記錄下需要删除的資料,再送出後,再最後post-ddl階段執行真正的删除表對象和檔案操作

代碼實作:

主要實作代碼集中在檔案storage/innobase/log/log0ddl.cc中,包含了向log_ddl表中插入記錄以及replay的邏輯。

隐藏的innodb_log_ddl表結構如下

def->add_field(0, "id", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT");
  def->add_field(1, "thread_id", "thread_id BIGINT UNSIGNED NOT NULL");
  def->add_field(2, "type", "type INT UNSIGNED NOT NULL");
  def->add_field(3, "space_id", "space_id INT UNSIGNED");
  def->add_field(4, "page_no", "page_no INT UNSIGNED");
  def->add_field(5, "index_id", "index_id BIGINT UNSIGNED");
  def->add_field(6, "table_id", "table_id BIGINT UNSIGNED");
  def->add_field(7, "old_file_path",
                 "old_file_path VARCHAR(512) COLLATE UTF8_BIN");
  def->add_field(8, "new_file_path",
                 "new_file_path VARCHAR(512) COLLATE UTF8_BIN");
  def->add_index(0, "index_pk", "PRIMARY KEY(id)");
  def->add_index(1, "index_k_thread_id", "KEY(thread_id)");           

記錄類型

根據不同的操作類型,可以分為如下幾類:

  1. FREE_TREE_LOG

    目的是釋放索引btree,入口函數

    log_DDL::write_free_tree_log

    ,在建立索引和删除表時會調用到

對于drop table中涉及的删索引操作,log ddl的插入操作放到父事務中,一起要麼送出要麼復原

對于建立索引的case, log ddl就需要單獨送出,父事務将記錄标記删除,這樣後面如果ddl復原了,也能将殘留的index删掉。

  1. DELETE_SPACE_LOG

入口函數:

Log_DDL::write_delete_space_log

用于記錄删除tablespace操作,同樣分為兩種情況:

  1. drop table/tablespace, 寫入的記錄随父事務一起送出,并在post-ddl階段replay
  2. 建立tablespace, 寫入的記錄單獨送出,并被父事務标記删除,如果父事務復原,就通過replay删除參與的tablespace
  3. RENAME_SPACE_LOG

Log_DDL::write_rename_space_log

用于記錄rename操作,例如如果我們把表t1 rename成t2,在其中就記錄了逆向操作t2 rename to t1.

在函數

Fil_shard::space_rename()

中,總是先寫ddl log, 再做真正的rename操作. 寫日志的過程同樣是獨立事務送出,父事務做未送出的删除操作

  1. DROP_LOG

Log_DDL::write_drop_log

用于記錄删除表對象操作,這裡不涉及檔案層操作,寫ddl log在父事務中執行

  1. RENAME_TABLE_LOG

Log_DDL::write_rename_table_log

用于記錄rename table對象的逆操作,和rename space類似,也是獨立事務送出ddl log, 父事務标記删除

  1. REMOVE_CACHE_LOG

Log_DDL::write_remove_cache_log

用于處理記憶體表對象的清理,獨立事務送出,父事務标記删除

  1. ALTER_ENCRYPT_TABLESPACE_LOG

Log_DDL::write_alter_encrypt_space_log

用于記錄對tablespace加密屬性的修改,獨立事務送出. 在寫完ddl log後修改tablespace page0 中的加密标記

綜上,在ddl的過程中可能會送出多次事務,大概分為三類:

  • 獨立事務寫ddl log并送出,父事務标記删除, 如果父事務送出了,ddl log也被順便删除了,如果父事務復原了,那就要根據ddl log做逆操作來復原ddl
  • 獨立事務寫ddl log 并送出, (目前隻有ALTER_ENCRYPT_TABLESPACE_LOG)
  • 使用父事務寫ddl log,在ddl結束時送出。需要在post-ddl階段處理

post_ddl

如上所述,有些ddl log是随着父事務一起送出的,有些則在post-ddl階段再執行, post_ddl發生在父事送出或復原之後: 若事務復原,根據ddl log做逆操作,若事務送出,在post-ddl階段做最後真正不可逆操作(例如删除檔案)

Log_DDL::post_ddl -->Log_DDL::replay_by_thread_id

根據執行ddl的線程thread id通過innodb_log_ddl表上的二級索引,找到log id,再到聚集索引上找到其對應的記錄項,然後再replay這些操作,完成ddl後,清理對應記錄

崩潰恢複

在崩潰恢複結束後,會調用

ha_post_recover

接口函數,進而調用innodb内的函數

Log_DDL::recover()

, 同樣的replay其中的記錄,并在結束後删除記錄。但ALTER_ENCRYPT_TABLESPACE_LOG類型并不是在這一步删除,而是加入到一個數組ts_encrypt_ddl_records中,在之後調用

resume_alter_encrypt_tablespace

來恢複操作,

參考文檔

1. 官方文檔 2. wl#9536: support crash safe ddl