暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL之Online DDL

GrowthDBA 2021-08-17
1642



在生产环境有DDL真实使用经验的同学,会发现一个情况,有的时候执行一个ALTER TABLE ... 操作很快就完成了,但是有的时候执行就要耗费很长时间,发生这样的原因是为什么呢,今天我们来一探究竟。




为什么快 OR 为什么慢





  • 为什么快?

有的ALTER TABLE ... 执行速度很快,是因为涉及的变更没有涉及到真实存放的数据,即只修改了元数据信息,何为元数据(MetaData)?元数据,是指定义数据结构的数据。例如数据库中的数据库名、表名、 列名、用户名、版本名以及从SQL语句得到的结果中的大部分字符串是元数据。举例来看一下:

存储引擎为InnoDB的表,磁盘上会生成两个物理文件,分别是.frm、.ibd,.frm就是表结构定义文件,我们通过MySQL官方提供的mysql-utilities工具包中的mysqlfrm来看一下.frm文件中存放的内容(因为.frm文件是二进制格式的,我们不能使用常规的Linux操作系统查看文本的方式来看,会出现乱码):
[172.xx.xx.xxx:root@sz-pg-backup-zookeeper-003:/mysql/mysql3307/employees]# usr/bin/mysqlfrm --diagnostic mysql/mysql3307/employees/employee_test.frm
# WARNING: Cannot generate character set or collation names without the --server option.
# CAUTION: The diagnostic mode is a best-effort parse of the .frm file. As such, it may not identify all of the components of the table correctly. This is especially true for damaged files. It will also not read the default values for the columns and the resulting statement may not be syntactically correct.
# Reading .frm file for /mysql/mysql3307/employees/employee_test.frm:
# The .frm file is a TABLE.
# CREATE TABLE Statement:

CREATE TABLE `employees`.`employee_test` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(42) NOT NULL,
  `last_name` varchar(48) NOT NULL,
  `gender` enum('M','F') NOT NULL,
  `hire_date` date NOT NULL,
PRIMARY KEY `PRIMARY` (`emp_no`),
KEY `idx_birth_date` (`birth_date`)
) ENGINE=InnoDB;

#
...done.

从分析出的内容来看,.frm存放的就是表结构定义语句,即表的元数据信息。综上所述,我们知道,有一部分DDL操作因为快,是只修改元数据的,所以,我们来做一个测试:
[root@localhost][employees]> SELECT current_timestamp();
+---------------------+
| current_timestamp() |
+---------------------+
| 2021-08-16 11:48:45 |
+---------------------+
1 row in set (0.00 sec)

[root@localhost][employees]> ALTER TABLE `employee_test` COMMENT '雇员测试表';
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

此操作我们为表employee_test添加一个注释,执行前我们获取了一下当前的时间戳,可以看出执行速度很快,那么我们来看下物理文件的情况:

从图中,我们可以看出,.frm后缀的文件最后的修改日期和时间戳的日期可以对上,而.ibd文件的最后修改日期没有变化。再来验证一下.frm文件的内容,发现增加了我们刚才添加注释的内容。
[172.xx.xx.xxx:root@sz-pg-backup-zookeeper-003:/mysql/mysql3307/employees]# usr/bin/mysqlfrm --diagnostic mysql/mysql3307/employees/employee_test.frm
# WARNING: Cannot generate character set or collation names without the --server option.
# CAUTION: The diagnostic mode is a best-effort parse of the .frm file. As such, it may not identify all of the components of the table correctly. This is especially true for damaged files. It will also not read the default values for the columns and the resulting statement may not be syntactically correct.
# Reading .frm file for /mysql/mysql3307/employees/employee_test.frm:
# The .frm file is a TABLE.
# CREATE TABLE Statement:

CREATE TABLE `employees`.`employee_test` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(42) NOT NULL,
  `last_name` varchar(48) NOT NULL,
  `gender` enum('M','F') NOT NULL,
  `hire_date` date NOT NULL,
PRIMARY KEY `PRIMARY` (`emp_no`),
KEY `idx_birth_date` (`birth_date`)
) ENGINE=InnoDB COMMENT '雇员测试表';

