用户往往更容易注意到低并发性能的下降,而高并发性能的改善则往往难以察觉。因此,维护低并发性能至关重要,因为它直接影响用户体验和升级意愿[1]。
根据大量用户反馈,升级到 MySQL 8.0 后,用户普遍感受到性能下降,特别是在批量插入和连接操作中。这种下降趋势在更高版本的 MySQL 中变得更加明显。此外,一些 MySQL 爱好者和测试人员在升级后报告了多个 sysbench 测试中性能下降的情况。
这些性能问题可以避免吗?或者,更具体地说,我们应该如何科学地评估持续的性能下降趋势?这些都是需要考虑的重要问题。
虽然官方团队持续进行优化,但性能的逐渐恶化不容忽视。在某些场景中,可能会出现改善,但这并不意味着所有场景的性能都得到了同等优化。此外,针对特定场景优化性能也容易以牺牲其他领域的性能为代价。
MySQL 性能下降的根本原因
一般而言,随着功能的增加,代码库也在增长,随着功能的不断扩展,性能变得越来越难以控制。
MySQL 开发人员常常没有注意到性能的下降,因为每次对代码库的添加仅导致性能微小下降。然而,随着时间的推移,这些微小的下降会累积,导致显著的累积效应,使用户在新版本的 MySQL 中感知到明显的性能退化。
例如,以下图表显示了一个简单单连接操作的性能,其中 MySQL 8.0.40 与 MySQL 8.0.27 相比表现出性能下降:
以下图表显示了单并发下的批量插入性能测试,MySQL 8.0.40 与版本 5.7.44 相比的性能下降:
从上面的两张图中可以看出,版本 8.0.40 的性能并不好。
接下来,让我们从代码层面分析 MySQL 性能下降的根本原因。以下是 MySQL 8.0 中的 PT_insert_values_list::contextualize
函数:
MySQL 5.7 中对应的 PT_insert_values_list::contextualize
函数如下:
从代码比较来看,MySQL 8.0 的代码似乎更优雅,似乎在进步。
不幸的是,很多时候,正是这些代码改进背后的动机导致了性能下降。MySQL 官方团队将之前的 List
数据结构替换为 deque
,这已成为逐渐性能下降的根本原因之一。让我们来看一下 deque
的文档:
std::deque (double-ended queue) is an indexed sequence container that allows fast insertion and deletion at both its
beginning and its end. In addition, insertion and deletion at either end of a deque never invalidates pointers or
references to the rest of the elements.
As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence
of individually allocated fixed-size arrays, with additional bookkeeping, which means indexed access to deque must
perform two pointer dereferences, compared to vector's indexed access which performs only one.
The storage of a deque is automatically expanded and contracted as needed. Expansion of a deque is cheaper than the
expansion of a std::vector because it does not involve copying of the existing elements to a new memory location. On
the other hand, deques typically have large minimal memory cost; a deque holding just one element has to allocate its
full internal array (e.g. 8 times the object size on 64-bit libstdc++; 16 times the object size or 4096 bytes,
whichever is larger, on 64-bit libc++).
The complexity (efficiency) of common operations on deques is as follows:
Random access - constant O(1).
Insertion or removal of elements at the end or beginning - constant O(1).
Insertion or removal of elements - linear O(n).
如上所述,在极端情况下,保留单个元素可能需要分配整个数组,导致内存效率非常低。例如,在大批量插入时,需要插入大量记录,官方实现将每个记录存储在单独的双端队列中。即使记录内容很少,仍然需要分配一个双端队列。MySQL双端队列实现为每个双端队列分配1KB的内存以支持快速查找。
The implementation is the same as classic std::deque: Elements are held in blocks of about 1 kB each.
官方实现使用1KB的内存来存储索引信息,即使记录长度不大但记录数量很多,内存访问地址可能变得不连续,导致缓存友好性较差。这种设计旨在提高缓存友好性,但效果并不十分显著。
值得注意的是,原始实现中使用了List数据结构,其中内存是通过内存池分配的,提供了一定程度的缓存友好性。虽然随机访问效率较低,但为了优化对List元素的顺序访问,可以显著提高性能。
在升级到MySQL 8.0期间,用户观察到批量插入性能显著下降,其中一个主要原因是基础数据结构发生了重大变化。
此外,尽管官方团队改进了重做日志机制,但也导致了MTR提交操作效率的降低。与MySQL 5.7相比,新增的代码明显降低了单个提交的性能,尽管整体写入吞吐量得到了很大的提升。
让我们来看一下MySQL 5.7.44中MTR提交的核心execute
操作:
让我们来看一下 MySQL 8.0.40 中 MTR 提交的核心 execute
操作:
相比之下,很明显在 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
函数:
在 MySQL 8.0.40 中,rec_init_offsets_comp_ordinary
函数如下:
从上述代码可以清楚地看出,引入了即时添加/删除列功能后,rec_init_offsets_comp_ordinary
函数变得明显更加复杂,引入了更多的函数调用,并添加了一个严重影响缓存优化的switch语句。由于这个函数被频繁调用,直接影响了更新索引、批量插入和连接的性能,导致了重大性能损失。
此外,在MySQL 8.0中的性能下降不仅限于上述内容;还有许多其他方面导致了整体性能下降,特别是对内联函数扩展的影响。例如,以下代码会影响内联函数的扩展:
根据我们的测试,ib::fatal
语句严重干扰内联优化。对于频繁访问的函数,建议避免干扰内联优化的语句。
接下来,让我们看一个类似的问题。row_sel_store_mysql_field function
被频繁调用,其中的row_sel_field_store_in_mysql_format
是其中的一个热点函数。具体代码如下:
row_sel_field_store_in_mysql_format
函数最终调用了row_sel_field_store_in_mysql_format_func
。
row_sel_field_store_in_mysql_format_func
函数由于存在ib::fatal
代码而无法内联。
频繁调用的低效函数,每秒执行数千万次,会严重影响连接性能。
让我们继续探讨性能下降的原因。以下官方性能优化实际上是连接性能下降的根本原因之一。虽然某些查询可能得到改进,但它们仍然是普通连接操作性能下降的原因之一。
MySQL的问题不仅仅局限于此。正如上文所述,MySQL的性能下降并非无因。一系列小问题,当积累起来时,可能导致用户感受到的明显性能下降。然而,这些问题通常很难识别,使其解决变得更加困难。
所谓的“过早优化”是万恶之源,在MySQL开发中并不适用。数据库开发是一个复杂的过程,长期忽视性能会使后续性能改进变得更加困难。
缓解MySQL性能下降的解决方案
写入性能下降的主要原因与MTR提交问题、即时添加/删除列以及其他几个因素有关。这些问题很难用传统方法进行优化。然而,用户可以通过PGO优化来弥补性能下降。通过合适的策略,性能通常可以保持稳定。
对于批量插入性能下降问题,我们的开源版本[2]将官方的deque替换为改进的列表实现。这主要解决了内存效率问题,并可以部分缓解性能下降。通过将PGO优化与我们的开源版本结合,批量插入性能可以接近MySQL 5.7的水平。
用户还可以利用多个线程进行并发批量处理,充分利用重做日志的改进并发性,这可以显著提升批量插入性能。
关于更新索引问题,由于新代码的不可避免添加,PGO优化可以帮助缓解这个问题。我们的PGO版本[2]可以显著缓解这个问题。
对于读取性能,特别是连接性能,我们进行了大幅改进,包括修复内联问题和进行其他优化。通过添加PGO,连接性能可以比官方版本提高超过30%。
我们将继续投入时间优化低并发性能。这个过程很长,涉及许多需要改进的领域。
开源版本已经可以进行测试,我们将继续努力改进MySQL性能。
参考资料
[1] 王斌(2024)。软件工程问题解决之道:如何让MySQL更好。
Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis