记一次mysql内存泄漏排查经历

记一次艰难的mysql数据库内存泄漏排查经历,以供以后类似问题参考。

背景

客户反应现场部署的系统,mysql数据库进程占用内存持续上涨,最终在占用到5G左右,mysql进程崩溃,日志提示虚拟内存不足。

操作系统:windows server 2012

数据库:mysql 8.0.30

软件对于数据库的使用情况:数据量比较大,在持续不断地写入和查询。

排查思路

1、数据库连接泄露

起初怀疑是不是有代码频繁起新连接,而未关闭旧连接,但经查并没有,连接数量比较稳定。

查询当前连接数:

SHOW STATUS LIKE 'Threads_connected';

查询设置的最大连接数:

show variables like "max_connections";

2、怀疑事务未提交,持续的未提交事务积压,导致内存爆炸

经查,事务都及时提交了,并没有事务积压。

查询当前事务:

SELECT * FROM performance_schema.`processlist`;

3、怀疑慢sql导致内存占用升高

经监控观察,并不存在慢sql。

4、希望用清理的方式降低内存占用

想通过flush query cache清理查询缓存,但执行报错,查询才知道,mysql8.0版本已经取消了查询缓存

执行几个flush命令,均没有任何效果:

FLUSH TABLES;
FLUSH TABLES WITH READ LOCK;
FLUSH QUERY CACHE;
RESET QUERY CACHE;
FLUSH HOSTS;
FLUSH PRIVILEGES;
FLUSH LOGS;

FLUSH TABLES WITH READ LOCK命令执行后还引起了一点小插曲,执行这个命令时会锁表,导致执行时很多事务无法提交(99个事务),我当时一度以为自己找到问题原因了,是我们的系统有大量未提交事务,但后来才发现是自己执行FLUSH TABLES WITH READ LOCK引起的,断开锁表的连接后(即我的heidisql连接),这些事务立即提交了。

总之,各种清理命令,未起到一丁点效果。

5、怀疑临时表空间占用大量内存

经查,临时表空间占用也并不大。

查询临时表空间:

select * from performance_schema.global_status where variable_name like '%tmp%tables';
select * from sys.memory_global_by_current_bytes where event_name like '%temp%';

6、通过performance_schema观察内存占用情况

# 查询总的分配的内存
select * from sys.memory_global_total;
# 查询总的内存分配
SELECT t.EVENT_NAME, t.CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_global_by_event_name t ORDER BY t.CURRENT_NUMBER_OF_BYTES_USED DESC;
# 查询用户占用的内存
SELECT t.EVENT_NAME, t.CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_by_user_by_event_name t ORDER BY t.CURRENT_NUMBER_OF_BYTES_USED DESC;
# 查询线程占用的内存
SELECT t.EVENT_NAME, t.CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_by_thread_by_event_name t ORDER BY t.CURRENT_NUMBER_OF_BYTES_USED DESC;
# 查询连接主机占用的内存
SELECT t.EVENT_NAME, t.CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_by_host_by_event_name t ORDER BY t.CURRENT_NUMBER_OF_BYTES_USED DESC;
# 查询用户占用的内存
SELECT t.EVENT_NAME, t.CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_by_account_by_event_name t ORDER BY t.CURRENT_NUMBER_OF_BYTES_USED DESC;

performance_schema观察到的内存总体比较稳定,没有持续上涨,与mysql进程的内存上涨趋势不符,说明mysql的运行可能实际并不需要任务管理器中显示的那么多内存。

7、调整mysql配置

尝试设置了innodb_buffer_pool_size等参数,主要效果是启动时mysql进程占用的内存变小了(之前一些参数设置得比较大,改小了),但持续增长问题依旧,没有丝毫缓解。

而且尝试了mysql的默认配置,也没有效果。

8、怀疑是mysql版本问题

去mysql官网查询,升级日志显示,8.0.31版本,修复了两个内存泄露的bug,就怀疑是不是碰上了这个版本mysql的内存泄漏问题,其实官网bug的描述,与我们的情况并不是一个情况,纯属死马当活马医了。

于是在测试服务器上部署了一套8.0.33版本的mysql(绿色部署),将8.0.30的数据全都导入,然后运行观察,发现内存泄漏问题依旧,无任何缓解。

9、怀疑windows server 2012与mysql二者的内存管理机制不太融洽

