关于数据库:MySQL-8029-instant-DDL-数据腐化问题分析

39次阅读

共计 5570 个字符,预计需要花费 14 分钟才能阅读完成。

  • 前言
  • Instant add or drop column 的主线逻辑
  • 表定义的列程序与 row 存储列程序论述
  • 引入 row 版本的必要性
  • 数据腐化问题
  • 起因剖析
  • Bug 重现与解析
  • MySQL8.0.30 修复计划

前言

DDL 绝对于数据库的 DML 之类的其余操作,相对来说是比拟耗时、绝对重型的操作; 因而对业务的影比较严重。MySQL 从 5.6 版本开始始终在继续改良其 DDL 性能:引入了 online DDL,inplace DDL,instant DDL 等实用性极强的性能,DDL 目前对业务的影响继续升高。

MySQL 8.0.29 引入了 instant add/drop column 性能,反对在任意地位增加 column, drop column 也不须要表数据的任何模式的挪动,只须要批改表的元数据就能够实现 add/drop column,所以 instant add/drop column 的操作是轻型操作,速度快,资源需求量少。

ALTER table drop column a, ALGORITHM=INSTANT;

8.0.29 引入了新的 alter 算法 INSTANT。

然而这个新性能目前很不稳固,导致的问题比拟多; 而且通常都比较严重: 数据损坏,或者数据库无奈启动等。

本文是剖析其中的一个问题: 对表进行 instant drop 后,进行 update,之后数据库停机,而后数据库无奈启动。

为剖析这个问题,咱们会从 instant add/drop column 在 Innodb 的实现原理与细节方面来论述这个数据腐化 bug 的具体起因。

Instant add or drop column 的主线逻辑

因为这个性能的 WorkLog 无奈从官网获取,所以无奈失去精确的设计出发点,通过浏览相干代码,得出要实现这个性能,必须要解决以下关键点:

  • 因为要反对在任意地位增加 / 删除列,同时不会更改表数据文件,所以表的逻辑定义与 row 的理论存储模式须要映射关系,不再是所见即所得的一一对应的关系。即为了实现这样性能:
    • 表中列的定义程序与表中行数据 (row) 的存储程序是不同的。
    • 同时对同一个 table 能够做屡次 instant DDL, 所以须要引入版本机制,在表的数据文件中, 不同 row 对应的表定义可能是不同的,须要在 row 中记住表定义的 version。

以上能够认为是该性能的设计准则与实现的主线逻辑。

表定义的列程序与 row 存储列程序论述

在引入这个性能之前,create table 时列定义的程序与列在 InnoDB 中存储的程序是统一的。(这里咱们不必思考 InnoDB 增加零碎暗藏列)

Instant add/drop column 要实现的亮点性能是在表定义的任意地位增加或者缩小 column,同时做这样的操作的时候,可能做到不须要重构表数据。

咱们称 column 在表定义中呈现的程序为 逻辑程序;

而 column 在行数据的存储程序为 物理程序

要做到批改表定义,而不重构表数据,就必须将逻辑程序与物理程序解耦: 不能再像 MySQL 8.0.29 之前的版本那样,逻辑程序与物理程序是完全一致的;而从 8.0.29 开始通过表的元数据保留了逻辑程序与物理程序的映射关系。这种映射关系的构建与保护形成了 instant add/drop column 的根底.

如下图简略论述了逻辑 / 物理程序的关系。

引入 row 版本的必要性

对于同一张表,Instant add/drop DDL 能够执行屡次;每一次执行后,逻辑 / 物理程序的映射关系就发生变化;同时 instant add/drop DDL 并不需要做表数据的重构操作;因而能够得出通过屡次 instant add/drop DDL,InnoDB 存储的行数据与表定义存在多种逻辑 / 物理程序映射关系:比如说,在 ibd 文件中,前十行数据对应原始的表定义,接下来的十行可能对应着 instant add column 后的数据,再接下来的十行,可能对应着 instant drop column 后的数据。

为了治理这种模式的逻辑 / 物理,在 InnoDB 中,为每一行理论存储的数据引入了版本号的概念:每个版本号对应着一个逻辑 / 物理映射关系。

为存储这个版本信息,InnoDB 中,row 的信息头记录的格局有略微的变动:

