MySQL5.7 新特性: Atomic Truncate

最近在测试MySQL5.7时,随手truncate了一个空表,竟然触发了一次checkpoint操作,每秒写入量达到好几百M,直接把redo log 和脏页刷到底了,显然在生产场景这是不可接受的。

report的bug地址见:http://bugs.mysql.com/bug.php?id=74312

相关堆栈为:

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->space,
                        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:
http://dev.mysql.com/worklog/task/?id=6501  
(注意这个worklog描述的大部分内容是正确的,但关于truncate redo log实际上在后面替换成了一个单独的log 文件,有特定的命名方式)

主要rev:

http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/6092   (最初版本)
以及:
http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/6193   (5.7.2)

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

原创文章,转载请注明: 转载自Simple Life

本文链接地址: MySQL5.7 新特性: Atomic Truncate

Post Footer automatically generated by wp-posturl plugin for wordpress.


Comments

Leave a Reply

Your email address will not be published. Name and email are required


Current month ye@r day *