上述一通折腾下来无任何效果,于是静下心观察:

  • 即使不跑我们的服务,用heidisql执行各种查询与写入,mysql进程内存依然会上涨(查询上涨得更快),这点说明,应该不是我们服务的问题,我们正常执行sql,内存依然会持续上涨;
  • 我们测试服务器上的mysql进程的内存占用从来不曾下降,只涨不降,即使任务管理器中的内存占用已经比performance_schema中查询内存分配高得多得多了,也不会下降;
  • 停了所有的服务,所有连接正常断开(除开heidisql),内存会下降一些,但不是很多,大约一个连接降低1M的样子,高出的内存依然不会释放,即使等了一整晚。比如启动所有服务时是400M,运行一段时间到了800M,断开所有的连接,内存下降到750M左右,但基本不会再降低了,永远也无法回到400M。

通过以上内存观察,我感觉可能是windows server 2012内存管理机制与mysql的内存管理器不太融洽而导致的问题,为了进行对比,我尝试在个人电脑部署相同版本相同配置的mysql及相同的服务对比运行(我本地请求的频率设置得更高),个人电脑系统为windows 10专业版,结果对比下来果然有发现:

  • 我本地的mysql启动时进程占用明显小了很多;
  • 随着服务运行,持续的写入与请求,mysql进程的内存占用依然会持续上涨,最高达到180M左右,但与服务器上的mysql的显著区别就是,本地的mysql进程的内存占用会回落,隔一段时间,它会掉一块下来,最低一度到了二十多兆,内存占用非常平稳,而且这些都是在mysql服务与业务服务都正常运行的情况下发生的;

相同的mysql相同的业务服务,而运行结果却大相径庭,于是分析差异原因:

  • 怀疑可能是我本地是固态硬盘而服务器上是机械硬盘,固态硬盘速度显著快于机械硬盘导致的,于是观察磁盘IO情况,实际本地和服务器上的磁盘IO速度都是100k/s的样子,磁盘IO并不是瓶颈;
  • 不是磁盘速度的问题,那肯定就要着重考虑操作系统的差异了;

从网上了解的资料,mysql的内存管理可能是释放了内存,但是这些内存并没有返还给操作系统,而是以碎片的形式残留在mysql占用的内存中,而windows server 2012对内存的回收可能并不积极,从而导致mysql进程占用的内存中包含大量的碎片,占用内存不断膨胀而且无法释放,但实际上mysql真正使用的并没有这么多。

而windows 10增强了内存管理,支持自动内存管理,对内存的回收更为积极,所以在本地电脑的mysql的内存虽然会上涨,但也会回落,总体趋于平稳,不会出现内存始终上涨无法释放的问题。

以上为结合资料个人理解,如有谬误,欢迎指正。

基于上述推断,于是考虑从系统内存清理的角度进行尝试,遂找到memreduct这个小巧的windows内存清理工具,memreduct是开源的,代码托管在github,从其官方说明来看,它的原理是通过调用windows api来实现各种内存清理,用户评价颇高。

memreduct官网:Mem Reduct

memreduct代码库:henrypp (henrypp) · GitHub

于是将memreduct安装到测试服务器的windwos server 2012中,执行清理,神奇的事情发生了,mysql的内存占用直接从800+M被清理到了几十兆(当然被进行内存清理的不仅仅是mysql进程,也包含其它进程,比如java进程),而且这是在mysql与业务服务均正常运行的前提下发生的。

memreduct可以设置定时清理与阈值清理,于是本着极端测试的想法,我将其设置为每分钟清理一次,持续观察了几天,mysql服务与业务服务始终可以正常使用,而如果要给客户服务器实施,实际并不需要这么高的频率,只需要每天或者每几天在闲时进行一次清理就足够了。

于是,初步观察下来这似乎是一个可行的有效的解决方案,不过可能实现方式对于用户接受度有限,因为用户会担心清理过程引发一些其它的问题,但目前也未能找到更好的方法。

至此,个人对此次mysql内存泄漏问题排查就结束了,作此笔记,以供参考。

附录

记录一段时间内执行的sql

以下命令sql编辑器执行即可。

1、开启日志记录

SET global general_log = 'ON';

2、关闭日志记录

SET global general_log = 'OFF';

3、导出日志

SHOW VARIABLES LIKE 'general_log%';

4、到mysql数据目录下获取截取的sql日志

你可能感兴趣的:(数据库,mysql)