如上图所示,在 row 的 extra 中存储了其对应的版本号,同时在 row header 中有标记位批示出了是否存在版本号信息。

依据版本号获取相应的映射关系,就能够正确的解析行数据。

目前版本号最大反对到 64,instant add/drop column 达到这个限度后报错;其后如果还须要 instant add/drop column DDL 操作,可能须要做一次可能触发 table rebuild 操作才能够。

数据腐化问题

由 instant add/drop column 引入了多个数据腐化问题,其中一个问题能够从:

[PS-8292] MySQL 8.0.29 fails to perform crash recovery – Percona JIRA(https://jira.percona.com/browse/PS-8292) 查看。

这个问题简略来说:在对表进行 instant drop 后,进行 update 操作,之后 MySQL server 重启,在启动阶段复原之前的 update 操作会引发 assert 解体(debug 版本的状况下)。

从代码上看,这个 bug 可能会造成数据的静默谬误(数据齐全错乱而且不报任何谬误),而不仅仅是解体这一种景象。

通过对 core 文件的简略剖析,造成该问题的大略起因如下:

在通过 redo 做复原的时候,字段的逻辑程序与物理存储程序之间的映射关系不对 (错位) 导致的。在复原期间可能会 找不到对应的字段 ,或者 更新了谬误的字段

起因剖析

从原始的问题看,这个是产生在 InnoDB 启动复原阶段。这一阶段离不开 redo log 的参加。后面介绍 instant add/drop 设计要点的时候,那些列出的要点,能够认为是在在 DDL 期间的工作以及编码的根本逻辑;那么在实现 instant DDL 时候,在 DML 的时候也须要将必要的信息写入 redo log 能力做到 recovery。

  • 为反对 instant add/drop column,redo log 记录的格局产生了变动,因为代码 bug,导致在解析 redo log 做复原的时候,失去的字段信息谬误,导致数据腐化。
  • 问题体现进去可能是: 复原始终无奈执行,数据库无奈启动;还可能是复原到谬误的数据,数据库可能启动。

因为 redo log 的品种较多,信息也比拟繁冗,这里咱们只关注问题自身中呈现的 update 相干的 redo log,进而较多的关注 update redo log 与该问题相干的字段信息。

下图简要的论述了 update redo log 相干内容:

到这里,能够看到 在 MySQL 8.0.29 中,update redo log 引入了 instant column 的物理逻辑程序。

上面从 InnoDB 的复原流程跟踪问题产生的起因,其中次要须要关注的是复原过程中的表 (索引) 定义。

  • 利用 redo log 是在数据库启动阶段最开始就执行,此时数据字典无奈关上,获取不到待复原表的定义信息
  • 然而此时须要表的定义信息去解析 redo log 中的相干数据
  • 此时就会依据 redo log 中记录的长度信息,以及记录长度的程序构建长期的表定义,此时仅仅是为了复原,并不需要准确的表定义,此时只须要晓得 field 的长度和地位即可。
  • 同时如果 redo log 中如果有 instant DDL 的信息,那么也会用这些信息去批改长期构建的表定义: 这是问题产生的初始谬误的中央。
  • 复原过程中,构建出的长期表实际上表中列的逻辑程序,这是合乎失常运行的需要的。
  • 然而实际上 8.0.29 中字段长度的记录程序是按字段 (列) 的物理存储程序写入的。
  • 如果带有 instant DDL 的信息,那么批改表定义时就会按物理程序去批改逻辑程序的表定义,这样会批改到非预期的字段,导致谬误产生!

Bug 重现与解析

CREATE TABLE `tb1` (`col1` VARCHAR(10) NOT NULL,
  `col2` char(13),
  `col3` varchar(11),
  PRIMARY KEY (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO tb1  VALUES ('4000','50','100');
--echo # the FIRST INSTANT ALTER
ALTER TABLE tb1 DROP COLUMN col2, LOCK=DEFAULT;
INSERT INTO tb1  VALUES('4545', '52');
UPDATE tb1 SET col3 = '46' WHERE col1 = '4545';
--echo # crash and restart 1
--source include/kill_and_restart_mysqld.inc
CHECK TABLE tb1;
DROP TABLE tb1;

以上 MySQL MTR 测例能够重现 InnoDB 启动复原期间始终 core 的问题。咱们从这个例子登程,联合下面解释的 instant drop DDL 代码行为看看问题是如何一步步产生的。

  1. 首先阐明一下,在测例运行期间逻辑程序与物理程序的变动。如下图所示略微展现了 table 的逻辑定于与 InnoDB row 存储的以下细节。这里留神的是 被 dropped column 依然会以暗藏列的模式存在于表定于中:因为 drop 之前存在的 row 还是须要这样信息解析字段。
  1. 联合 redo log 的复原过程看看问题产生的第一现场。这里针对这个测例摘取相干 redo log 的局部信息:

    2.1 依照字段长度列表(8.0.29 中是物理程序写入的列表)创立的专门用于复原的表,相似于: create table dummy_table (d1:10, d2:13, d3:11)

    2.2 依照 instant 字段信息批改 dummy 表:依照 physical pos=1 去批改后,后果相似于:create table dummy_table (d1:10, d2:13[dropped], d3:11)

    2.3 冀望的正确的表应该相似于:create table dummy_table(d1:10, d3:11, d2:13[dropped]);

    2.4 Redo log 中的 Field_no=1, 去复原期间望用到的是 #2.3 的表,然而过程中创立的是 #2.2 中谬误的表,这样当 Field_no= 1 去复原数据时,会谬误的发现对应的 field(column)曾经 dropped, 导致 core!

MySQL8.0.30 修复计划

晓得了问题产生的起因, 修复起来就比较简单了:

  • MySQL 8.0.30 的代码修复计划
    • Redo log 中字段的长度列表,依照字段的逻辑程序写入,不再按存储程序写入。
    • 在 redo log 的 instant column 信息中也蕴含了字段的逻辑地位。
    • Redo log 的记录自身的版本设置为 1,与 8.0.29 的版本为 0,做出差异。
    • 8.0.30 的修复代码自身也是不能正确解析 8.0.29 产生的 redo log,只是依据版本号检测出 8.0.29 redo log,进而报错避免数据进一步好转。实际上 8.0.29 的 redo log,在 instant DDL 后,是不可能正确解析的,因为没有逻辑 / 物理的映射关系。
  • 修复的逻辑比较简单:
    • Redo log 中字段的长度列表,依照字段的逻辑程序写入:

      保障在复原阶段构建的长期表是按正确的逻辑定义程序构建的。

    • 在 redo log 的 instant column 信息中也蕴含字段的逻辑地位:

      保障在更新长期表的字段时,依照逻辑程序,不会呈现谬误更新的状况。

上面是 MySQL 8.0.30 update redo log 相干字段信息:

从上图能够看出,MySQL 8.0.30 redo log 中曾经不存储物理地位相干的信息了,全副是逻辑地位相干的信息;这样就和 MySQL 8.0.29 redo log 这种有问题的记录形式是过眼云烟了。

附带的这个测例能够重现数据的静默谬误(复原过程没问题,然而数据实际上错了)

CREATE TABLE `tb2` (`c1` char(4) NOT NULL, `c2` char(4), `c3` char(4), PRIMARY KEY (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
begin;
INSERT INTO tb2  VALUES ('1000','2000','3000');
commit;
--echo # the FIRST INSTANT ALTER
ALTER TABLE tb2 add COLUMN c4 char(4) after c1, LOCK=DEFAULT;
INSERT INTO tb2 VALUES ('1001','4001', '2001', '3001');
SELECT * FROM tb2;
UPDATE tb2 set c4='4002' WHERE c1='1001';
--echo # crash and restart 1
--source include/kill_and_restart_mysqld.inc
select * from tb2;
CHECK TABLE tb2;

须要把这个测例放到 innodb test case suite 中。


Enjoy GreatSQL :)

## 对于 GreatSQL

GreatSQL 是由万里数据库保护的 MySQL 分支,专一于晋升 MGR 可靠性及性能,反对 InnoDB 并行查问个性,是实用于金融级利用的 MySQL 分支版本。

相干链接:GreatSQL 社区 Gitee GitHub Bilibili

GreatSQL 社区:

社区博客有奖征稿详情:https://greatsql.cn/thread-100-1-1.html

技术交换群:

微信:扫码增加 GreatSQL 社区助手 微信好友,发送验证信息 加群

正文完
 0