一文为你解读 MySQL8.0 Instant DDL 源码实现
一、背景介绍
数据库中每一行数据都被持久化存储在磁盘中。当我们对表进行 ADD/DROP COLUMN 操作时,磁盘中的数据也会相应地被修改,所需时间与对应表的大小成正比。因此,对大表进行 ADD/DROP COLUMN 操作时,花费的时间可能长达数小时或数天,这给用户的业务带来了诸多不便。
MySQL 5.5 版本之前,只支持 DDL 的 COPY 算法。MySQL 5.6 版本后,新增了支持 INPLACE 算法。两者区别在于是否能进行 ONLINE DDL 操作。对于 ADD/DROP COLUMN 操作,无论是 COPY 算法还是 INPLACE 算法,都需要修改用户原有数据,且没有解决耗时问题。
MySQL 8.0.12 版本引入了 INSTANT(即时)算法,对部分 ADD COLUMN 操作,不再修改用户原有数据,只需对表元信息进行修改。因此,操作时间与表大小脱钩,并且操作时间可以到秒级,但这个版本的 INSTANT 算法有以下几点问题:
只支持 INSTANT ADD COLUMN,不支持 INSTANT DROP COLUMN
INSTANT ADD COLUMN 位置只能在已有列的最后,不能指定其他位置
MySQL 8.0.29 版本中推出了 INSTANT 算法新的实现方式,解决了上述的 2 个问题。本文将分析最新 INSTANT 算法的实现方式。
二、INSTANT 语法
注意 :
未指定 ALGORITHM 情况下,所有的 ADD/DROP COLUMNS 会优先尝试 INSTANT 算法
在单条 ALTER TABLE 语句中可以 ADD/DROP COLUMNS 多列
三、原理分析
对于 ADD/DROP COLUMN 操作,之前提到的 COPY/INPLACE 算法都需要重建表,将磁盘中原有 record 进行修改。而 INSTANT 算法只需要修改表元信息,就可以快速完成 ADD/DROP COLUMN 操作。
INSTANT 算法技术上需要解决什么问题?
我们通过一个例子来分析:
因为 INSTANT ADD/DROP 操作不会修改原有数据,所以可以推出数据在磁盘中分布如下:
可以看到,只有数据三的存储内容和当前表的元信息(表的列)匹配,数据三能正常解析读取。数据一多了 C3 列的值,但缺少了 C5 列的值,数据二则多了 C3 列的值。
于是问题出现了:如何正确解析返回老数据。若将解析和返回分开,则可以分为以下两个问题:
第一,解析。如何正确解析老数据?
第二,返回。老数据中缺少列如何填充?
注:这里老数据指的是存储内容和当前列不匹配的数据,上述例子中就是数据一、二。
INSTANT 如何解决上述问题
如何解析
以 COMPACT 行格式为例,我们知道在 record 中存储数据是需要结合表的元数据(列类型、是否为空、是否变长等信息)进行解析的。
上述用例进行了多次 DDL,每次都会修改表的元信息。所以在解析数据一、二、三时,所需的元数据版本肯定是不同的。
我们很自然会想到对元数据信息的历史版本进行维护。在解析数据一的时候,找到与数据一对应的元数据版本;在解析数据二时,找到与数据二对应的元数据版本,以此类推。
MySQL 8.0.29 中 INSTANT DDL 功能的实现,在于引入了元数据 version 的概念。
如何返回
可以在 INSTANT ADD COLUMN 操作时,将新列的默认值存储在元数据中。对于缺少该新列的老数据,在返回客户端时,可直接使用默认值进行填充。
此方式在 MySQL 8.0.12 版本的 INSTANT DDL 就已经实现,并在 MySQL 8.0.29 版本中进行了沿用。
本次 INSTANT 算法的实现重难点在于元数据 version 实现,下文也将着重解释。
四、元数据 version
元数据 version 实现涉及表元数据、列元数据、行格式三部分的修改。
表元数据
在 INFORMATION_SCHEMA.INNODB_TABLES 中新增了 TOTAL_ROW_VERSIONS 字段:记录当前表 VERSION 值,新建的表初始值为 0,每次 INSTANT ADD/DROP 后值递增 1,重建表后清零。
注意:在执行一次 INSTANT ADD/DROP 语句后,表 VERSION 的值会自增 1,而不是针对每次添加或删除的列进行自增。如果一条语句增加了 n 个列,减少了 m 个列,表 VERSION 仍然只会自增 1。
列元数据
在进行 ADD/DROP COLUMN 操作后,列元数据将进行修改。可以在 mysql.columns 表中体现。
新建表列元数据如上图所示,HIDDEN 列表示可见性。三个系统列的可见性为 SE,代表 INNODB 可见,SERVER 层不可见。SE_PRIVATE_DATA 中也只记录着 table_id。
下面将进行 INSTANT ADD COLUMN 操作。
原有列变化(DB_ROLL_PTR、DB_ROW_ID、DB_TRX_ID、C1、C2、C3、C4)
SE_PRIVATE_DATA 中新增了 physical_pos,表示列在 record 中的相对物理位置。为何是相对物理位置,后续说明。
ADD 列变化(C5)
SE_PRIVATE_DATA 中 default
存储列的默认值,对于缺少该列的数据(如数据一),解析返回时将使用默认值。
SE_PRIVATE_DATA 中 physical_pos
新增列的 physical_pos 为原表 physical_pos 最大值加 1,说明该列的物理位置在行尾,此特性与新增列是否指定位置无关。
SE_PRIVATE_DATA 中 version_added
值为本次 INSTANT 操作后表的 VERSION,对于本例,添加后的表的 VERSION 值从 0 到 1,故 version_added 记录为 1。
下面将进行 INSTANT DROP COLUMN 操作
DROP 列变化(C3/ !hidden!_dropped_v2_p5_C3)
name
列元数据中 C3 列的名字已经修改为了 '!hidden!_dropped_v2_p5_C3'。v2 代表在表 version 为 2 的时候删除的,p5 表示此列的 physical_pos 为 5。
可能有人会问:C3 列不是被删除了么,为啥在元数据中还要保留 C3 列?另外,为什么要改名为 '!hidden!_dropped_v2_p5_C3',而不直接沿用 C3 的名字?
那是因为部分 record(数据一、数据二)中还有此列数据,解析 record 时需要所有列元数据,所以 C3 列的元数据需要保留。而关于改名成 '!hidden!_dropped_v2_p5_C3',是因为 C3 列在逻辑上被删除了,后续若添加同名列(重新添加 C3 列),那么应该要添加成功。于是就换成由删除版本信息以及当前物理位置信息共同构成的一个名字。
hidden
将被删除列的可见性设置为 SE,对存储引擎可见,但是对 server 层不可见,和 DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 一样。因此,在 Innodb 层解析出该列数据后,这些数据不会返回到 server 层。
SE_PRIVATE_DATA 中 version_dropped
本次 INSTANT 操作后,表的 VERSION 更新为新的值。对于本例,删除列后的表的 VERSION 值从 1 变成 2,故 version_dropped 记录为 2。
可以看到数据三 C4 列的 physical_pos 值为 6,但是因为数据三中没有 C3 列的数据,所以,C4 列数据实际在位置 5。这说明 physical_pos 记录的是相对物理位置,要结合其他元信息才能确定该 record 中列的具体物理分布情况。
列元数据修改总结:
1. 经过 instant 操作后,所有列都将新增一个 physical_pos 字段记录的相对物理位置。
2. 对于 instant add 的列,新增 version_added 字段来记录添加后的表 version 值,并且列的 physical_pos 值为当前表最大的 physical_pos 值加 1,与该列实际添加的位置无关。
3. 对于 instant drop 的列,元数据将被保留,但列名会进行修改名字,并将 hidden 属性设置为 SE。
行格式
INSTANT 思路是,对于磁盘中的每一行 record,使用不同的元数据去解析。元数据修改如上文所述。但如何确定 record 对应元数据哪一个版本呢?所以需要修改行格式,确保在每条新产生的 record 中记录当前表 VERSION 信息。
本次修改包括:
使用了行格式中原有 INFO BITS 字段的第二个比特位(称为`VERSION BIT`);
行格式新增了 ROW VERSION 字段。
VERSION BIT
INFO BITS 是行格式原有字段,之前 VERSION BIT 所在的第二个比特位未被赋予含义。故已有的 record 此处值都为 0。
本次实现规定:
表 VERSION 为 0 时,此时插入的 record 的 version bit 为 0;
表 VERSION 不为 0 时,此时插入的 record 的 version bit 为 1;
ROW VERSION
本次实现规定:
表 VERSION 为 0 时,此时插入的 record 没有 ROW VERSION 字段;
表 VERSION 不为 0 时,此时插入的 record 有 ROW VERSION 字段;
注:如果表的 VERSION 不为 0,那么表示该表曾经进行过 INSTANT ADD/DROP 操作,且之后未被重建。
下面两图(图 9 和图 10)体现插入和查询时如何使用 version bit 和 row version。
INSTANT 任意位置实现
在列元数据中提到:
1. 进行 INSTANT 操作后,列元数据中会维护一个 physical_pos 字段,用于记录该列在行中的相对物理位置。
2. 对于 INSTANT ADD 的列,physical_pos 值都为原表最大值加 1,说明新列的物理位置在行尾。
从 physical_pos 可知,数据一的实际存储如下,b 列数据在 a 列前面。
而我们期待的返回,a 列应该在 b 列前面(a 列添加时有 FIRST 关键字)。
故需要维护一个逻辑位置和物理位置的对应关系。在 'dict_index_t' 结构体中新增名字为 'fields_array' 的成员。t1 表的 fields_array 如下:
有了这个对应关系,从 record 中解析出的数据,可以以正确的逻辑顺序返回。
五、解析/返回 record
列元数据中引入了 VERSION_ADDED、VERSION_DROPPED。解析/返回一个 record,可以依照下面的规则:
1. 解析数据时,忽略 VERSION_DROPPED > 0 的列(表示已经被删除);
2. 返回结果时,对于 VERSION_ADDED > ROW_VERSION 的情况(record 中无此列数据),使用该列元数据中默认值。
图示:
六、用例总图解
新建表,插入数据一
ADD COLUMN,插入数据二
DROP COLUMN,插入数据三
七、总结
Instant 算法的原理大致如上。实现特点总结:
1. 修改元数据,不修改原有的数据,让 ADD/DROP COLUMN 操作的时间缩短到秒级;
2. 维护元数据版本,并修改行格式,让 record 找到对应的元数据版本进行解析;
3. 维护列的物理位置和逻辑位置的对应关系,以确保能够按照正确顺序将数据返回客户端。
INSTANT ADD/DROP COLUMN 功能同样存在一些小问题,MySQL 8.0.29 版本推出 Instant 算法最新实现后,一开始遇到了不少 bug,导致社区也认为该版本是不稳定的,到 8.0.33 版本之后才逐渐稳定。我们在 MySQL 8.0.37 版本进行测试时,也发现 2 个 bug,并已向社区反馈。
[1] https://bugs.mysql.com/bug.php?id=116035
[2] https://bugs.mysql.com/bug.php?id=115890
社区的改进极大优化了 ADD/DROP COLUMN 的效率,希望通过本文的介绍,读者对最新的实现有进一步的了解,欢迎交流。
参考:https://blogs.oracle.com/mysql/post/mysql-80-instant-add-drop-columns
评论