#
...done.

由此可证,此DDL只修改了元数据信息,同时,相比较真实数据的.ibd文件,一般情况下.frm的物理大小小于.ibd文件,这也是执行快的原因之一。只修改元数据信息的DDL还有很多,例如,给字段添加注释、表的重命名等,大家有时间可以自己尝试,这里就不一一赘述了。
  • 为什么慢?

除了只涉及修改元数据信息的DDL之外,还有很大一部分DDL会涉及调整表数据,再看一例:
[root@localhost][employees]> SELECT current_timestamp();
+---------------------+
| current_timestamp() |
+---------------------+
| 2021-08-16 11:59:31 |
+---------------------+
1 row in set (0.00 sec)

[root@localhost][employees]> ALTER TABLE `employee_test` ENGINE=InnoDB;
Query OK, 0 rows affected (3.19 sec)
Records: 0 Duplicates: 0 Warnings: 0

ALTER TABLE ... ENGINE=InnoDB;是经典回收表空间的语句,这个DDL就会涉及调整表数据,同时执行时间比只修改元数据信息的DDL慢了300多倍。

同时,你会发现,.frm和.ibd文件的最后修改时间、文件大小都发生了变化,如果一张表的数据量非常大,那么.ibd文件同样也会非常大,操作一次DDL的时间成本和资源成本可想而知,至此,你知道为什么有的DDL执行速度快,有的DDL执行速度慢了吧。




小提示


这里拓展一下MySQL官方提供的mysql-utilities工具包的安装:
# 从官网中下载mysql-utilities工具包安装包
wget -q https://downloads.mysql.com/archives/get/p/30/file/mysql-utilities-1.6.5.tar.gz
# 解压
tar xf mysql-utilities-1.6.5.tar.gz
# 进入解压目录
cd mysql-utilities-1.6.5
# 通过Python构建
python setup.py build
# 通过Python安装
python setup.py install
# 查看工具包中都有哪些工具
ll usr/bin/mysql*
lrwxrwxrwx 1 root root    30 4月 19 15:45 usr/bin/mysql3306 -> usr/local/mysql3306/bin/mysql
lrwxrwxrwx 1 root root    30 7月 14 18:11 usr/bin/mysql3307 -> usr/local/mysql3307/bin/mysql
lrwxrwxrwx 1 root root    30 5月 12 19:06 usr/bin/mysql3386 -> usr/local/mysql3386/bin/mysql
-rwxr-xr-x 1 root root 11969 8月  3 11:15 usr/bin/mysqlauditadmin
-rwxr-xr-x 1 root root 12210 8月  3 11:15 usr/bin/mysqlauditgrep
-rwxr-xr-x 1 root root 15941 8月  3 11:15 usr/bin/mysqlbinlogmove
-rwxr-xr-x 1 root root  7996 8月  3 11:15 usr/bin/mysqlbinlogpurge
-rwxr-xr-x 1 root root  3764 8月  3 11:15 usr/bin/mysqlbinlogrotate
-rwxr-xr-x 1 root root 17960 8月  3 11:15 usr/bin/mysqldbcompare
-rwxr-xr-x 1 root root 16196 8月  3 11:15 usr/bin/mysqldbcopy
-rwxr-xr-x 1 root root 14793 8月  3 11:15 usr/bin/mysqldbexport
-rwxr-xr-x 1 root root 14163 8月  3 11:15 usr/bin/mysqldbimport
-rwxr-xr-x 1 root root 12371 8月  3 11:15 usr/bin/mysqldiff
-rwxr-xr-x 1 root root  7386 8月  3 11:15 usr/bin/mysqldiskusage
-rwxr-xr-x 1 root root 17249 8月  3 11:15 usr/bin/mysqlfailover
-rwxr-xr-x 1 root root 18222 8月  3 11:15 usr/bin/mysqlfrm
-rwxr-xr-x 1 root root 10957 8月  3 11:15 usr/bin/mysqlgrants
-rwxr-xr-x 1 root root  6462 8月  3 11:15 usr/bin/mysqlindexcheck
-rwxr-xr-x 1 root root  5358 8月  3 11:15 usr/bin/mysqlmetagrep
-rwxr-xr-x 1 root root  5975 8月  3 11:15 usr/bin/mysqlprocgrep
-rwxr-xr-x 1 root root  7695 8月  3 11:15 usr/bin/mysqlreplicate
-rwxr-xr-x 1 root root 16913 8月  3 11:15 usr/bin/mysqlrpladmin
-rwxr-xr-x 1 root root  6408 8月  3 11:15 usr/bin/mysqlrplcheck
-rwxr-xr-x 1 root root 15548 8月  3 11:15 usr/bin/mysqlrplms
-rwxr-xr-x 1 root root  6696 8月  3 11:15 usr/bin/mysqlrplshow
-rwxr-xr-x 1 root root 12066 8月  3 11:15 usr/bin/mysqlrplsync
-rwxr-xr-x 1 root root  8984 8月  3 11:15 usr/bin/mysqlserverclone
-rwxr-xr-x 1 root root  5962 8月  3 11:15 usr/bin/mysqlserverinfo
-rwxr-xr-x 1 root root  6415 8月  3 11:15 usr/bin/mysqlslavetrx
-rwxr-xr-x 1 root root  6959 8月  3 11:15 usr/bin/mysqluc
-rwxr-xr-x 1 root root  8049 8月  3 11:15 usr/bin/mysqluserclone

