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的多个版本做的性能对比测试

可以看到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的描述,优化后只读事务快照读的步骤如下:

改进之后,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链表的长度。

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

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

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

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

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)

时间: 2024-11-01 13:09:54

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

php结合redis实现高并发下的抢购、秒杀功能的实例_Redis

抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个: 1 高并发对数据库产生的压力 2 竞争状态下如何解决库存的正确减少("超卖"问题) 对于第一个问题,已经很容易想到用缓存来处理抢购,避免直接操作数据库,例如使用Redis. 重点在于第二个问题 常规写法: 查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数 <?php $conn=mysql_connect("localho

Spring高并发下Cglib代理性能问题

问题描述 这两天做压力测试(服务器是IBMP7508C32G),高并发下如果用反射直接调用一个简单的服务(空方法,直接return)TPS大概能到将近300,如果用Spring的applicationContextgetBean来获取服务对象调用的话(服务bean是prototype类型),TPS只有不到90.在日志里记录了一下执行时间,用反射调用服务执行时间基本在10毫秒以内,但是用Spring的ApplicationContext执行时间在1秒左右.这样的问题是不是cglib代理引起的?有什

MySQL 5.7增强版Semisync Replication性能优化

  这篇文章主要介绍了MySQL 5.7增强版Semisync Replication性能优化,本文着重讲解支持发送binlog和接受ack的异步化.支持在事务commit前等待ACK两项内容,需要的朋友可以参考下 一 前言 前文 介绍了5.5/5.6 版本的MySQL semi sync 基础原理和配置,随着MySQL 5.7 的发布,新版本的MySQL修复了semi sync 的一些bug 并且增强了功能. 支持发送binlog和接受ack的异步化; 支持在事务commit前等待ACK; 在

MySQL中实现高性能高并发计数器方案

  现在有很多的项目,对计数器的实现甚是随意,比如在实现网站文章点击数的时候,是这么设计数据表的,如:"article_id, article_name, article_content, article_author, article_view--在article_view中记录该文章的浏览量.诈一看似乎没有问题.对于小站,比如本博客,就是这么做的,因为小菜的博客难道会涉及并发问题吗?答案显而易见,一天没多少IP,而且以后不会很大. 言归正传,对文章资讯类为主的项目,在浏览一个页面的时候不但要

并发读写缓存实现机制:高并发下数据写入与过期

一般来说并发的读取和写入是一对矛盾体,而缓存的过期移除和持久化则是另一对矛盾体.这一节,我们着重来了解下高并发情况下缓存的写入.过期控制及周边相关功能.系列文章目录:并发读写缓存实现机制(零):缓存操作指南并发读写缓存实现机制(一):为什么ConcurrentHashMap可以这么快?并发读写缓存实现机制(二):高并发下数据写入与过期并发读写缓存实现机制(三):API封装和简化1.高效的数据写入(put)    在研究写入机制之前,我们先来回顾下上一节的内容.ConcurrentHashMap之

分享MYSQL中的各种高可用技术(源自姜承尧大牛)

原文:分享MYSQL中的各种高可用技术(源自姜承尧大牛) 分享MYSQL中的各种高可用技术(源自姜承尧大牛) 图片和资料来源于MYSQL大牛姜承尧老师(MYSQL技术内幕作者) 姜承尧: 网易杭州研究院 技术经理 主导INNOSQL的开发 mysql高可用各个技术的比较 数据库的可靠指的是数据可靠  数据库可用指的是数据库服务可用 可靠的是数据:例如工商银行,数据不能丢失 可用的是服务:服务器不能宕机       灵活运用MYSQL的各种高可用技术来达到下面各种级别的高可用要求 要达到99.9%

php结合redis高并发下发帖、发微博的实现方法_php实例

发帖.发微博.点赞.评论等这些操作很频繁的动作如果并发量小,直接入库是最简单的 但是并发量一大,数据库肯定扛不住,这时可采取延迟发布:先将发布动作保存在队列里,后台进程循环获取再入库 模拟发布微博先进入redis队列 weibo_redis.php <?php //此处需要安装phpredis扩展 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth("php001"); //连接

mina高并发下响应时间过长的一些失败的尝试及寻求正确方法

问题描述 楼楼做这个mina的服务端的时间大概不到一个月. 楼楼之前主要是做http的编码,对socket的了解也不是很深, 但因为mina的易用性所以整个服务端也算是很快就搭建起来了. 在和客户端对接测试的时候稳定性和服务端的功能也都还算满意.所以兴冲冲的就开始做压力测试了. 压力测试一做就做出来一身冷汗. 楼楼的服务器是4核的,所以服务端启动的ioprocesser=5. 目前最大的问题是: 当session数量到达100以上时. 客户端收到服务端响应的时间大约就要2到3秒, 当同时并发2W

一个关于HashMap高并发下死循环的问题.

问题描述 当前系统在高并发下出现进程CPU占用很高的问题.即使没有处理业务.也占用很高.后定位出原因可能是HashMap在高并发条件下会死循环.有两个问题想请问下:1.出现死循环的话.是否还会回复原样?即HashMap死循环是因为元素的问题.但元素时刻都在改变.是否会出现自动死循环自动修复?2.现在想重现这个死循环条件.但重现很难.高并发访问下出现几率也比较低.请问有没有什么好办法可以提高重现几率. 问题补充:freish 写道 解决方案 1.高并发场景下HashMap可能会形成闭环,导致死循环