|||
作者简介:高鹏,笔名八怪。《深入理解MySQL主从原理》图书作者,同时运营个人公众号“MySQL学习”,持续分享遇到的有趣case以及代码解析!
本文只是描述DDL中最慢的一个环节是在完成哪些工作,深入程度有限,如果有误请谅解。主要从DBA的角度来看这个问题,也是我一直比较迷惑的地方。基于代码8.0.23。
本文术语:
我们在做DDL的时候,经常会用到online DDL(inplace),当然某些online DDL是比较慢的比如:
这是因为,这些操作会导致读取全表的主键数据,同时需要新建二级索引或者重建整个表。
在官方文档中我们可以看到有如下一些主要的阶段(做了删减,否则太长):
我们可以通过如下语句在执行DDL时候观察具体达到了哪个阶段:
select *from performance_schema.threads where PROCESSLIST_INFO like '%alter%' \G
*************************** 1. row ***************************
THREAD_ID: 52
NAME: thread/sql/one_connection
TYPE: FOREGROUND
PROCESSLIST_ID: 12
PROCESSLIST_USER: root
PROCESSLIST_HOST: localhost
PROCESSLIST_DB: testPROCESSLIST_COMMAND: Query
PROCESSLIST_TIME: 2 PROCESSLIST_STATE: alter table (read PK and internal sort)
PROCESSLIST_INFO: alter table sales1 add llll int
PARENT_THREAD_ID: NULL
ROLE: NULL
INSTRUMENTED: YES
HISTORY: YES
CONNECTION_TYPE: Socket
THREAD_OS_ID: 10653
接下来我们就对这些阶段进行描述,实际上上面提到了几个阶段,除了alter table (end)以外都存在于DDL语句的执行阶段,执行阶段通常来讲是最费时间的,因为要做的事情本来很多任务也很重。当然整个inplace DDL对应3个阶段的函数如下:
mysql_inplace_alter_table
-> handler::ha_prepare_inplace_alter_table
-> handler::ha_inplace_alter_table
-> handler::ha_commit_inplace_alter_table
本文的分析的步骤,也几乎集中在handler::ha_inplace_alter_table函数下。
用于表示在进行online DDL时,对现有表进行的DML操作记录的日志,这里简单提一下。实际上DML log是以行为单位写入,但是对于Insert/update和delete操作记录的行差别很大,因为delete语句我通常记录主键信息就能做删除操作了,但是insert/update 则需要记录更多的字段。函数参考(row_log_table_delete、row_log_table_update、row_log_table_insert)
同时DML log在内存的写入量是由innodb_sort_buffer_size(8.0.23默认4M)控制的,内部叫做一个block,如果一个block写满后需要写入到临时文件,言外之意每次临时文件的增量为innodb_sort_buffer_size大小。在每行DML log进行写入的时候会判断总的大小是否大于了参数innodb_online_alter_log_max_size(8.0.23默认1G)参数设置大小,如果超过了则会报错:
Creating index '%-.192s' required more than 'innodb_online_alter_log_max_size' bytes of
modification log. Please try again
其次重建主键只需要建立一个主键上的DML log就可以了,但是新建索引需要为每个新建的索引都分配一个DML log。评判是否需要重建主键的标准可能是,是否更改了主键的字段和顺序(先不考虑instant DDL,prepare_inplace_alter_table_dict函数)。
我们还需要注意的是重建主键需要建立一个临时文件放到data目录下,类似“#sql-ib1408-817418612.ibd”的形式。但是新建索引则不需要,将新加入的所以你放到原有的数据文件就可以了。
下面是我观察到的这种临时文件。我的方法对一个大表做了如下到操作:
这个操作会重建主键,注意即便是8.0这个操作也不是instant DDL,因为它改变了字段顺序。我是将断点放到row_log_tmpfile函数上,查看返回临时文件的文件描述符,然后通过lsof找到这个临时文件,也可以在/prod/pid/fd下看到,如下:
(gdb) p log->fd
$4 = 54
[root@mgr4 fd]# lsof -p 5517|grep delete
....
mysqld 5517 mysql 54u REG 8,16 0 284059 /newdata/mysql/mysql8023/tmp/#284059 (deleted)
(DML log临时文件)
mysqld 5517 mysql 55u REG 8,16 29360128 284065 /newdata/mysql/mysql8023/tmp/#284065 (deleted)
mysqld 5517 mysql 56u REG 8,16 29360128 284066 /newdata/mysql/mysql8023/tmp/#284066 (deleted)
这里/newdata/mysql/mysql8023/tmp/#284059就是为主键上的DML log临时文件
[root@mgr4 fd]# lsof -p 5517|grep delete
....
mysqld 5517 mysql 54u REG 8,16 4194304 284059 /newdata/mysql/mysql8023/tmp/#284059 (deleted)
(DML log临时文件)
mysqld 5517 mysql 56u REG 8,16 29360128 284066 /newdata/mysql/mysql8023/tmp/#284066 (deleted)
mysqld 5517 mysql 57u REG 8,16 29360128 284067 /newdata/mysql/mysql8023/tmp/#284067 (deleted)
注意这里有其他的临时文件,这些文件是每个索引做merge操作的临时文件。我们稍微观察一下不管DML log还是merge排序的临时文件这里大小都是4M的整数倍。其实除了DML log,实际上做索引merge操作的时候同样是以innodb_sort_buffer_size(8.0.23默认4M)为block的,因此临时文件大小也是4M的整数倍。
这个阶段是重建的第一个阶段(row_merge_build_indexes开头进入这个状态),主要是在函数row_merge_read_clustered_index中完成逻辑。本阶段完成任务包含:
注意这里有一个名场面btr_bulk->insert(dtuple),往新的主键或者索引中插入数据的时候是通过bulk方式插入的,这有别于我们平时插入数据的改变索引结构的方式,速度更快,但是能力有限不做讨论,有感兴趣的朋友可以看看。
这一个阶段,有几点注意的地方:
因此这个阶段除了读取老的主键数据以外,总会将主键数据插入到新的主键中,而二级索引完成排序后插入到新的二级索引或者临时文件中,使用需要使用到临时文件这取决于你的表大小(是否重建的记录超过sort buffer大小),同时每个需要重建的二级索引均有一个临时文件。
####从这一步开始下面的步骤是循环每个需要重建的索引, LOOP
关于磁盘merge排序的具体步骤不做研究了,简单的说就是读取临时文件中每一个block,注意每一个block是一个sort buffer的大小,然后通过归并的方式,最终排序完成。
显然alter table (merge sort) 和alter table (insert)阶段,对于小表而言通常是不存在的,因为在read PK and internal sort阶段就完成了主键和二级索引的插入操作。这里都是通过是否打开了临时文件判断的。
如果没有重建主键,那么新的二级索引是直接bulk插入到原有space中,但是这个步骤没有记录redo,因此做一次脏数据刷盘。压力小的时候脏数据就少。
如果不重建主键(比如add index,add index操作),那么每个增加的二级索引都需要包含一个这样的DML log,对于每个二级索引的DML log应用只有这里的一次,commit阶段不会再次应用了,这有别于主键上的DML log。这里我们也看到实际插入就是类似我们inset操作做的插入,因此也是比较耗时的操作。压力小的时候的DML log就小,DDL也会更快。
###八、阶段alter table (log apply table)(srv_stage_alter_table_log_table)
这里的应用是几乎不会影响到正常的写入的,也就是生产者还在向DML log写入,而消费者也就是DDL线程也可以消费,直到达到最后一个block。
来到这一步就是已经到了DDL的commit阶段了,在之前会上MDL 的独占锁,代码注释Upgrade to EXCLUSIVE before commit。这里先不考虑(实际上我也还没学习)所有的commit流程,我们只考虑和DML log相关的部分。
在这个阶段会判断是否重建了主键,如果重建了则会调用row_log_table_apply进行最后的DML log应用,同时阶段会切换为alter table (log apply table)(srv_stage_alter_table_log_table)且一直保持到结束。不过这个阶段一般很快就完成了,因此很难观察到。
我们还是以2个SQL为例进行总结描述:
这里面语句1不会重建主键,而语句2由于增加字段且改变了顺序需要重建主键。
因此重建主键需要的空间往往比2倍本表空间还要大,需要注意。
最后一个张流程图,将这些复杂步骤串联起来,也有利于后期复习(高清图在https://www.jianshu.com/p/746610cb5f5b)。
DDL STAGE.png
我的笔记放到另外一个文章,供参考:
参考资料:
合作电话:010-64087828
社区邮箱:greatsql@greatdb.com