22

​X侦探所事件薄 | 一次内存溢出之谜

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=Mzg4NjA4NTAzNQ%3D%3D&%3Bmid=2247489272&%3Bidx=1&%3Bsn=95cce03cdfd6eeb4070ec5b55594e959
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

| 作者    姜宇祥,曾就职于达梦和携程,目前在CDB/CynosDB数据库内核团队担任TXSQL云数据库内核研发,多年深耕数据库领域,为国内早期一批数据库内核研发人员。过去曾在达梦经历了新一代达梦从零开始的整个研发过程,并参与多个版本的迭代与架构调整;还曾在携程率先开启MySQL的定制开发,为线上业务提供支持。另一方面,他也积极参与MySQL开源社区在中国成长过程,通过技术宣讲与文章编写助力MySQL在中国的传播。

在数字领域,TX王国是一个统御着“成T上P”数据子民的大国,这里的T和P是极大极大的数,用成千上万来形容数据量之多并不为过。 X侦探事务所就是TX王国中负责MySQL领域 管理数据子民的有关部门,而事务所中探员们就是专门负责解决各种各样突发事件的战斗精英。

我们将要讲述的是关于这些探员的侦探故事,他们擅长在海量的数据中追寻蛛丝马迹,屡破奇案。这次,我们将要讲述的是一个连环宕机血案的侦破故事。

案发现场

一天,探员T 因遇到了一个棘手的MySQL 实例宕机问题而头疼不已,通过内部的监控系统发现一个MySQL数据服务使用的内存就像坐了加了速的小汽车一样飞速上涨。 操作系统为了保证整个系统的运行,不得不将该MySQL服务杀死,以释放足够的资源用于系统正常运转。 这是一个很严重的问题,任何服务的宕机以及内存不正常现象都是要优先进行排查并处理。

内存溢出(Out Of Memory)

一般是由于程序编写者对内存使用不当,如没有及时释放申请的内存资源,导致该内存一直不能被再次使用而使计算机内存被耗尽的现象。杀死进程或重启计算机可从操作系统层面解决问题,但根本解决办法还是对代码进行改进。

Uz26zi6.png!mobile

案件经过

面对这种紧急情况,经验丰富的探员 T迅速登录服务器查看情况。 首先怀疑的是打开的表太多,导致大量的表对象占用了 内存空间。 经过对frm文件和ibd文件的底层粗略查询,该MySQL实例上有20多万张的表。 那么,大量的表对象占用了内存空间的必要条件就成立了。 于是进一步查看,限制表打开数目的变量“table_definition_cache”是否设置的过大,导致占用的内存过多。

但是,该变量并未如预期中设置的过大,属于合理范围。 那么为什么内存还会占用如此之多? 探员T此刻陷入了深深的思考。 现在案件似乎走入了一个死胡同,也就是存在大量的表但是对打开表的资源限制在了一个合理的范围内,这似乎是一个悖论。 关键问题来了,到底是哪里占用了大量的资源呢?

作为一个优秀的探员,探员T立刻意识到事件发生的现场应该还会存有大量的案发信息,于是他立刻又回到案发现场,努力尝试重现该事件发生的整个过程。这是一个很重要的环节,很多问题的定位都是通过还原重现场景来完成的。经过对线上管控人员的细致调查,发现了一条可疑地SQL语句,每当执行该语句的时候,内存使用就会不可遏制的向上增长,这条语句就是:

SELECT table_schema, table_name, partition_name, table_rows

FROM information_schema.partitions

WHERE partition_name IS NOT NULL

ORDER BY table_schema, table_name;

通过对该语句的跟踪,发现该语句主要完成两件事情:

(1)遍历打开MySQL实例的所有表并获取这些表信息

(2)现在将这些信息写入临时创建的表中

从以往的经验来看,临时创建的表不会占用太多的资源,而且理论上二十多万行的数据也不会占用太多空间,于是遍历所有表这个操作就变得愈发可疑。这也联系上了之前的猜测,“打开的表太多,导致大量的表对象占用了内存空间”,事情排插到了这一步,探员T 直觉推测很可能就是该操作导致的OOM。真相只有一个,那么 探员T该如何印证这个猜想呢?

工欲善其事,必先利其器。想要快速的定位问题,探员们必须熟练掌握并使用恰当的排障工具,而MySQL就提供了这样一个强大的实时运行工具箱——performance_schema。之所以称之为工具箱,是因为它是很多工具的集合,今天我们要用的是这个工具箱中关于内存的工具,其他的工具我们将来会有专门的专题来讲述。

现在我们需要在配置文件中增加如下配置以开启对内存使用的监控:

并通过该语句查询内存使用情况:

Select * from performance_schema.memory_summary_by_thread_by_event_name order by CURRENT_COUNT_USED

desc limit 10;

通过上面的操作,探员T 发现确实是表对象打开的过多,不过这些表对象不是MySQL Server层打开的对象,而是存储引擎层innobase打开几乎全部的表对象并进行缓存,从而没有及时释放导致了大量内存的占用。但对于一个成熟的程序来说不会不回收资源,那么innodb为什么没有回收资源呢?原来对于表的内存对象回收是在下面这个后台线程进行回收的

z2UZrma.png!mobile

如下代码所示,在srv_master_thread的后台线程函数中,会在active和idle两种情况下进行资源回收。

u26fqqR.png!mobile

在频繁有操作的环境下,idle场景是不会被触发;而在active场景下,结合如下代码分析,平均47秒才会有一次主动的内存回收。

IVn6Jfu.png!mobile

ANjUbyN.png!mobile

话说办法总比问题多,既然定位了问题,那么就可以解决问题了。 探员T在被动释放内存对象的基础上,innobase每次打开表时检测内存中表对象的打开数量,当超过指定的阈值就进行释放,从而解决了问题。

再次案发

至此,探员T已经完成了OOM问题的定位和解决,在其他相关部门相互协作下,将修改好的新版本发布到了线上。 但就在大家觉得问题已经解决,可以放松一下的时候,噩耗传来,新版本竟又出现了宕机。 一波未平一波又起,还未来得及好好休息的探员T又再次披挂上阵来解决问题。

首先要分析是不是新修改引进的问题,一般会用两个方法:

(1)   快速回滚发布到线上的版本,对于触发频繁的实例,该方法为首选,因为可以快速验证;

(2)   另一个是审查修改后的源代码,对于改动较少的版本来说,这个方法可以作为首选。

探员T 首先重新审查了修改的代码,这次修改只增加89行的内容,理论上可以很快就定位到问题,而且线上问题的出现频率并不是很高。

经过反复从代码层面进行分析,却并没有能找到引发错误的任何蛛丝马迹。用于回收innobase内存对象的函数是经过验证的函数,这个函数已经伴随着MySQL发布的很多版本,无论如何都不应该也不会出现,那么问题的根本原因会是什么呢?

此时在 探员T脑海中开始回想事情发生的整个经过:首先是针对innobase内存对象优化的修改而引发的服务崩溃,其次是通过线上实例的堆栈了解到问题是发生在执行前文中提到的information_schema查询语句,最后通过分析新增代码的逻辑确认该改动没有问题。

在这种情况下,就不能仅凭静态的现场进行分析了。正所谓“纸上得来终觉浅,觉知此事要躬行”,需要能够复现事件的发生,通过coredump或者gdb的断点是解决这类只有静态现场但并无思路的好办法。很多人以为重现问题很简单,但由于大多数时候的问题是并发造成的,并发的偶然性就造成了问题出现的偶然性。尝试重现有两个好处,一是能摸清楚问题发生的规律,这本身就能帮助我们将问题限定在某个范围;二是,稳定重现可以帮助我们在不停尝试断点的设置,同样会不断缩小问题的范围。最终通过上下文环境,进而推断出问题原因。

首先尝试的是运行前文中提到的SQL语句,但在多次运行后并未触发服务崩溃的问题,同时结合上线前跑过的MySQL基本测试,可以判定该问题为并发模式下被触发。这里介绍一款比较热门的工具sysbench,因其易安装易使用的特点,在DBA和测试人员中被广泛的使用。首先通过sysbench创建了2万张数据表并在每张表中插入两条数据,然后发起压力测试,测试期间运行上文中的SQL语句。在多次尝试后,问题再次出现,并通过该方法稳定的重现,得到了出问题的core dump。

以下是在打开表时出现错误的堆栈以及出错时出现问题的变量。

YNVzYz6.png!mobile

以下是运行时出错位点出现宕机的断言

J7Fvaa.png!mobile

断言

MySQL在运行时进行状态检查的一种手段,用于断定某种情况的必然成立,所以被称为断言。

通过对core dump的分析,发现问题是发生在打开表的过程中,快速获取的数据表内存对象出现了内存访问出错,也就是通过如下方式获取的内存对象。

为什么会在这一步获取的内存对象会出现错误?从这里看,和之前修改有什么必然关 联? 探员T 又开始回顾出问题时的变量,如下图所示:

以其丰富的经验看,此时m_share中的index对象已经被释放,联系之前的改动是innodb在打开表达到阈值时释放内存对象,那么也就是说在释放内存对象的时候没有进行响应的保护。如果是这样的话的,那么也就是在innodb在进行active/idle工作时也会出错,只是由于对于释放操作函数srv_master_evict_from_table_cache的调用不够频繁,所以出现问题的概率降低到非常低。于是尝试修改代码,提高释放内存对象的频率,代码修改如下:

aQzEvmb.png!mobile

重新运行测试验证。Bingo,得到了同样的结果,社区版的MySQL同样会出现宕机的情况,至此,终于确定了问题的根本原因。那么接踵而至的是,为什么share对象中的表内存对象没有被保护,在innodb进行active/idle工作时被释放?此时需要进行追本溯源,对get_share/free_share和dict_table_open/dcit_table_close的过程进行分析,发现如下在innodb中打开表的顺序存在问题。如下图,当active/idle后台线程释放了内存中的表对象后,事务线程恰好获取了share对象则该share对象中的表内存对象都是无效的。

fumqe2Z.png!mobile

这里就是涉及到编写代码的一个原则,两个不同资源的获取与释放,在获取时,被依赖的资源需要放在前面获取,在释放时,先获取资源要后释放,如下图所示:

JFJZ3eb.png!mobile

按照这个原则进行代码修改,在进行测试验证,内存问题再也没有出现,至此对OOM问题的修改所引发的隐藏问题也得到解决。这就是编写代码中经常碰到的,当我们修复了一个问题后,极有可能会触发另外一个隐藏的问题,而D侦探事务所的 探员T,就是将两个问题串联起来进行分析,才能顺利定位根本原因并进行修正。

探员T寄语:案件终于顺利解决了, 希望此类案件以后不会再发生了, 这种一个bugfix暗戳戳自带了一个bug真是防不胜防啊,不过我们的探员T经验足够丰富所以此次有惊无险,在 MySQL这个领域有X侦探所各位身怀绝技的探员们为大家保驾护航,请大家放心~接下来我们还有其他探员的故事,敬请大家期待~

手机运维小程序限时免费体验!

手机运维小程序——腾讯云数据库上线啦,从此在手机里可以实现实例信息查看,健康报告接收,慢SQL分析和异常查看等功能,以后回家终于可以不背电脑了!

iuYbQbY.jpg!mobile

↓↓一年19.9特惠Cynos点这儿~  


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK