ToB企服应用市场:ToB评测及商务社交产业平台

标题: MySQL 8.0.29 instant DDL 数据腐化问题分析 [打印本页]

作者: 羊蹓狼    时间: 2023-6-12 16:29
标题: MySQL 8.0.29 instant DDL 数据腐化问题分析
  前言

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 存储列顺序阐述

在引入这个功能之前, 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。

因为 redo log 的种类较多,信息也比较繁杂,这里我们只关注问题本身中出现的 update 相关的 redo log ,进而较多的关注 update redo log 与该问题相关的字段信息。
下图简要的阐述了 update redo log 相关内容:

到这里,可以看到 在MySQL 8.0.29中,update redo log 引入了 instant column 的物理逻辑顺序。
下面从 InnoDB 的恢复流程跟踪问题发生的原因,其中主要需要关注的是恢复过程中的表(索引)定义。

Bug重现与解析

  1. CREATE TABLE `tb1` (
  2.   `col1` VARCHAR(10) NOT NULL,
  3.   `col2` char(13),
  4.   `col3` varchar(11),
  5.   PRIMARY KEY (`col1`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  7. INSERT INTO tb1  VALUES ('4000','50','100');
  8. --echo # the FIRST INSTANT ALTER
  9. ALTER TABLE tb1 DROP COLUMN col2, LOCK=DEFAULT;
  10. INSERT INTO tb1  VALUES( '4545', '52' );
  11. UPDATE tb1 SET col3 = '46' WHERE col1 = '4545';
  12. --echo # crash and restart 1
  13. --source include/kill_and_restart_mysqld.inc
  14. CHECK TABLE tb1;
  15. DROP TABLE tb1;
复制代码
以上MySQL MTR 测例可以重现 InnoDB 启动恢复期间始终 core 的问题。我们从这个例子出发,结合上面解释的 instant drop DDL 代码行为看看问题是如何一步步发生的。

MySQL8.0.30修复方案

知道了问题发生的原因,修复起来就比较简单了:

下面是MySQL 8.0.30 update redo log 相关字段信息:

从上图可以看出,MySQL 8.0.30 redo log 中已经不存储物理位置相关的信息了,全部是逻辑位置相关的信息;这样就和MySQL 8.0.29 redo log 这种有问题的记录方式是昙花一现了。
附带的这个测例可以重现数据的静默错误(恢复过程没问题, 但是数据实际上错了)
  1. CREATE TABLE `tb2` ( `c1` char(4) NOT NULL, `c2` char(4), `c3` char(4), PRIMARY KEY (`c1`)
  2. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  3. begin;
  4. INSERT INTO tb2  VALUES ('1000','2000','3000');
  5. commit;
  6. --echo # the FIRST INSTANT ALTER
  7. ALTER TABLE tb2 add COLUMN c4 char(4) after c1, LOCK=DEFAULT;
  8. INSERT INTO tb2 VALUES ('1001','4001', '2001', '3001');
  9. SELECT * FROM tb2;
  10. UPDATE tb2 set c4='4002' WHERE c1='1001';
  11. --echo # crash and restart 1
  12. --source include/kill_and_restart_mysqld.inc
  13. select * from tb2;
  14. CHECK TABLE tb2;
复制代码
需要把这个测例放到innodb test case suite中。
  
Enjoy GreatSQL
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4