好文推荐 | MySQL 的 truncate/drop table 问题通关了吗?

科技   2024-11-26 17:11   上海  

文/轻松的鱼
truncate/drop table 的性能问题是一个历史悠久,横跨多个大版本的复杂问题,本文以「官方手册」和「历史bug」为线索从时间顺序上梳理这个问题的表现和官方的优化动作。长篇预警,列个目录抢先看:
    • 以「官方手册」为引

    • 从「历史bug」里找线索

    • 小结-MySQL5.5.23惰性删除策略

    • 未解决的疑问-MySQL5.7究竟还做了什么优化?

    • 事情还没完,MySQL8.0.0-8.0.23:我胡汉三又回来了

    • 8.0.23 帽子戏法:我还没浪够

    • MySQL8.4:要不你别用AHI了

以「官方手册」为引
MySQL5.7的手册有这么一段描述:

说的是在具有大型 InnoDB 缓冲池并启用 innodb_adaptive_hash_index 的系统上,由于删除 InnoDB 表的自适应哈希索引条目时发生 LRU 扫描,TRUNCATE TABLE 操作可能会导致系统性能暂时下降。MySQL 5.5.23 中的 DROP TABLE 问题已得到解决(bug#13704145,bug#64284),但对于 TRUNCATE TABLE 仍然是一个已知问题( bug#68184)。
然后是MySQL8.0的手册:“之前版本 truncate 的性能问题,8.0 已经将 truncate 映射成了 drop table + create table 避免了这个问题”。

一句话总结官方手册的意思:“MySQL 5.5.23解决了 DROP TABLE 的性能问题,8.0 解决了 TRUNCATE TABLE 的性能问题”。但显然这不是事实,在我们的体验中,无论是MySQL5.7还是8.0都依然存在 truncate/drop table 导致的MySQL hang甚至crash。

那么问题来了,为什么官方手册会这么写?为什么性能问题依然存在? 

从「历史bug」里找线索
1. bug#64284 & bug#51325

5.7手册里中正式提到的 bug 是:https://bugs.mysql.com/bug.php?id=64284,但更详细的讨论应该看这个 bug:https://bugs.mysql.com/bug.php?id=51325。

bug 指出 drop table 会做两次 LRU 扫描:一次是从 LRU list 中删除表的数据页,一次是删除表的 AHI 条目。

优化后变成:扫描一次 LRU list 来删除 AHI;扫描一次 flush list 来删除表的数据页;并且buffer pool mutex 会定期释放,不再是一直持有直到扫描完 LRU list(这样持有 mutex 的时间太长,会阻塞其他请求)。

其中 bug#51325 中有些讨论可以帮助我们理解这个bug。

这条评论的大意是对于 60G 的缓冲池和其中分配的约 320 万页,删除一个空的 innodb 表会将缓冲池互斥锁锁定约 0.5 秒,因为它会扫描 LRU 两次:

    • buf_LRU_invalidate_tablespace 会扫描一次LRU list,需要从adaptive hash中删除对要删除的表的page的引用;

    • buf_LRU_invalidate_tablespace 调用的 buf_LRU_drop_page_hash_for_tablespace 会扫描一次LRU list:如果是dirty block,需要从flush list remove掉,然后从page hash中删除,最后从LRU list中删除。

这一条评论大意是:删除表时,对于表在 LRU list 中的 page,惰性失效比同步删除要更合理。如果要删除一个小的或未使用的表,则扫描缓冲池的成本不值得它释放的几个page;即使要删除一个大表,也可能不会从释放大量缓冲池中看到足够的好处(如果有的话)来证明服务中断是合理的,所以我仍然愿意等待这些 page 通过LRU算法正常的失效。

这条评论是关于优化方案:

    • 采用了惰性删除的思路,通过扫描 flush list 来删除对应的 page(替换了扫描 LRU list,因为 flush list 通常比 LRU list 小的多);
    • 但是 LRU 的扫描只是减少了一次,并没有完全消除,删除AHI条目还是要扫一次LRU List;
    • buffer pool mutex 会定期释放,不再是一直持有直到扫描完 LRU list(这样持有 mutex 的时间缩短,如果删除操作需要很长时间,其他线程可以同时工作)。
2. bug#61188

https://bugs.mysql.com/bug.php?id=61188,这个bug指出对于分区表,drop table 会删除多个分区,删除每个分区时都是扫描 LRU list 两次,放大了 bug#51325 的问题。

3. bug#68184

https://bugs.mysql.com/bug.php?id=68184,这个bug 说的是 truncate table 会扫描 LRU 来删除 AHI,导致性能下降;8.0 已修复,方法是将 truncate 映射成 drop table + create table。

注意:这个bug 的开头和结尾都说是因为扫描 LRU 删除 AHI 导致的,甚至官方文档中说的也是“由于删除 InnoDB 表的自适应哈希索引条目时发生 LRU 扫描,可能会导致系统性能暂时下降”。

这里存在疑问,因为从 bug#68284 和 bug#51325 我们得知其实主要还是因为扫描 LRU 删除 page,并且持有 buffer pool mutex 时间过长,5.5.23 修复后还是得扫描 LRU 删除 AHI。

其中有一个评论:truncate table 会重新为表分配一个 space id,因此这张表的 page 留在 LRU list 中是没有问题的(这些 page 的 space id 是旧的,不会被读取到),所以可以和 drop table 一样使用惰性删除进行优化。

备注:这个说法我怀疑是因果倒置了。要知道 MySQL5.7中 truncate table 是不会重新分配 space id 的,因此需要同步删除 LRU list 中的page。再来看 8.0 将 truncate 映射成 drop table + create table,这是为了解决同步删除 page 的问题,8.0 中 truncate table 会重新分配 space id 这件事只是一种修复后的结果。

4. bug#91977

https://bugs.mysql.com/bug.php?id=91977,这个bug说的是 drop table 扫描 LRU 删除 AHI 导致信号量等待,造成长时间的阻塞。这个其实就是 5.5.23 修复方案中没能解决的问题:LRU 的扫描只是减少了一次,并没有完全消除。

5. bug#98869

https://bugs.mysql.com/bug.php?id=98869,这个bug指出虽然 8.0 依旧修复了 truncate table 的问题,但是对于一些查询产生的磁盘临时表(innodb 表),在临时表被删除时,还是会有同样的问题。这个bug在8.0.23中得到修复。

小结-MySQL5.5.23惰性删除策略
前面的信息量很大,我们做一个小结。一切要从 bug#64284 & bug#51325 开始说起:

当时 drop table 的实现中,需要同步删除 LRU list 中表的 page 和 AHI 条目,需要扫描两次 LRU list,持有 buffer pool mutex 直到结束。这在 innodb buffer pool 很大时会很慢,并且由于 buffer pool mutex 会导致其他线程基本上处于堵塞状态,MySQL hang。

在 5.5.23 中用惰性删除策略做了一次优化:

    • 扫描 flush list( 因为 flush list 通常比 LRU list 小的多)删除对应的 page,而 LRU list 上的 page 则依靠 LRU 算法慢慢失效;

    • 但是 LRU 的扫描只是减少了一次,并没有完全消除,删除AHI条目还是要扫一次LRU List;

    • buffer pool mutex 会定期释放,不再是一直持有直到扫描完 LRU list(这样持有 mutex 的时间缩短,如果删除操作需要很长时间,其他线程可以同时工作)。

对于分区表,删除每个分区时都会扫描 LRU List,问题更严重。

truncate table 因为实现问题,在MySQL5.7 中不会重新分配 table space id,为了防止读到已删除表的 page,因此仍然需要同步删除 LRU list 中对应表的的 page,5.5.23的这次优化对 truncate table 无效。MySQL8.0 将 truncate table 映射成 drop table + create table,终于和 drop table 一样使用惰性删除了。
未解决的疑问-MySQL5.7究竟还做了什么优化?
5.7版本的手册里写的是 5.5.23版本修复了 bug#64284,解决了「删除 InnoDB 表的 AHI 条目时发生 LRU List扫描带来的性能问题」。
但显然根据上文的研究,这句话显然是错的。bug#64284 的修复方案是删除date page不再需要同步扫描 LRU List,但是删除 AHI 条目还是要扫LRU List。
或者有其他一种可能:5.7 确实还做了优化,但是文档里没写相关的 bug,只写了一个 bug#64284 把我们误导了。
这个问题困扰了我很久,直到我的同事大春仔细研究了源码后告诉了我答案。5.7确实做了额外的优化,drop table的实现方式:
  1. 删除 buffer pool 中表的索引页,以及索引页的 AHI
    a. 不扫描 LRU list,扫磁盘表空间可以得知索引页在 buffer pool 中的位置,如果索引页的记录在 AHI 中,继续到 AHI 中删除对应记录
  2. 删除 buffer pool 中表的数据页
    a. 扫描 flush list,同步删除表的数据页
    b. LRU list 上的 page 通过 LRU 算法慢慢失效

在 MySQL5.7 中,drop table 确实不需要扫描 LRU。如果表小,扫磁盘表空间很快,比扫 LRU list 成本低很多;如果表很大,扫磁盘表空间也会很耗时,与直接扫LRU相比优势不大。所以其实问题并没有完全解决。

事情还没完-MySQL8.0.0-8.0.23:我胡汉三又回来了

MySQL8.0.0 确实将 truncate 映射成 drop table + create table 了,因此 truncate table 的问题终于也解决了。但是对 AHI 的处理上,又放弃了 5.7 的优化,还是要扫描 LRU list,如果索引页在 AHI 中,则删除对应的 AHI 记录。这个改动似乎回到了 5.5.23,对于小表来说,也要扫描整个 LRU list,成本比较高。

8.0.23 帽子戏法:我还没浪够
把视线再聚焦到8.0.23版本 的 Release Note:

翻译一下就是:删除buffer pool 中的 data page 和 AHI 不再是同步删除,而是被动删除(也就是异步删除、惰性删除)。
但是另一篇官方博客(https://dev.mysql.com/blog-archive/a-250x-improvement-to-tablespace-truncation-in-mysql-server-8-0-23/)到底是把底漏给我们老实人看了:

意思很清楚:虽然 release note 写了 AHI 不再是同步删除,但 8.0.23 版本并未实现,将很快解决。要不说这是帽子戏法呢,官方文档里老是隐藏关键信息,这谁受得了。
还好另一方面 data page 确实是变成惰性删除了,不再需要同步扫描 Flush List。因此确实性能上有提示。只是博客里说「将很快解决」,我查了一遍 release note 和这个博客里的其他文章,再未见其描述,也不知道是不是偷偷修复了。
现在 drop/truncate table 最耗时的操作只剩一个:同步扫描 LRU List 删除 AHI。
MySQL8.4:要不你别用AHI了
前段时间 MySQL突然跳版本发布了 8.4 版本,其中关注到 innodb_adaptive_hash_index 默认值变为 false,也就是说 AHI 默认是关闭的。文档中也提示需要我们根据业务的负载情况进行调整,最好根据实际业务压测开启、关闭AHI的表现:

说实话看到这里我也是迷茫了,truncate/drop table问题通关了吗?同步扫描 LRU List 删除 AHI 这个事大概率是没修好。
最后,如果我们频繁遇到 truncate/drop table 导致性能抖动,甚至 MySQL 信号量 600秒超时触发 Crash,使用 MySQL8.0.23 以上的版本仍不失一个好方法,如果不行,那就关掉 AHI 试试吧。
感谢阅读。



爱可生开源社区
爱可生开源社区,提供稳定的MySQL企业级开源工具及服务,每年1024开源一款优良组件,并持续运营维护。
 最新文章