可以看到,工具安装完成后,会在/usr/bin目录下生成很多可执行文件,其中mysqlfrm也在其中,工具的具体用法可通过下面的命令查看:
/usr/bin/mysqlfrm --help


重建表的两种方式:Copy & Inplace





相比起执行时间短、处理速度快只修改元数据信息的DDL而言,我们更在意的是会调整真实数据、执行速度慢、影响范围广的DDL语句。何为“调整数据”?我们知道,InnoDB存储引擎的数据是以B+树数据结构进行组织存储的,当然还有很多如B*树等数据结构,比如我们调整字段字符编码或排序规则,因为调整了字符编码和排序规则,所以我们不得不对现有数据进行重新排序来保证B+树结构的顺序性,这个调整的过程我们称为表的重建,另外,重建的过程还会对表空间做“瘦身”,即让表空间变得更紧凑,释放更多的空余空间。在MySQL 5.5版本之前,重建表都是通过Copy的方式来实现的。

  • 何为Copy?
官档中给出的释义(MySQL 5.7):
https://dev.mysql.com/doc/refman/5.7/en/alter-table.html
COPY: Operations are performed on a copy of the original table, and table data is copied from the original table to the new table row by row. Concurrent DML is not permitted.
# COPY:对原表的一个副本进行操作,将表数据从原表逐行复制到新表中。不允许并发DML。

首先回顾一下MySQL三层架构:连接层、Server、存储引擎层,Copy的过程首先会在Server层创建一张临时表temp',然后将源表的数据转存到临时表temp’上,交换表名:源表 → temp',temp' → 源表,最终删除temp'表,显然,花时间最多的步骤是往临时表转存数据的过程,如果在这个过程中,有新的数据要写入到源表的话,就会造成数据丢失。同时,在整个DDL过程中,源表是可读的,会消耗一倍的存储空间,但是不能对源表有更新,即不支持DML。因为这个过程对于业务来说是有影响的,可以这么说,这个DDL不是Online的。
[root@localhost][employees]> ALTER TABLE `employee_test` ENGINE=InnoDB, ALGORITHM=copy;
Query OK, 299962 rows affected (3.27 sec)
Records: 299962 Duplicates: 0 Warnings: 0

  • 何为Inplace?
官档中给出的释义(MySQL 5.7):
INPLACE: Operations avoid copying table data but may rebuild the table in place. An exclusive metadata lock on the table may be taken briefly during preparation and execution phases of the operation. Typically, concurrent DML is supported.
# INPLACE:操作避免复制表数据,但可能会就地重建表。在操作的准备和执行阶段可能会短暂地对表进行独占元数据锁定。通常,支持并发DML。

