MySQL 8.0性能下降的深度分析

用户往往更容易注意到低并发性能的下降,而高并发性能的改善则往往难以察觉。因此,维护低并发性能至关重要,因为它直接影响用户体验和升级意愿[1]。

根据大量用户反馈,升级到 MySQL 8.0 后,用户普遍感受到性能下降,特别是在批量插入和连接操作中。这种下降趋势在更高版本的 MySQL 中变得更加明显。此外,一些 MySQL 爱好者和测试人员在升级后报告了多个 sysbench 测试中性能下降的情况。

这些性能问题可以避免吗?或者,更具体地说,我们应该如何科学地评估持续的性能下降趋势?这些都是需要考虑的重要问题。

虽然官方团队持续进行优化,但性能的逐渐恶化不容忽视。在某些场景中,可能会出现改善,但这并不意味着所有场景的性能都得到了同等优化。此外,针对特定场景优化性能也容易以牺牲其他领域的性能为代价。

MySQL 性能下降的根本原因

一般而言,随着功能的增加,代码库也在增长,随着功能的不断扩展,性能变得越来越难以控制。

MySQL 开发人员常常没有注意到性能的下降,因为每次对代码库的添加仅导致性能微小下降。然而,随着时间的推移,这些微小的下降会累积,导致显著的累积效应,使用户在新版本的 MySQL 中感知到明显的性能退化。

例如,以下图表显示了一个简单单连接操作的性能,其中 MySQL 8.0.40 与 MySQL 8.0.27 相比表现出性能下降:

Figure 1. Significant decline in join performance in MySQL 8.0.40.

以下图表显示了单并发下的批量插入性能测试,MySQL 8.0.40 与版本 5.7.44 相比的性能下降:

Figure 2. Significant decline in bulk insert performance in MySQL 8.0.40.

从上面的两张图中可以看出,版本 8.0.40 的性能并不好。

接下来,让我们从代码层面分析 MySQL 性能下降的根本原因。以下是 MySQL 8.0 中的 PT_insert_values_list::contextualize 函数:

C++

 

MySQL 5.7 中对应的 PT_insert_values_list::contextualize 函数如下:

C++

 

从代码比较来看,MySQL 8.0 的代码似乎更优雅,似乎在进步。

不幸的是,很多时候,正是这些代码改进背后的动机导致了性能下降。MySQL 官方团队将之前的 List 数据结构替换为 deque,这已成为逐渐性能下降的根本原因之一。让我们来看一下 deque 的文档:

Markdown

 

如上所述,在极端情况下,保留单个元素可能需要分配整个数组,导致内存效率非常低。例如,在大批量插入时,需要插入大量记录,官方实现将每个记录存储在单独的双端队列中。即使记录内容很少,仍然需要分配一个双端队列。MySQL双端队列实现为每个双端队列分配1KB的内存以支持快速查找。

Plain Text

 

官方实现使用1KB的内存来存储索引信息,即使记录长度不大但记录数量很多,内存访问地址可能变得不连续,导致缓存友好性较差。这种设计旨在提高缓存友好性,但效果并不十分显著。

值得注意的是,原始实现中使用了List数据结构,其中内存是通过内存池分配的,提供了一定程度的缓存友好性。虽然随机访问效率较低,但为了优化对List元素的顺序访问,可以显著提高性能。

在升级到MySQL 8.0期间,用户观察到批量插入性能显著下降,其中一个主要原因是基础数据结构发生了重大变化。

此外,尽管官方团队改进了重做日志机制,但也导致了MTR提交操作效率的降低。与MySQL 5.7相比,新增的代码明显降低了单个提交的性能,尽管整体写入吞吐量得到了很大的提升。

让我们来看一下MySQL 5.7.44中MTR提交的核心execute操作:

C++

 

让我们来看一下 MySQL 8.0.40 中 MTR 提交的核心 execute 操作:

C++

 

相比之下,很明显在 MySQL 8.0.40 中,MTR 提交中的 execute 操作变得更加复杂,涉及的步骤更多。这种复杂性是低并发写性能下降的主要原因之一。

