概述
我们知道 MySQL InnoDB 引擎中的表是索引组织表(IOT),数据是基于主键顺序存放的。这对于基于主键的查询,排序等操作是有天然优势,但是在做 DDL 变更时,会带来一些困扰。相比Oracle(默认是堆表),加列可以迅速完成对比,有着不小的差距,当然这是数据结构决定的,并不是说Oracle这方面做的怎么好(做的确实很好),如果 Oracle 也是用 IOT,估计存在同样的问题(不是很确定)。
那么对于生产系统,DDL 操作会对业务造成影响,比如会产生复制延迟,导致读写分离失效,会阻塞DML等;DMP 目前支持 5.7、8.0版本,自 5.6 版本开始官方声称已经支持 Online DDL,不过还是存在一些坑。
那么什么是 Online DDL?这个功能引入的目的其实就是为了在 DDL 期间不阻塞其他会话的 DML 操作。
截止到 8.0 版本,DDL有三种算法,即Copy、Inplace以及Instant。
- COPY : 是指DDL时,会生成(临时)新表,将原表数据逐行拷贝到新表中,在此期间会阻塞DML
- INPLACE: 无需拷贝全表数据到新表,但可能还是需要IN-PLACE方式(原地,无需生成新的临时表)重建整表。这种情况下,在DDL的初始准备和最后结束两个阶段时通常需要加排他MDL锁(metadata lock,元数据锁),除此外,DDL期间不会阻塞DML
- INSTANT: 只需修改数据字典中的元数据,无需拷贝数据也无需重建整表,同样,也无需加排他MDL锁,原表数据也不受影响。整个DDL过程几乎是瞬间完成的,也不会阻塞DML。这个新特性是8.0.12引入的
这里可以知道,执行成本是从上至下,也就是Copy的执行成本是最高的,其次是Inplace,最后Instant是几乎瞬间完成。
下图是在不同版本下不同操作所产生的影响:
指标解释:
- Instant:此变更可以"立刻"完成
- In Place:此变更由 InnoDB 引擎独立完成,不需要使用 Redo log 等,可以节省开销
- Rebuild Table:此变更会重建聚簇索引,一般情况下,涉及到数据变更时才需要重建聚簇索引
- Permits Concurrent DML:此变更进行时,是否允许其他 DML 变更同一张表。此特性关系到变更是
- 否会长时间阻塞业务。
- Only Modifies Metadata:此变更是否只变更元信息,不涉及数据变更。
Online DDL 执行过程
上图为 DDL 的大致流程,讲的比较细致,大体其实可以分为三大部分(也就是关注红框部分):
- PREPARE:会申请MDL X锁,然后更新数据字典并分配row_log开始记录表上的DML增量数据,这个过程如果没有被MDL锁阻塞,那么是非常快的。
- DDL:将所持有的MDL X锁降级为MDL S锁。
-
- 一方面进行数据拷贝,比如建二级索引或者重建原表。
- 另一方面记录这期间产生的DML日志,写到row_log中后进行数据重放。
- COMMIT:等到重放至最后一个block时,从MDL S锁升级到MDL X锁,回放最后一个block,最后更新数据字典,完成DDL,释放MDL锁。
下面使用五个典型案例来说明 DDL 变更成本(变更成本由低到高)
1、设置列的默认属值
ALTER TABLE `t1`
ALTER COLUMN `c1` SET DEFAULT '1';
2、删除二级索引
ALTER TABLE `t1`
DROP INDEX `idx1`;
3、创建二级索引
ALTER TABLE `t1`
ADD INDEX `idx1` (`name`(10) ASC);
4、删除列
ALTER TABLE `t1`
DROP COLUMN `c1`;
5、变更列的数据类型
ALTER TABLE `t1`
MODIFY COLUMN `c1` INTEGER;
从如上图可以看出,增删索引这类常用的 DDL 语句,并不能做到Instant。
DDL 对 MySQL 产生的影响
1、产生主从延迟
滚动 DDL 实现原理
首先,滚动 DDL的前提条件是 MySQL 必须是主从架构(最好是3节点),并且要保证客户侧没有直连数据库(会影响读一致性)。
下面看一下基本实现思路:
假设我们有一套1主2从的MySQL 集群:
第一步:Slave 1 执行 DDL 变更
第二步:Slave 2 执行 DDL 变更
第三步:此时两个 Slave 已经完成了所有变更,此时做一次Switchover,将Slave 1 或者 2提升为新 Master
第四步:切换成功,原主执行 DDL 变更
至此,已完成所有 DDL 变更。
这是一个理想状态,可能会存在失败的可能,并且 DDL 开始时应将只读连接切换到另外2个没有执行DDL的节点,防止 DDL 被阻塞。
这里的关键点只要保证切换之前我们的 DDL 变更成功执行即可,如果真的发生了一些DDL错误,最不好的结果就是直接重做这一台从库。
DDL 实验
根据“概述”以及“滚动 DDL”中的概念和实现步骤,下面模拟一下实验;
为了达到真实效果,提前做如下准备:
- 准备一个脚本,模拟生产上的DML操作;
- 使用SysBench造数据
- 分别对5.7、8.0执行 DDL 操作
脚本
1、模拟OLTP,每秒执行一次select、insert、update
#!/bin/bash
while true
do
echo "######"
echo `date`
mysql -h10.106.101.14 -uroot -pQwer1234 -e "
use sbtest;
set @v_id=FLOOR(RAND() * 10000000)+1;
select * from sbtest1 where id=@v_id;
insert into sbtest1 values(default,FLOOR(RAND() * 10000000)+1,uuid(),uuid());
update sbtest1 set k=FLOOR(RAND() * 10000000)+1 where id=@v_id;
exit
"
echo `date`
echo "######"
echo
sleep 1
done
2、模拟大事务
select * from sbtest1 t1,sbtest1 t2
order by rand() limit 10;
造数据
创建一张2000W行的表
#5.7
mysql -h10.106.101.4 -uroot -pQwer1234 -e "create database sbtest;"
sysbench /usr/share/sysbench/oltp_read_write.lua
--mysql-host=10.106.101.4 --mysql-user=root --mysql-password=Qwer1234
--db-ps-mode=disable --report-interval=1 --forced-shutdown=1
--mysql-db=sbtest
--tables=1 --table-size=20000000
--threads=1
prepare
#8.0
mysql -h10.106.101.14 -uroot -pQwer1234 -e "create database sbtest;"
sysbench /usr/share/sysbench/oltp_read_write.lua
--mysql-host=10.106.101.14 --mysql-user=root --mysql-password=Qwer1234
--db-ps-mode=disable --report-interval=1 --forced-shutdown=1
--mysql-db=sbtest
--tables=1 --table-size=20000000
--threads=1
prepare
场景 1:设置列的默认值
5.7
ALTER TABLE `sbtest1`
ALTER COLUMN `k` SET DEFAULT '1';
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> ALTER COLUMN `k` SET DEFAULT '1';
Query OK, 0 rows affected (0.20 sec)
Records: 0 Duplicates: 0 Warnings: 0
瞬间完成
8.0
ALTER TABLE `sbtest1`
ALTER COLUMN `k` SET DEFAULT '1';
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> ALTER COLUMN `k` SET DEFAULT '1';
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
瞬间完成
场景 2:删除二级索引
5.7
ALTER TABLE `sbtest1`
DROP INDEX `k_1`;
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> DROP INDEX `k_1`;
Query OK, 0 rows affected (0.10 sec)
Records: 0 Duplicates: 0 Warnings: 0
8.0
ALTER TABLE `sbtest1`
DROP INDEX `k_1`;
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> DROP INDEX `k_1`;
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
场景 3:创建二级索引
5.7
ALTER TABLE `sbtest1`
ADD INDEX `k_1` (`k`);
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> ADD INDEX `k_1` (`k`);
Query OK, 0 rows affected (33.95 sec)
Records: 0 Duplicates: 0 Warnings: 0
会产生 frm 临时文件
Sangfor:DBVM/host-fefcfe70d9d0 /db_data/mysql/data/sbtest # ll
total 7163984
-rw-r----- 1 mysql mysql 61 Jun 15 19:02 db.opt
-rw-r----- 1 mysql mysql 8632 Jun 15 19:21 sbtest1.frm
-rw-r----- 1 mysql mysql 2453667840 Jun 15 19:22 sbtest1.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:08 sbtest2.frm
-rw-r----- 1 mysql mysql 2441084928 Jun 15 19:09 sbtest2.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:08 sbtest3.frm
-rw-r----- 1 mysql mysql 2441084928 Jun 15 19:09 sbtest3.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:21 '#sql-9381_f78.frm'
8.0
ALTER TABLE `sbtest1`
ADD INDEX `k_1` (`k`);
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> ADD INDEX `k_1` (`k`);
Query OK, 0 rows affected (28.18 sec)
Records: 0 Duplicates: 0 Warnings: 0
场景 4:删除列
5.7
ALTER TABLE `sbtest1`
DROP COLUMN `pad`;
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> DROP COLUMN `pad`;
Query OK, 0 rows affected (2 min 0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
产生 frm、ibd临时文件
Sangfor:DBVM/host-fefcfe70d9d0 /db_data/mysql/data/sbtest # ll
total 7409696
-rw-r----- 1 mysql mysql 61 Jun 15 19:02 db.opt
-rw-r----- 1 mysql mysql 8632 Jun 15 19:21 sbtest1.frm
-rw-r----- 1 mysql mysql 2453667840 Jun 15 19:23 sbtest1.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:08 sbtest2.frm
-rw-r----- 1 mysql mysql 2441084928 Jun 15 19:09 sbtest2.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:08 sbtest3.frm
-rw-r----- 1 mysql mysql 2441084928 Jun 15 19:09 sbtest3.ibd
-rw-r----- 1 mysql mysql 8604 Jun 15 19:23 '#sql-9381_f78.frm'
-rw-r----- 1 mysql mysql 251658240 Jun 15 19:23 '#sql-ib353-3527892213.ibd'
还原
ALTER TABLE `sbtest1`
add COLUMN `pad` char(60);
8.0
ALTER TABLE `sbtest1`
DROP COLUMN `pad`;
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> DROP COLUMN `pad`;
Query OK, 0 rows affected (2 min 45.22 sec)
Records: 0 Duplicates: 0 Warnings: 0
产生临时文件
Sangfor:DBVM/host-fefcfe907c6f /db_data/mysql/data/sbtest # ll
total 9113628
-rw-r----- 1 mysql mysql 2453667840 Jun 15 18:28 sbtest1.ibd
-rw-r----- 1 mysql mysql 2436890624 Jun 15 17:57 sbtest2.ibd
-rw-r----- 1 mysql mysql 2436890624 Jun 15 17:57 sbtest3.ibd
-rw-r----- 1 mysql mysql 2004877312 Jun 15 18:28 '#sql-ib1063-572597130.ibd'
还原,否则脚本会一直报错
ALTER TABLE `sbtest1`
add COLUMN `pad` char(60);
场景 5:变更列的数据类型
5.7
ALTER TABLE `sbtest1`
MODIFY COLUMN `k` char(60);
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> MODIFY COLUMN `k` char(60);
Query OK, 10000862 rows affected (4 min 29.77 sec)
Records: 10000862 Duplicates: 0 Warnings: 0
脚本卡住
mysql> show processlist;
+------+------------------+--------------------+--------+------------------+------+---------------------------------------------------------------+------------------------------------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+------+------------------+--------------------+--------+------------------+------+---------------------------------------------------------------+------------------------------------------------------------------------------+
| 5 | sangforroot | 10.106.48.5:38876 | NULL | Sleep | 5 | | NULL |
| 6 | sangforroot | 10.106.48.5:38878 | NULL | Sleep | 5 | | NULL |
| 7 | sangforroot | 10.106.48.5:38880 | NULL | Sleep | 5 | | NULL |
| 8 | sangforroot | 10.106.48.5:38884 | NULL | Sleep | 4849 | | NULL |
| 16 | sangforroot_repl | 10.106.101.2:55328 | NULL | Binlog Dump GTID | 4859 | Master has sent all binlog to slave; waiting for more updates | NULL |
| 18 | sangforroot_repl | 10.106.101.3:47282 | NULL | Binlog Dump GTID | 4858 | Master has sent all binlog to slave; waiting for more updates | NULL |
| 3960 | root | 10.106.99.99:49556 | sbtest | Query | 34 | copy to tmp table | ALTER TABLE `sbtest1`
MODIFY COLUMN `k` char(60) |
| 4698 | root | 10.106.99.99:50428 | sbtest | Query | 34 | Waiting for table metadata lock | insert into sbtest1 values(default,FLOOR(RAND() * 10000000)+1,uuid(),uuid()) |
| 4716 | sangforroot | 10.106.48.5:55458 | mysql | Sleep | 6 | | NULL |
| 4721 | sangforroot | localhost | NULL | Query | 0 | starting | show processlist |
+------+------------------+--------------------+--------+------------------+------+---------------------------------------------------------------+------------------------------------------------------------------------------+
10 rows in set (0.00 sec)
并且产生frm、ibd临时文件
Sangfor:DBVM/host-fefcfe70d9d0 /db_data/mysql/data/sbtest # ll
total 7012432
-rw-r----- 1 mysql mysql 61 Jun 15 19:02 db.opt
-rw-r----- 1 mysql mysql 8632 Jun 15 19:26 sbtest1.frm
-rw-r----- 1 mysql mysql 2030043136 Jun 15 19:28 sbtest1.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:08 sbtest2.frm
-rw-r----- 1 mysql mysql 2441084928 Jun 15 19:09 sbtest2.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:08 sbtest3.frm
-rw-r----- 1 mysql mysql 2441084928 Jun 15 19:09 sbtest3.ibd
-rw-r----- 1 mysql mysql 8632 Jun 15 19:28 '#sql-9381_f78.frm'
-rw-r----- 1 mysql mysql 268435456 Jun 15 19:28 '#sql-9381_f78.ibd'
8.0
ALTER TABLE `sbtest1`
MODIFY COLUMN `k` char(60);
#执行结果
MySQL [sbtest]> ALTER TABLE `sbtest1`
-> MODIFY COLUMN `k` char(60);
Query OK, 10002517 rows affected (4 min 32.80 sec)
Records: 10002517 Duplicates: 0 Warnings: 0
此时会发现,脚本会卡住(DML),不影响查询。
mysql> show processlist;
+-------+------------------+---------------------+--------+------------------+------+-----------------------------------------------------------------+------------------------------------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-------+------------------+---------------------+--------+------------------+------+-----------------------------------------------------------------+------------------------------------------------------------------------------+
| 10 | sangforroot | 10.106.48.5:49408 | NULL | Sleep | 0 | | NULL |
| 11 | sangforroot | 10.106.48.5:49412 | NULL | Sleep | 0 | | NULL |
| 12 | sangforroot | 10.106.48.5:49410 | NULL | Sleep | 0 | | NULL |
| 13 | sangforroot | 10.106.48.5:49416 | NULL | Sleep | 4498 | | NULL |
| 21 | sangforroot_repl | 10.106.101.12:54962 | NULL | Binlog Dump GTID | 4508 | Source has sent all binlog to replica; waiting for more updates | NULL |
| 23 | sangforroot_repl | 10.106.101.13:39072 | NULL | Binlog Dump GTID | 4507 | Source has sent all binlog to replica; waiting for more updates | NULL |
| 4989 | root | 10.106.99.99:42804 | sbtest | Query | 182 | copy to tmp table | ALTER TABLE `sbtest1`
MODIFY COLUMN `k` char(60) |
| 10609 | root | 10.106.99.99:50608 | sbtest | Query | 182 | Waiting for table metadata lock | insert into sbtest1 values(default,FLOOR(RAND() * 10000000)+1,uuid(),uuid()) |
| 10642 | sangforroot | localhost | sbtest | Query | 0 | init | show processlist |
+-------+------------------+---------------------+--------+------------------+------+-----------------------------------------------------------------+------------------------------------------------------------------------------+
9 rows in set (0.01 sec)
Waiting for table metadata lock:Insert 在等待元数据锁
还原
ALTER TABLE `sbtest1`
MODIFY COLUMN `k` int;
实验小结
场景 1、2 在没有未提交事务或大事务前提下,基本都是瞬间完成,不需要做滚动 DDL。
场景 3 创建二级索引可以滚动 DDL,且失败成本较低,因为索引本身不影响数据一致性 。
场景 4 已经涉及到rebuild table,执行时间较长,不过删除列会影响数据一致性以及对业务可能会造成影响,所以需要业务的配合去做滚动 DDL。
场景 5 与场景 4 同理。
下面对场景 3、4、5进行滚动DDL。
滚动 DDL 实验
5.7
场景 3:创建二级索引
1、DBVM 进入维护模式
2、半同步降级,节点 1 执行(节点 1 为主库,2,3分别为从库;后面都有节点 1 2 3 进行解释)
mysql> set global rpl_semi_sync_master_wait_for_slave_count=0;
Query OK, 0 rows affected, 1 warning (0.00 sec)
2、节点 2 停止同步(此时 数据库负载、数据库中间件的只读连接不能指向 节点 2)
mysql> stop slave;
mysql> set global super_read_only=OFF;
3、执行 DDL 语句;创建二级索引
mysql> set sql_log_bin=0;
mysql> ALTER TABLE `sbtest1`
ADD INDEX `k_2` (`c`);
#执行结果
mysql> ALTER TABLE `sbtest1`
-> ADD INDEX `k_2` (`c`);
Query OK, 0 rows affected (3 min 6.74 sec)
Records: 0 Duplicates: 0 Warnings: 0
4、“节点 3”停止同步
mysql> stop slave;
mysql> set global super_read_only=OFF;
5、“节点 3”执行 DDL 语句;创建二级索引
mysql> set sql_log_bin=0;
mysql> ALTER TABLE `sbtest1`
ADD INDEX `k_2` (`c`);
#执行结果
mysql> ALTER TABLE `sbtest1`
-> ADD INDEX `k_2` (`c`);
Query OK, 0 rows affected (2 min 40.37 sec)
Records: 0 Duplicates: 0 Warnings: 0
6、“节点 2”和“节点 3”开启同步
mysql> start slave;
mysql> set global super_read_only=ON;
等待数据同步完成
7、做一次switchover、将”节点 1“切为从库、”节点 2“升级为主库
业务存在一两秒的抖动,属于正常现象。
8、“节点 1”执行 DDL;添加二级索引
mysql> set global super_read_only=OFF;
mysql> ALTER TABLE `sbtest1`
ADD INDEX `k_2` (`c`);
#执行结果
mysql> ALTER TABLE `sbtest1`
-> ADD INDEX `k_2` (`c`);
Query OK, 0 rows affected (3 min 6.74 sec)
Records: 0 Duplicates: 0 Warnings: 0
此时,所有节点已完成“二级索引的添加”,可以再一次执行Switchover,将“节点 1”作为主库,也可以保持现状。
场景 4:删除列
删除列比较特殊,需要应用端先适配,在进行删除列操作;而且在实际生产环境中,删除列的操作相对较少。
下面也按照“场景 3”的步骤,进行滚动 DDL,“删除列”操作。
1、DBVM 进入维护模式
2、半同步降级,节点 1 执行(节点 1 为主库,2,3分别为从库;后面都按“节点 1 2 3 ”进行解释)
mysql> set global rpl_semi_sync_master_wait_for_slave_count=0;
Query OK, 0 rows affected, 1 warning (0.00 sec)
2、“节点 2” 停止同步(此时 数据库负载、数据库中间件的只读连接不能指向 节点 2)
mysql> stop slave;
mysql> set global super_read_only=OFF;
3、执行 DDL 语句;删除列
mysql> set sql_log_bin=0;
mysql> ALTER TABLE `sbtest1`
DROP COLUMN `pad`;
#执行结果
mysql> ALTER TABLE `sbtest1`
-> DROP COLUMN `pad`;
Query OK, 0 rows affected (5 min 4.89 sec)
Records: 0 Duplicates: 0 Warnings: 0
4、“节点 3”停止同步
mysql> stop slave;
mysql> set global super_read_only=OFF;
5、“节点 3”执行 DDL 语句;创建二级索引
mysql> set sql_log_bin=0;
mysql> ALTER TABLE `sbtest1`
DROP COLUMN `pad`;
#执行结果
mysql> ALTER TABLE `sbtest1`
-> DROP COLUMN `pad`;
Query OK, 0 rows affected (4 min 41.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
6、“节点 2”和“节点 3”开启同步
start slave;
set global super_read_only=ON;
等待数据追评
7、做一次switchover、将”节点 1“切为从库、”节点 2“升级为主库
业务存在一两秒的抖动,属于正常现象。
8、“节点 1”执行 DDL;添加二级索引
set global super_read_only=OFF;
ALTER TABLE `sbtest1`
DROP COLUMN `pad`;
#执行结果
mysql> ALTER TABLE `sbtest1`
-> DROP COLUMN `pad`;
Query OK, 0 rows affected (5 min 48.26 sec)
Records: 0 Duplicates: 0 Warnings: 0
DDL 滚动实验总结
生产环境中,最常见的DDL操作为增删字段和索引。
这里删除索引几乎是瞬间完成。
”场景 5“中的变更列的数据类型很少会用到。
所以只要能满足增加、删除索引,以及增加、删除字段即可满足大部分生产环境中的 DDL 操作。
这里最重要的一点是必须要保证数据的一致性,其次才是保证其他如连续性。
运维建议
参考文献
mp.weixin.qq.com/s/n7RIHh20B…
dev.mysql.com/doc/refman/…
大智小技:MySQL DDL 为什么成本高?
大智小技:MySQL 为表添加列是怎么"立刻"完成的
www.cnblogs.com/timePasser-…
juejin.cn/post/685457…