MySQL | 滚动执行DDL

2023年 10月 27日 85.3k 0

概述

我们知道 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…

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论