天天看點

MySQL5.7 新特性: Atomic Truncate

最近在測試mysql5.7時,随手truncate了一個空表,竟然觸發了一次checkpoint操作,每秒寫入量達到好幾百m,直接把redo log 和髒頁刷到底了,顯然在生産場景這是不可接受的。

相關堆棧為:

ha_innobase::truncate->row_truncate_table_for_mysql->log_make_checkpoint_at

一個小小的truncate竟然觸發了一次完全的checkpoint,這到底是為什麼?帶着這個問題,我們來看看在mysql5.7中對truncate table邏輯的相關改動

0.background

在5.7中,開始支援原子的truncate table,這意味着truncate操作是可復原,可恢複的。

但如下的場景可能不支援atomic truncate:

不支援全文索引

存在外鍵限制的場景

分區表

主備架構來看,不是原子的,因為binlog無法復原.

truncate的主要實作在新增檔案row/row0trunc.cc中. 完成通過c++ 類的方式來實作,這和5.6及之前版本是很大的變化,實際上,5.7已經幾乎完全在重構成c++,這對像我這樣習慣了c語言風格的人是個不小的挑戰…

主要包含以下幾個類:

include/row0trunc.h

truncate_t :用于記錄truncate log資訊的類

      |—> index_t   //index 類,crash recovery時從日志中擷取,并建構index資訊

truncatelogparser: 用于掃描并解析truncate 日志記錄

row/row0trunc.cc

indexiterator: 用于周遊索引記錄,不支援mvcc, 被sysindexiterator類引用到

sysindexiterator: sysindex table iterator, 用于在系統表sys_indexes中檢索指定table id資訊

class callback: 回調基類,包含如下子類

     |—>truncatelogger:用于建立truncate日志檔案和記錄, ref:truncatelogger::operator()

     |—>dropindex:用于在truncate表的過程中drop 索引, ref :dropindex::operator()

     |—>createindex:用于在truncate表的過程中建立索引, ref:createindex::operator()

     |—>tablelocator:用于在系統表中查找對應table_id, ref: tablelocator::operator()

1. truncate操作過程

這裡我們隻考慮普通的使用者表的執行路徑

入口函數:

ha_innobase::truncate —> row_truncate_table_for_mysql:

step1: truncate合法性檢查,判斷表是否損壞,ibd miss,或者bid已經被discard了

row_truncate_sanity_checks

然後做一次redo checkpoint (!!!!!!!) —— 目前來看是比較可怕的行為,會把undo和髒頁一刷到底,這也是bug#74312提到的問題

log_make_checkpoint_at(lsn_max, true);

根據注釋,做checkpoint的原因是:

       – log checkpoint is done before starting truncate table to ensure

        that previous redo log entries are not applied if current truncate

        crashes. consider following use-case:

         – create table …. insert/load table …. truncate table (crash)

         – on restart table is restored …. truncate table (crash)

         – on restart (assuming default log checkpoint is not done) will have

           2 redo log entries for same table. (note 2 redo log entries

           for different table is not an issue).

step 2: 如果表不是臨時表,開啟事務

trx_start_for_ddl(trx, trx_dict_op_table);

step 3:

row_mysql_lock_data_dictionary(trx)

dict_operation_lock && dict_sys->mutex

step 4:等待所有背景線程停止使用該表

dict_stats_wait_bg_to_stop_using_table(table, trx);

通過标記table->stats_bg_flag來判定

step5: 檢查是否存在外鍵限制

err = row_truncate_foreign_key_checks(table, trx);

或者是否有memcache dml 引用該表(table->memcached_sync_count)

如果上述存在,則truncate失敗.

移除表上所有的記錄鎖(表鎖除外):

lock_remove_all_on_table(table, false); (疑問:都truncate到innodb層了,不應該存在記錄鎖的,因為外層mdl鎖就可以保證這一點了)

step 6: 為truncate事務配置設定復原段

                err = trx_undo_assign_undo(

                        trx, &trx->rsegs.m_redo, trx_undo_update);

step 7: 配置設定新的table id .

為什麼需要新的table id ? purge and rollback: we assign a new table id for the table. since purge and rollback look for the table based on the table id, they see the table as ‘dropped’ and discard their operations

dict_hdr_get_new_id(&new_id, null, null, table, false);

