破解gh-ost变更导致MySQL表膨胀之谜|得物技术

一、问题背景

业务同学在 OneDBA 平台进行一次正常 DDL 变更完成后(变更内容跟此次问题无关),发现一些 SQL 开始出现慢查,同时变更后的表比变更前的表存储空间膨胀了几乎 100%。经过分析和流程复现完整还原了整个事件,发现了 MySQL 在平衡 B+tree 页分裂方面遇到单行记录太大时的一些缺陷,整理分享。

为了能更好的说明问题背后的机制,会进行一些关键的“MySQL原理”和“当前DDL变更流程”方面的知识铺垫,熟悉的同学可以跳过。

本次 DDL 变更后带来了如下问题:

  • 变更后,表存储空间膨胀了几乎 100%;
  • 变更后,表统计信息出现了严重偏差;
  • 变更后,部分有排序的 SQL 出现了慢查。

现在来看,表空间膨胀跟统计信息出错是同一个问题导致,而统计信息出错间接导致了部分SQL出现了慢查,下面带着这些问题开始一步步分析找根因。

二、索引结构

B+tree

InnoDB 表是索引组织表,也就是所谓的索引即数据,数据即索引。索引分为聚集索引和二级索引,所有行数据都存储在聚集索引,二级索引存储的是字段值和主键,但不管哪种索引,其结构都是 B+tree 结构。

一棵 B+tree 分为根页、非叶子节点和叶子节点,一个简单的示意图(from Jeremy Cole)如下:

由于 InnoDB B+tree 结构高扇区特性,所以每个索引高度基本在 3-5 层之间,层级(Level)从叶子节点的 0 开始编号,沿树向上递增。每层的页面节点之间使用双向链表,前一个指针和后一个指针按key升序排列。

最小存储单位是页,每个页有一个编号,页内的记录使用单向链表,按 key 升序排列。每个数据页中有两个虚拟的行记录,用来限定记录的边界;其中最小值(Infimum)表示小于页面上任何 key 的值,并且始终是单向链表记录列表中的第一个记录;最大值(Supremum)表示大于页面上任何 key 的值,并且始终是单向链表记录列表中的最后一条记录。这两个值在页创建时被建立,并且在任何情况下不会被删除。

非叶子节点页包含子页的最小 key 和子页号,称为“节点指针”。

现在我们知道了我们插入的数据最终根据主键顺序存储在叶子节点(页)里面,可以满足点查和范围查询的需求。

页(page)

默认一个页 16K 大小,且 InnoDB 规定一个页最少能够存储两行数据,这里需要注意规定一个页最少能够存储两行数据是指在空间分配上,并不是说一个页必须要存两行,也可以存一行。

怎么实现一个页必须要能够存储两行记录呢? 当一条记录 <8k 时会存储在当前页内,反之 >8k 时必须溢出存储,当前页只存储溢出页面的地址,需 20 个字节(行格式:Dynamic),这样就能保证一个页肯定能最少存储的下两条记录。

溢出页

当一个记录 >8k 时会循环查找可以溢出存储的字段,text类字段会优先溢出,没有就开始挑选 varchar 类字段,总之这是 InnoDB 内部行为,目前无法干预。

建表时无论是使用 text 类型,还是 varchar 类型,当大小 <8k 时都是存储在当前页,也就是在 B+tree 结构中,只有 >8k 时才会进行溢出存储。

页面分裂

随着表数据的变化,对记录的新增、更新、删除;那么如何在 B+tree 中高效管理动态数据也是一项核心挑战。

MySQL InnoDB 引擎通过页面分裂和页面合并两大关键机制来动态调整存储结构,不仅能确保数据的逻辑完整性和逻辑顺序正确,还能保证数据库的整体性能。这些机制发生于 InnoDB 的 B+tree 索引结构内部,其具体操作是:

  • 页面分裂:当已满的索引页无法容纳新记录时,创建新页并重新分配记录。
  • 页面合并:当页内记录因删除/更新低于阈值时,与相邻页合并以优化空间。

深入理解上述机制至关重要,因为页面的分裂与合并将直接影响存储效率、I/O模式、加锁行为及整体性能。其中页面的分裂一般分为两种:

  • 中间点(mid point)分裂:将原始页面中50%数据移动到新申请页面,这是最普通的分裂方法。
  • 插入点(insert point)分裂:判断本次插入是否递增 or 递减,如果判定为顺序插入,就在当前插入点进行分裂,这里情况细分较多,大部分情况是直接插入到新申请页面,也可能会涉及到已存在记录移动到新页面,有有些特殊情况下还会直接插入老的页面(老页面的记录被移动到新页面)。

表空间管理

InnoDB的B+tree是通过多层结构映射在磁盘上的,从它的逻辑存储结构来看,所有数据都被有逻辑地存放在一个空间中,这个空间就叫做表空间(tablespace)。表空间由段(segment)、区(extent)、页(page)组成,搞这么多手段的唯一目的就是为了降低IO的随机性,保证存储物理上尽可能是顺序的。

三、当前DDL变更机制

在整个数据库平台(OneDBA)构建过程中,MySQL 结构变更模块是核心基础能力,也是研发同学在日常业务迭代过程中使用频率较高的功能之一。

主要围绕对表加字段、加索引、改属性等操作,为了减少这些操作对线上数据库或业务的影响,早期便为 MySQL 结构变更开发了一套基于容器运行的无锁变更程序,核心采用的是全量数据复制+增量 binlog 回放来进行变更,也是业界通用做法(内部代号:dw-osc,基于 GitHub 开源的 ghost 工具二次开发),主要解决的核心问题:

  • 实现无锁化的结构变更,变更过程中不会阻挡业务对表的读写操作。
  • 实现变更不会导致较大主从数据延迟,避免业务从库读取不到数据导致业务故障。
  • 实现同时支持大规模任务变更,使用容器实现使用完即销毁,无变更任务时不占用资源。

变更工具工作原理简单描述(重要)

重点:

简单理解工具进行 DDL 变更过程中为了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值