以社交平台的用户表为例,随着业务的快速增长,用户表user单表数据量越来越大,此时,如果我们想给user表添加索引,数据规模对添加过程的影响势必要考虑在内,但是,单表数据规模对添加索引会产生什么样的影响呢,我们在什么样的数据库请求状态下给大表添加索引比较好呢?
今天,我就详细回答一下上面两个问题:
我们先来看下第一个问题,当我们回答了第一个问题,那么,第二个问题的答案也就浮出水面了。
我们先来看一个结构,它叫Row Log
,用于在DDL过程中记录DML操作的日志文件。
我以user表为例,讲解一下Row Log
。它有如下特点:
index_age_birth
对应的Row Log。Row Log在逻辑上由多个Block组成,每个Block可以存储多个DML操作、一个DML操作也会落在多个Block中。如上图中的Log代表DML操作:
在物理存储上Row Log
分为两部分:
inndob_sort_buffer_size
的Block,用于写入DML操作innodb_sort_buffer_size
,且小于innodb_online_alter_log_max_size
时,写满的Block会刷到磁盘上,空出内存中的Block给后续的Log写入,日志文件中,所有Block总大小如果超过innodb_online_alter_log_max_size
,写入就会报错Row Log
的核心结构如下:
操作flag + 事务id + 操作记录
,其中,操作flag包含两种:INSERT和DELETE,UPDATE看作是先DELETE,再INSERT
。比如,上图第一个Log中包含一条记录<0x61 + 1234 + <25, 1998-01-02, 1>>
,其中,0x61
代表这是一个插入操作,1234
表示这个操作的事务id,<25, 1998-01-02, 1>
表示操作的记录。下面我们再来看下Log是如何追加到Row Log的?我以user表的index_age_birth
索引的Row Log为例来说明:
见上图,从上到下,我们来看下这个追加的过程:
innodb_sort_buffer_size
大小的Block,tail指针指向Block中的第一个Log,如果有Block,tail指针指向Block中最后一个Log。如上图,内存中有Block,tail指向Block中最后一个Log,也就是虚线框前面那个Log
DML操作日志大小 >= innodb_sort_buffer_size - 当前Block中已有Log的总大小,则偏移量为innodb_sort_buffer_size - 当前Block中已有Log的总大小
(2) 如果DML操作日志大小 < innodb_sort_buffer_size - 当前Block中已有Log的总大小,则偏移量为DML操作日志大小
DML操作日志大小 < innodb_sort_buffer_size - 当前Block中已有Log的总大小
,将DML操作日志全部拷贝到Block中末尾Log。如上图,全拷贝最右侧,将DML日志<0x61 + 3355 + <25, 1998-01-02, 1>>
完整拷贝到末尾Log,然后,将tail移到被拷贝的Log上(2) 半拷贝
DML操作日志大小 >= innodb_sort_buffer_size - 当前Block中已有Log的总大小
,拷贝DML操作日志的前面部分到tail后面偏移量大小的空间。如上图半拷贝里的上半部分,将DML日志<0x61 + 3355 + <25, 1998-01-02, 1>>
的前半部分拷贝到末尾Log,然后,将tail移到被拷贝的Log上row_log_file
<0x61 + 3355 + <25, 1998-01-02, 1>>
的后半部分拷贝到Block的头部MySQL将DML日志写到Row Log只是为了在执行DDL期间,可以并行执行DML,最后,这些DML日志还是要更新(回放)到索引树上的,所以,同样以索引index_age_birth
为例,我们再来看下Row Log中的日志是如何更新到索引树的?
从上到下,我们来看上面这张图:
0x61 + 3355 + <25, 1998-01-02, 1>>
中的记录写入索引树index_age_birth
的第一个叶子节点。
(2) 头部Log清空,将head指针移到后面一个Log。如上图,文件扫描中的第二块长方框。
(3) 重复(1)和(2)两步,直到head指针移到Block中最后一个Log,然后,将该Log中的记录写入索引树index_age_birth
。如上图,文件扫描中的第三个长方框及方框中最后一个Log中的记录写入索引树index_age_birth
的第二个叶子节点。。
index_age_birth
,至此,Row Log文件清空。如上图,文件扫描中最后一个虚线长方框,表示Row Log文件清空。
index_age_birth
,如上图,内存扫描部分。
在讲解添加索引的过程之前,还有一个概念再讲解一下,这就是Bulk Load
,在添加索引的过程中,会将已排序的记录批量插入索引树的叶子节点中,这个批量插入的过程就叫做Bulk Load
,我以索引index_age_birth
为例,讲解一下这个过程,见下图:
15, 2008-02-03, 2
和15, 2008-02-06, 5
两条记录,下面的bulk包含16, 2007-06-06, 6
、17, 2006-03-03, 4
和18, 2002-06-07, 3
三条记录。index_age_birth
的第三个叶子节点,下面的bulk记录集插入到索引树index_age_birth
的倒数第二个叶子节点。Row Log的追加和回放,以及Bulk Load是添加索引过程中的核心步骤,讲完这三个步骤,下面我再来看一下InnoDB引擎中MySQL添加索引的过程就比较容易理解了,该过程主要分三个阶段,我以user表为例详细讲解一下:
Prepare阶段:
online_status
为ONLINE_INDEX_CREATION
,表示该表索引都处在在线DDL状态。关于数据字典的结构,我在《我们可以干预MySQL选择正确的执行计划吗?》中有讲解过。DDL执行阶段:
sort_buffer
,大小为innodb_sort_buffer_size/索引叶子节点中最小的记录的大小
(2) 将每一条记录写入sort_buffer
(3) sort_buffer
写满后对里面的记录进行升序排序
(4) sort_buffer
写满了,如果临时文件不存在,就创建一个临时文件
(5) 遍历sort_buffer
记录,将sort_buffer
中的记录写入文件中
a. 生成一个block,将记录添加到block
(6) 将block写入临时文件
Commit阶段:
在讲解完添加索引的过程后,我们发现影响业务DML操作的环节包含:
buffer pool
又满了,触发刷脏行为,这时就会出现查询请求等待刷脏结束,查询响应变慢。可能这时候你会问,Prepare阶段和Commit阶段都加了排它锁,为什么这两个环节不影响DML操作呢?因为虽然这两个阶段都加了排它锁,但是,加锁后的操作都是小数据规模的操作,所以,加锁时间很短,对DML的影响不大,所以,可以忽略不计。
那么,我们看看上面两个问题怎么解决呢? 针对第一个问题,由于表中的原有记录的数量是由业务发展决定的,业务发展快,记录数就会多,这点我们无法控制,所以,针对表数据量大导致扫描聚簇索引变慢,我们只能规避DDL带来的风险,规避方法如下:
结合上面两个因素,如果数据量不大,那么,只要在非极端高峰期执行DDL,对DML的 影响是不大的。如果数据量很大,建议找到MySQL的CPU使用率比较低的情况下做DDL,保证不影响DML操作.。
针对第二个问题,我们可以通过调整参数innodb_sort_buffer_size
,将其调大,使归并排序来源的临时文件中已排序的block数量尽可能少,减少大量block的合并,从而降低磁盘IO
平时我们用的最多的MySQL架构就是主从模式,所以,我们来看一下在这种模式下,在线DDL的过程是怎么样的呢?
通过上面这个过程,你应该已经想到,在DDL和DML并行的阶段,如果产生大量的DML操作,那么,在slave端回放这些DML操作会耗费大量的时间,会影响从库读的数据一致性。所以,这就是主从模式下,在线DDL的问题和风险。
通过本章的讲解,我想你应该对MySQL的在线DDL的机制有了清晰的认识,同时,通过在线DDL机制的讲解,我们也发现了一些优化的方法:
目的 | 解决方法 |
---|---|
减少业务影响 | 调大innodb_sort_buffer_size,降低磁盘IO |
避免DDL过程中写Row Log溢出 | 调大innodb_online_alter_log_max_size |
一定要在高峰期做DDL | 建议使用第三方工具,比如,gh-ost,它是通过binlog完成DDL的,避免了扫描聚簇索引带来的CPU开销 |