天天看点

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 组合

继续阅读