同時檢查表上是否存在全文索引。。。以下我們隻考慮普通使用者表,

step 8.

a) x lock表上所有索引dict_table_x_lock_indexes(table);

b)對于非臨時表,且不存在全文索引,并且不是系統表時,調用 row_truncate_prepare(table, &flags); 做必要的檢查,并保證表上面沒有pending的操作,如果insert buffer merge(fil_ibuf_check_pending_ops), pending io等

對于全文索引,直接調用err = row_truncate_fts(table, new_id, trx); 這裡不展開了.

c)  生成truncate的undo 日志,這也是atomic truncate的核心,即可以通過redo來進行恢複操作,大概分為下面幾步來完成日志記錄

logger = ut_new_nokey(truncatelogger(table, flags, new_id));

err = logger->init();

err = sysindexiterator().for_each(*logger);

err = logger->log();

上調用會建立一個單獨的日志檔案,來儲存truncate的表的相關資訊,以便于crash recovery後重建

例如:

sudo cat /u01/my575/data/ib_469_439_trunc.log

檔案名種的兩個數字取自:

(gdb) p logger->m_table->space

$17 = 469

(gdb) p logger->m_table->id

$18 = 439

分别表示table id 及聚集索引id。

step 9: 删除表上所有的索引以及為索引配置設定的page

dropindex       dropindex(table, no_redo);

err = sysindexiterator().for_each(dropindex);

并重新初始化table space的header

        if (!is_system_tablespace(table->space)

            && !dict_table_is_temporary(table)

            && flags != ulint_undefined) {

                fil_reinit_space_header(

                        table->space,

                        table->indexes.count + fil_ibd_file_initial_size + 1);

        }

在函數fil_reinit_space_header中,會将屬于該tablespace的page抛棄(buf_lru_flush_or_remove_pages),同時還抛棄change buffer中的記錄(ibuf_delete_for_discarded_space)

step 10: 重建新的索引

createindex     createindex(table, no_redo);

err = sysindexiterator().for_each(createindex);

然後釋放所有的索引鎖

dict_table_x_unlock_indexes(table);

step 11: 更新系統表(sys_tables)中的table id 為新配置設定的table id.

                err = row_truncate_update_system_tables(

                        table, new_id, has_internal_doc_id, no_redo, trx);

調用棧:

row_truncate_update_system_tables->row_truncate_update_system_tables->row_truncate_update_table_id

更新dict cache資訊

dict_table_change_id_in_cache(table, new_id);

step 12: 清理階段,重置auto-inc為1,送出事務,并釋放所有的鎖

        dict_table_autoinc_lock(table);

        dict_table_autoinc_initialize(table, 1);

        dict_table_autoinc_unlock(table);

        if (trx_is_started(trx)) {

                trx_commit_for_mysql(trx);

        return(row_truncate_complete(table, trx, flags, logger, err));

函數row_truncate_complete中完成最後的清理工作(包括commit 和rollback之後都需要調用):

…釋放dict 鎖,row_mysql_unlock_data_dictionary(trx)

…checkpoint …

…重置stop_new_ops和is_being_truncated,讓該表恢複io操作

               dberr_t err2 = truncate_t::truncate(

                        table->data_dir_path,

                        table->name, flags, false);

…更新表統計資訊

dict_stats_update(table, dict_stats_empty_table);

2. truncate操作crash recovery階段

如果在崩潰恢複時存在truncate log檔案的話,掃描并解析

innobase_start_or_create_for_mysql

           err = truncatelogparser::scan_and_parse(srv_log_group_home_dir)

                    |—>truncate->parse  (truncate_t::parse()

                    |—>truncate_t::add(truncate) : 解析出來并建構的truncate_t被存儲到truncate_t::s_tables這個static變量

           /*一系列正常crash recovery後*/

           err = truncate_t::fixup_tables();  //根據之前解析的資訊恢複truncate,繼續完成truncate.

               具體的truncate恢複流程不展開說了.

worklog:

(注意這個worklog描述的大部分内容是正确的,但關于truncate redo log實際上在後面替換成了一個單獨的log 檔案,有特定的命名方式)

主要rev:

以及:

相關rev

8723, 8566, 7912, 7755, 7530,7247,7245, 6221, 6207,6198,6196, 6193,6171,6102, 6096,6094

下一篇: Java 組合

繼續閱讀