Inplace直译为原地,官档解释:就地重建表,与Copy方式的重建表不同,整个DDL过程都在存储引擎层InnoDB内部完成。对于Server层来说,没有把数据挪动到临时表,是一个“原地”操作。同时,在整个DDL过程中,源表的数据是可读可写,即支持并发DML操作的。整个过程对于业务来说是几乎没有影响的,所以可以这么说,这个DDL是Online的。




小提示


其实,支持Inplace的DDL也有有两种情况,一种情况是会重建表,即会rebuild,就是说,即使没有Server层创建临时表、转存数据、更换表名,删除旧表等一系列麻烦操作,但InnoDB存储引擎层也会保证B+树数据结构的顺序性而“原地”重建一次表数据。还有一种情况是不会重建表,比如,刚开始介绍的只修改元数据的DDL操作,即为no-rebuild。



Online DDL的执行过程 & 注意事项





从上面的介绍中我们得知,Copy重建表方式的DDL会阻塞DML操作。那么,不会阻塞DML操作的Online DDL又是怎么工作的,我们一起来揭晓。在MySQL 5.5版本之前,重建表都是通过Copy的方式来实现的。MySQL 5.6版本开始引入的Online DDL,对这个操作流程做了优化。具体的过程如下:

1、建立一个临时文件,扫描源表主键的所有数据页;
2、用数据页中源表的记录生成B+树,存储到临时文件中;
3、生成临时文件的过程中,将所有对源表的操作记录在一个日志文件(row log)中;
4、临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与源表相同的数据文件;
5、用临时文件替换源表的数据文件。

和Copy方式不同,由于日志文件记录和重放操作这个功能的存在,这个方案在重建表的过程中,允许对源表做增删改操作。这也就是Online DDL名字的来源。

还有一个问题需要注意,在MySQL 5.5版本中引入了MDL(MetaData Lock),MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,维护元数据的一致性、保证读写的正确性。MDL锁是表级锁,是Server层的锁,主要用于隔离DML和DDL操作之间的干扰。当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。根据锁兼容矩阵来看:

读写锁之间、写锁之间是互斥的,如果互斥,那么DDL还能是Online的吗?下面就从加锁角度来描述一下整个过程:
* PREPARE阶段:
a. 创建新的临时.frm文件;
b. 持有MDL写锁(EXCLUSIVE-MDL),禁止读写;
c. 根据ALTER类型确定执行方式(copy、online-rebuild、online-norebuild);
d. 更新数据字典的内存对象;
e. 分配row_log对象用来记录增量;
f. 生成新的临时ibd文件;

* DDL阶段:
g. 降级MDL写锁(EXCLUSIVE-MDL)为MDL读锁(SHARED_UPGRADABLE-MDL);
h. 扫描old_table的聚簇索引每一条记录record,并遍历新表的索引进行处理;
i. 根据record构造对应的索引项,将构造索引项插入soft_buffer块排序;
j. 将sort_buffer块更新到新的索引上;
k. 记录DDL执行过程中产生的增量并在新表上重放;
l. 记录DDL执行过程中产生的增量(仅rebuild类型需要);
m. 重放row_log间产生DML操作append到row_log最后一个block;

* COMMIT阶段:
n. 当前block为row_log最后一个时,禁止读写,升级到MDL写锁(EXCLUSIVE-MDL);
o. 重做row_log中最后一部分增量;
p. 更新InnoDB的数据字典表;
q. 提交事务(刷事务的redo日志);
r. 修改统计信息;
s. rename临时.ibd文件、.frm文件。

整个过程中,如果申请和升级MDL写锁的步骤没有锁冲突,相对于整个DDL过程来说,锁的时间非常短。Online DDL最耗时的过程就是DDL阶段拷贝数据到临时表的过程,这个步骤的执行期间可以接受增删改操作,对业务来说,就可以认为是Online的。
Online DDL的使用限制与问题:
1、仍然存在排他锁,有锁等待的风险;
2、增量日志大小是有限制的;
[root@localhost][(none)]> SHOW VARIABLES LIKE 'innodb_online_alter_log_max_size';
+----------------------------------+-----------+
| Variable_name                    | Value     |
+----------------------------------+-----------+
| innodb_online_alter_log_max_size | 134217728 |
+----------------------------------+-----------+
1 row in set (0.00 sec)