具体来说,操作 m_impl->m_log.for_each_block(write_log)log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn) 有很大的开销。这些变化是为了增强高并发性能,但却以低并发性能为代价。

重做日志对高并发模式的优先级造成了低并发工作负载性能不佳的结果。虽然引入 innodb_log_writer_threads 是为了缓解低并发性能问题,但并不影响上述函数的执行。由于这些操作变得更加复杂,并且需要频繁的 MTR 提交,性能仍然显著下降。

让我们来看一下即时添加/删除功能对性能的影响。以下是 MySQL 5.7 中的 rec_init_offsets_comp_ordinary 函数:

C++

 

在 MySQL 8.0.40 中,rec_init_offsets_comp_ordinary 函数如下:

C++

 

从上述代码可以清楚地看出,引入了即时添加/删除列功能后,rec_init_offsets_comp_ordinary函数变得明显更加复杂,引入了更多的函数调用,并添加了一个严重影响缓存优化的switch语句。由于这个函数被频繁调用,直接影响了更新索引、批量插入和连接的性能,导致了重大性能损失。

此外,在MySQL 8.0中的性能下降不仅限于上述内容;还有许多其他方面导致了整体性能下降,特别是对内联函数扩展的影响。例如,以下代码会影响内联函数的扩展:

C++

 

根据我们的测试,ib::fatal语句严重干扰内联优化。对于频繁访问的函数,建议避免干扰内联优化的语句。

接下来,让我们看一个类似的问题。row_sel_store_mysql_field function被频繁调用,其中的row_sel_field_store_in_mysql_format是其中的一个热点函数。具体代码如下:

C++

 

row_sel_field_store_in_mysql_format函数最终调用了row_sel_field_store_in_mysql_format_func

C++

 

row_sel_field_store_in_mysql_format_func函数由于存在ib::fatal代码而无法内联。

C++

 

频繁调用的低效函数,每秒执行数千万次,会严重影响连接性能。

让我们继续探讨性能下降的原因。以下官方性能优化实际上是连接性能下降的根本原因之一。虽然某些查询可能得到改进,但它们仍然是普通连接操作性能下降的原因之一。

GitHub Flavored Markdown

 

MySQL的问题不仅仅局限于此。正如上文所述,MySQL的性能下降并非无因。一系列小问题,当积累起来时,可能导致用户感受到的明显性能下降。然而,这些问题通常很难识别,使其解决变得更加困难。

所谓的“过早优化”是万恶之源,在MySQL开发中并不适用。数据库开发是一个复杂的过程,长期忽视性能会使后续性能改进变得更加困难。

缓解MySQL性能下降的解决方案

写入性能下降的主要原因与MTR提交问题、即时添加/删除列以及其他几个因素有关。这些问题很难用传统方法进行优化。然而,用户可以通过PGO优化来弥补性能下降。通过合适的策略,性能通常可以保持稳定。

对于批量插入性能下降问题,我们的开源版本[2]将官方的deque替换为改进的列表实现。这主要解决了内存效率问题,并可以部分缓解性能下降。通过将PGO优化与我们的开源版本结合,批量插入性能可以接近MySQL 5.7的水平。

Figure 3. Optimized MySQL 8.0.40 with PGO performs roughly on par with version 5.7.

用户还可以利用多个线程进行并发批量处理,充分利用重做日志的改进并发性,这可以显著提升批量插入性能。

关于更新索引问题,由于新代码的不可避免添加,PGO优化可以帮助缓解这个问题。我们的PGO版本[2]可以显著缓解这个问题。

对于读取性能,特别是连接性能,我们进行了大幅改进,包括修复内联问题和进行其他优化。通过添加PGO,连接性能可以比官方版本提高超过30%。

Figure 4. Using PGO, along with our optimizations, can lead to significant improvements in join performance.

我们将继续投入时间优化低并发性能。这个过程很长,涉及许多需要改进的领域。

开源版本已经可以进行测试,我们将继续努力改进MySQL性能

参考资料

[1] 王斌(2024)。软件工程问题解决之道:如何让MySQL更好。

[2] Enhanced for MySQL · GitHub

Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis