MySQL 5.7在高并发下性能劣化问题的详细剖析

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
性能测试 PTS,5000VUM额度
简介: TL;DR MySQL 5.7高并发读写混合场景下rt飙升,业务系统大量超时报错。本文总结了阿里业务场景下遇到的坑,剖析问题背后的原因,帮助读者更好的理解MySQL内核原理,降低升级MySQL 5.7的风险。

TL;DR

MySQL 5.7为了提升只读事务的性能改进了MVCC机制,虽然在只读场景下能获得很好的收益,但是在读写混合的高并发场景下却带来了性能劣化,导致的结果就是rt飙升和业务端超时。本文剖析了此问题背后的原因,并给出了解决办法。

引言

MySQL 5.7自发布以来备受关注,不仅是因为5.7的在功能特性上大大丰富,它的读写性能上相对于之前的版本也有了很大提升。正是由于5.7卓越的表现,我们自去年起就开始着手将AliSQL整体搬迁到5.7上。然而经过一年多的整合测试我们发现,5.7宣称的有些能力表现却不尽如人意。这里面当然有很多有趣的故事可以讲,本文要讲的这个故事却是对MySQL 5.7引以为傲的“高并发高性能”的一个很好的回应。

自MySQL从4.0发展演进到现在的8.0,高并发场景下MySQL的性能越来越强,以下是dimitrik针对MySQL的多个版本做的性能对比测试
e528be31_fae8_47a4_a5b2_e5c4a618f49c

可以看到MySQL 5.7的读能力有了很大的提升,这是因为它针对读性能做了很多优化,其中就包括了InnoDB引擎层MVCC机制的改进。本文要介绍的性能退化问题就和这个MVCC机制的改进密切相关。

背景介绍

在说明问题之前有必要交代一下InnoDB的多版本控制(MVCC),与MVCC密切相关的是快照读。所谓快照读既是无锁读,那么它是用来解决什么样问题的?

我们知道InnoDB是一个支持事务的引擎,事务的一个重要特性是隔离性,即还未提交的事务的修改对外界是不可见的。为了实现事务隔离性数据库一般使用两种实现手段,分别是当前读和快照读。

所谓当前读就是加锁读,事务对访问的数据进行加锁,事务提交时释放所持有的锁。由于锁的互斥性,正在被活跃事务修改的数据无法被其它事务访问,必须等到修改它的事务提交。这种方式的优点是实现简单,但缺点也很明显,并发性能差。

而快照读的特点是多个事务访问相同数据时不需要加锁,可以并发执行。具体做法是一份数据保存多个历史版本,不同事务访问不同历史版本的数据彼此之间互不影响。不过快照读也有一个明显的缺点,那就是事务对数据只能读不能修改。

InnoDB的事务在修改一份数据时对其进行加锁读,而只读不改数据的时候进行快照读,最大限度的提高事务的并发性能。

再来讲讲快照读的具体实现,这里涉及到几个问题:1,历史版本如何保存;2,如何拿到正确版本的历史数据;3,历史数据如何回收。

数据的历史版本有两种保存方法:一种保存全量的历史数据,另一种保存能够回滚到历史数据的undo日志。InnoDB采用的是后一种方式,事务更新数据同时产生一份对应的undo日志,并且日志中记录了事务id。

事务使用快照(readview)去读取数据正确的历史版本,快照中包含的信息是当前所有活跃事务的id。开始读之前分配一个快照(read-committed和repeatable-read隔离级别下快照分配方式略有不同),实际就是对当前所有的活跃事务做了个快照。在读取数据过程中,由于需要通过undo日志构造历史版本,而每个undo日志有一个事务id,对比undo日志中的事务id和readview中的事务id就可以判断历史版本是否可见。说直白点就是,快照创建的那一刻生成这份历史数据的事务是已提交状态(可见)还是未提交(不可见)。

最后,历史数据是需要回收的,不然undo日志占用的空间会越来越大。InnoDB回收历史数据的任务由后台purge线程完成,回收的原则是:只能回收那些永远不会再被用到的undo日志。具体做法是:purge线程从当前所有readview中找到创建时间最久的那个oldest readview,那些比oldest readview还要旧的undo日志就是可以被安全回收的。因此,InnoDB维护了一个全局的readview链表,链表中的readview按照创建时间排序,purge线程只须找到链表尾端的readview就是oldest readview。

5.7的改进

根据上面的介绍可以知道,事务进行一次快照读的步骤如下:

1. 分配一个快照对象
2. 将快照对象加入到全局readview链表头部
3. 对当前活跃事务打快照
3. 进行快照读
4. 完成后,将快照对象从全局readview链表中移除

需要说明的是,rr隔离级别下每个事务使用一个快照,而rc隔离级别下每条query使用一个readview,所以快照的分配和释放与事务的开始和结束不完全对应。

同时,后台purge线程进行一次purge操作的过程是

1. 从全局readview链表中找到oldest readview
2. 使用oldest readview去回收undo日志

看到这里可以很清楚的明白一件事:必定存在一把锁保护全局readview链表。没错,这把锁就是InnoDB事务系统的全局大锁(trx_sys->mutex)。这把大锁不光保护了全局的readview链表,还保护了全局的活跃事务链表等对象,事务在begin和commit等过程中也要竞争这把大锁,所以这是一把比较热的锁。

为了减少这把锁的争用,MySQL 5.7对一些特定场景做了优化。比如对于autocommit的只读事务,优化了readview的创建过程。优化的原理是:如果上次快照读与这次快照读之间没有写事务发生,那么一定也没有任何数据被修改,所以理论上可以直接复用上次的readview,这样做能够很大程度减少事务系统全局锁的争用开销。

根据WL#6578的描述,优化后只读事务快照读的步骤如下:

_2017_10_13_9_56_28

改进之后,autocommit只读事务完成快照读后,并不会将快照从全局readview链表中删除,而只是将它设置成close状态。再次进行快照读时,如果可以复用上次的快照则不需要操作全局readview链表,自然也不需要争用事务系统全局锁。而如果不能复用快照,依然需要操作全局readview链表,但是相对于改进前少了一次全局事务锁的争用。根据前面dimitrik做的5.6与5.7只读事务性能比较对比测试图,可以看到只读场景下5.7性能有了飞跃提升。

带来的问题

这样优化之后虽然对autocommit的只读事务的性能友好,但是某些场景下反而带来了性能劣化。为什么这么讲呢?因为我们实实在在踩到了这个坑!

让我们从原理上分析性能劣化的原因,这样修改之后虽然autocommit只读事务减少了对全局readview链表的操作,但是全局readview链表中却多出了很多close状态的readview,链表长度无端变长了!!

这样带来的最直接的影响是purge线程在获取oldest readview的开销变大了。之前只要获取全readview最末尾的readview就是oldest readview,但是修改之后需要从全局readview链表末端往前遍历,直到
找到第一个非close状态的readview。

在我们的业务场景下有非常多autocommit的query语句,这些query查询就是一个个的autocommit只读事务,因此它们提交时readview会留在全局readview链表中。下图是我们在全链路压测时一个业务节点上抓取的全局readview链表的长度。

121

这样一万多个readview大部分都是close状态,导致purge线程在查找oldest readview时须对链表进行大量的扫描,直接导致此过程非常耗时。下图是我们用perf抓的函数cpu开销

132

更加要命的是,后台purge线程在扫描全局readview链表时持有事务系统的全局锁,从而导致这把锁的争用更加激烈,从perf结果上可以看到ut_delay()函数调用稳居前列。

全链路压测过程我们一个核心业务上第一次暴露此问题,直接反应就是rt飙升吞吐下降。下图是压测过程中业务的rt表现(单位:us,平均RT已经到了8ms左右)
152

为什么是全链路压测过程中暴露此问题,因为此时业务场景满足了以下几个条件:

1. 活跃连接数够多,高峰能达到10000+;
2. autocommit只读事务和写事务混合并发;
3. 大部分是单条sql的简单事务;

这是因为,有autocommit只读事务且事务并发度高才会导致全局readview链表中close的readview足够多。有写事务并发才会产生undo日志,后台purge线程才需要进行回收,这样才会去查找oldest readview。都是单条sql的简单事务,事务begin和commit的开销占比才够大,竞争事务系统的全局大锁才能影响到rt和吞吐。

虽然是全链路压测过程中才集中爆发的问题,但是我们通过复盘系统监控发现此问题会经常导致rt飙升。这是因为业务习惯使用autocommit只读事务(autocommit=1时,一条query就是一个autocmmit只读事务),一旦读压力上升就会导致全局readview链表变长,后台purge线程获取oldest readview时会"长时间"持有全局事务锁,此时必然影响所有事务的begin和commit,尤其对于大多数小事务场景特别明显。

由于这是一个非常普遍的问题,我们已经将它提交给了MySQL官方。所以,如果平时你的系统会有rt突然飙升的情况发生,可以考虑从这个方向思考。

如何解决

问题产生的根本原因是全局readview链表中close状态的readview太多了,导致查找oldest readview时需要遍历非常多的无效节点,而产生这个问题的原因是autocommit只读事务延迟释放readview。清楚这一点后问题就很好解决了:autocommit只读事务结束快照读后随即将readviwe从全局链表中删除。这样修改后,查找oldest readview时依然只需找到全局readview链表最后一个节点即可,非常地快捷。

修改后的影响:

1, 只读场景是否会降低到5.6的水平?

答:不会的,5.7对只读场景的优化是多方面的,既包括上层元数据锁的优化也包括InnoDB层的若干优化,这些优化叠加起来在我们的环境下测试约有30%+的性能提升,而readview延迟的
优化只占了很小一块,根据我们的测试性能下降大概在5%以内

2, 对纯读写事务并发是否有影响?

答:没有影响,延迟释放readview只对autocommit只读事务有效,读写事务的readview完成快照读后依然是要从全局readview链表中摘除,因此此修改对读写事务没有影响。

3, 对只读事务和读写事务混合场景的影响?

答:此场景下性能是有提升的,因为能够降低事务系统全局锁的竞争。下图是修改后的rt(单位:us。相同的压力下,RT从修复前的8ms下降到了0.2ms)