innodb_online_alter_log_max_size参数的默认值是128M,如果在DDL过程中,row_log大小超过这个值,那么就会报错ERROR 1062 (23000): Duplication entry
3、有可能造成主从延迟;
4、无法暂停,只能中断;
5、VARCHAR数据类型最好不要跨越255字节门槛,可能会导致Online DDL失效。




小提示


※ Online DDL的使用建议:

在使用Online DDL的时候,建议在业务低峰期执行,每次操作前,一定要去查看一下官档,因为DDL语句种类繁多,有些情况理论上的不重建,也许还是会重建,官档中也给出了详细说明:

https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html

* 表示含有不确定因素,所以使用对应语法前一定要做好测试,数据量不大的时候,可以直接使用,如果数据量很大,还是不建议使用Online DDL,推荐使用pt-osc或者gh-ost,这是两个不错的开源工具,后面会开辟单独的文章进行讲解。

※ Online和Inplace的关系:

  • DDL过程如果是Online的,就一定是Inplace的

  • 反过来未必,也就是说Inplace的DDL,有可能不是Online的。截止到MySQL 8.0,添加全文索引(FULLTEXT INDEX)和空间索引 (SPATIAL INDEX) 就属于这种情况

ALTER TABLE `t` ADD FULLTEXT(`field_name`);

这个过程是Inplace的,但会阻塞增删改操作,是非Online的。

※ 总结一些元数据锁信息:

名称类型‍‍优先级说明‍‍
SHARED_UPGRADABLE共享升级锁0一般在执行DDL时在Online情况下会产生该锁
EXCLUSIVE‍‍排他锁X1一般在执行DDL时会产生该锁
SHARED_NO_READ_WRITE‍‍排他锁X1执行lock tables xxx write产生该锁
SHARED_WRITE‍‍意向排他锁IX2一般执行DML的更新语句 或 select ... for update产生该锁
SHARED_READ‍‍意向共享锁IS2.5select ... lock in share mode产生该锁(8.0版本以后使用select...for share)
SHARED_READ_ONLY‍‍共享锁S3执行lock tables xxx read产生该锁
  • SHARED_UPGRADABLE:(ALTER TABLE语句)本身为读锁,但是有特殊,优先级为0;
  • 并不受队列中的写锁等待而阻塞,只和当前持锁的SESSION比对,当前持锁SESSION为排他锁X,则等待,反之则获得锁;
  • 为了保证一张表同时只有一个DDL操作进行,SHARED_UPGRADABLE之间是互斥的,一个时间只能有一个SHARED_UPGRADABLE的状态是GRANTED ,其余被阻塞;
  • EXCLUSIVE、SHARED_NO_READ_WRITE级别相同,先进先出;
  • SHARED_WRITE(DML)和SHARED_READ(SELECT)兼容,但SHARED_WRITE_ONLY优先级高于SHARED_READ(LOCK TABLE T READ)且不兼容;
  • SHARED_WRITE_ONLY优先级最低,主要是因为被SHARED_WRITE互斥,但如果只有SHARED_READ则他们的优先级是兼容的。


小结





今天,我们知道了有的DDL会触发rebuild操作,而rebuild又分为Copy和Inplace两种方式。同时我们也学习了Online DDL的原理、执行流程和注意事项,通过讲解,可以在以后使用起来游刃有余。同时,我们也知道了Online和Inplace的关系:如果DDL过程是Online的,则一定是Inplace的,反之则不是,添加全文索引和空间索引就是个例,他们是Inplace的,但不是Online的。同时,也扩展介绍了一些元数据锁的信息。本章内容工作中使用比重较大,使用过程中一定要谨慎。每天进步一点点!~





参考资料:
* 《MySQL实战45讲》
* https://blog.csdn.net/jiaona_chen123/article/details/106900346
* MySQL官方文档


end


文章转载自GrowthDBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论