182

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
30天前
|
存储 监控 固态存储
在高并发环境下,如何优化 WAL 的写入性能?
在高并发环境下,如何优化 WAL 的写入性能?
|
16天前
|
SQL 关系型数据库 MySQL
MySQL 8.0:filesort 性能退化的问题分析
用户将 RDS MySQL 实例从 5.6 升级到 8.0 后,发现相同 SQL 的执行时间增长了十几倍。本文就该问题逐步展开排查,并最终定位根因。
|
17天前
|
缓存 监控 安全
如何提高 Java 高并发程序的性能?
以下是提升Java高并发程序性能的方法:优化线程池设置,减少锁竞争,使用读写锁和无锁数据结构。利用缓存减少重复计算和数据库查询,并优化数据库操作,采用连接池和分库分表策略。应用异步处理,选择合适的数据结构如`ConcurrentHashMap`。复用对象和资源,使用工具监控性能并定期审查代码,遵循良好编程规范。
|
26天前
|
SQL 关系型数据库 MySQL
【MySQL 慢查询秘籍】慢SQL无处遁形!实战指南:一步步教你揪出数据库性能杀手!
【8月更文挑战第24天】本文以教程形式深入探讨了MySQL慢SQL查询的分析与优化方法。首先介绍了如何配置MySQL以记录执行时间过长的SQL语句。接着,利用内置工具`mysqlslowlog`及第三方工具`pt-query-digest`对慢查询日志进行了详细分析。通过一个具体示例展示了可能导致性能瓶颈的查询,并提出了相应的优化策略,包括添加索引、缩小查询范围、使用`EXPLAIN`分析执行计划等。掌握这些技巧对于提升MySQL数据库性能具有重要意义。
53 1
|
30天前
|
缓存 关系型数据库 MySQL
在Linux中,如何优化MySQL性能,包括索引优化和查询分析?
在Linux中,如何优化MySQL性能,包括索引优化和查询分析?
|
1月前
|
SQL 存储 关系型数据库
"MySQL增列必锁表?揭秘InnoDB在线DDL,让你的数据库操作飞一般,性能无忧!"
【8月更文挑战第11天】在数据库领域,MySQL凭借其稳定高效的表现深受开发者喜爱。对于是否会在给数据表添加列时锁表的问题,MySQL的行为受版本、存储引擎等因素影响。从5.6版起,InnoDB支持在线DDL,可在改动表结构时保持表的可访问性,避免长时间锁表。而MyISAM等则需锁表完成操作。例如,在使用InnoDB的表上运行`ALTER TABLE users ADD COLUMN email VARCHAR(255);`时,通常不会完全锁表。虽然在线DDL提高了灵活性,但复杂操作或大表变更仍可能暂时影响性能。因此,进行结构变更前应评估其影响并择机执行。
48 6
|
1月前
|
缓存 NoSQL Redis
一天五道Java面试题----第九天(简述MySQL中索引类型对数据库的性能的影响--------->缓存雪崩、缓存穿透、缓存击穿)
这篇文章是关于Java面试中可能会遇到的五个问题,包括MySQL索引类型及其对数据库性能的影响、Redis的RDB和AOF持久化机制、Redis的过期键删除策略、Redis的单线程模型为何高效,以及缓存雪崩、缓存穿透和缓存击穿的概念及其解决方案。
|
1月前
|
存储 关系型数据库 MySQL
"揭秘!MySQL为何独宠B+树?跳表再牛,也敌不过这性能王者的N重诱惑!"
【8月更文挑战第11天】MySQL作为主流关系型数据库,优选B+树而非跳表作为索引结构,基于其对范围查询的支持、低磁盘I/O开销及事务处理能力。B+树叶节点构成有序链表,利于范围查询;较矮的树形结构减少了磁盘访问次数;支持多版本并发控制,保障事务ACID特性。而跳表在线性扫描范围查询时效率低,难以高效实现事务管理,且额外指针增加空间消耗。示例代码展示了B+树节点分裂过程,突显其内部机制。综上,B+树为MySQL提供了高性能、可靠的数据存储与检索能力。
47 4
|
1月前
|
存储 缓存 运维
优化高并发环境下的数据库查询性能:实战经验与技巧
在高并发环境下,数据库性能往往成为系统瓶颈。本文将深入探讨在高并发场景下优化数据库查询性能的策略与实践,包括索引优化、查询优化、数据库架构设计以及缓存机制的应用。通过对具体案例的分析,读者将能够掌握提升数据库性能的关键技术,从而在面对大规模用户请求时提高系统的响应速度和稳定性。
|
19天前
|
前端开发 C# 设计模式
“深度剖析WPF开发中的设计模式应用:以MVVM为核心,手把手教你重构代码结构,实现软件工程的最佳实践与高效协作”
【8月更文挑战第31天】设计模式是在软件工程中解决常见问题的成熟方案。在WPF开发中,合理应用如MVC、MVVM及工厂模式等能显著提升代码质量和可维护性。本文通过具体案例,详细解析了这些模式的实际应用,特别是MVVM模式如何通过分离UI逻辑与业务逻辑,实现视图与模型的松耦合,从而优化代码结构并提高开发效率。通过示例代码展示了从模型定义、视图模型管理到视图展示的全过程,帮助读者更好地理解并应用这些模式。
34 0

热门文章

最新文章