vlambda博客
学习文章列表

MySQL内存不释放分析

问题分析场景1 使用sysbench压测数据库场景2 load 一个很大事务的insert语句问题突破测试jemalloc场景1使用sysbench压测数据库场景2 load 一个很大事务的insert语句小结MySQL到底有没有释放内存?通过gdb调试结论

线上MySQL数据库发现一些实例,内存使用不断增高,并且当连接数断开后内存不会释放,最终导致的结果是被操作系统OOM

问题分析

模拟两个场景来分析此问题:

场景1 使用sysbench压测数据库

使用sysbench压测MySQL,等待连接断开后,使用top查看mysqld进程,内存使用情况
将mysql innodb_buffer_pool设置为128M,方便观察内存增长情况。

启动MySQL后内存使用情况,大约在150M:


sysbench压测60s

sysbench --db-driver=mysql /usr/share/sysbench/oltp_read_only.lua --mysql-host=127.0.0.1 --mysql-port=3320  --mysql-db=sbtest --mysql-user=root --mysql-password=123456 --tables=16 --table_size=500000 --threads=128 --time=60 --report-interval=1 run

观察内存使用情况:

MySQL内存不释放分析



内存增长到540M左右,但是内存并没有随着sysbench连接断开而释放


场景2 load 一个大事务的insert语句

重启MySQL观察内存使用情况, 大约占用170M:

MySQL内存不释放分析


文件大小43M

MySQL内存不释放分析


文件内容:

INSERT INTO dummy VALUES
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
(1,'This is a dummy value'),
.......

source 文件:

mysql> source test.sql

source完成后观察内存使用情况:

MySQL内存不释放分析



占用到1G,同样也是,没有随着连接断开,内存进行释放。


问题突破

通过上面两个例子,能证明MySQL连接断开后,内存没有释放。长时间下去就会导致内存不断增高,查看bugs.mysql 发现有人提过此问题:
https://bugs.mysql.com/bug.php?id=83047 --里面有人提出load个大事务,复现此问题。
同样percona版本也有人提过:
https://bugs.launchpad.net/ubuntu/+source/mysql-5.7/+bug/1710146
在连接中,有人提到过使用jemalloc好像能够解决此问题。

测试jemalloc

同样是测试上面两种场景, 使用jemalloc方式如下:

yum install jemalloc
在mysqld_safe中,最前面添加如下信息:
export LD_PRELOAD="/lib64/libjemalloc.so.1"
重启启动mysql实例

使用pt工具确认是否是用你了jemalloc
pt-mysql-summary  -S /tmp/mysql-3320.sock --user root --password 123456|grep -A 5 "Memory management" 
显示如下信息,则代表使用了jemalloc

场景1使用sysbench压测数据库

启动MySQL后内存使用情况,大约在150M:

MySQL内存不释放分析


sysbench压测60s

sysbench --db-driver=mysql /usr/share/sysbench/oltp_read_only.lua --mysql-host=127.0.0.1 --mysql-port=3320  --mysql-db=sbtest --mysql-user=root --mysql-password=123456 --tables=16 --table_size=500000 --threads=128 --time=60 --report-interval=1 run

观察内存使用情况:

MySQL内存不释放分析



压测时内存增长到550M左右,随着sysbench连接断开而释放了一部分内存, 保持在370M左右。


场景2 load 一个大事务的insert语句

重启MySQL观察内存使用情况, 大约占用150M:

MySQL内存不释放分析


source 文件:

mysql> source test.sql

source完成后观察内存使用情况:

MySQL内存不释放分析



随着SQL执行完成,内存释放到了300M。


小结

通过以上对比,可以得出,jemalloc内存分配方式,确实可以使内存快速释放给操作系统,减少系统内存使用过高而被OOM

MySQL到底有没有释放内存?

继续分析MySQL实际执行中,到底有没有主动释放内存?在哪里释放的内存?

通过gdb调试

  1. 先登录到mysql中

  2. 另外一个窗口,执行gdb attach 到mysql进程

/opt/rh/devtoolset-8/root/bin/gdb  -p  `ps -ef|grep  -w mysqld|grep -v grep | awk {'print $2'}`
  1. 设置断点

(gdb) b /mydata/Project/mysql-server/sql/sql_parse.cc:1947
(gdb) b connection_handler_per_thread.cc:321
Breakpoint 2 at 0x166ee7f: file /mydata/Project/mysql-server/sql/conn_handler/connection_handler_per_thread.cc, line 321.
  1. gdb 中执行c

(gdb) c
Continuing
  1. mysql客户端source 文件

mysql> source test.sql

此时可能需要等待一段时间,因为文件比较大,mysql client 需要进行解析文件,只要在gdb 窗口等待即可,不用执行任何命令,直到出现下面这种情况:

gdb) c
Continuing.
[Switching to Thread 0x7fea2b008700 (LWP 29406)]

Thread 28 "mysqld" hit Breakpoint 1, dispatch_command (thd=0x7fea02819000, com_data=0x7fea2b007c90, command=COM_QUERY)
    at /mydata/Project/mysql-server/sql/sql_parse.cc:1947
1947      free_root(thd->mem_root,MYF(MY_KEEP_PREALLOC));
  1. 此时就可以开始调试,查看MySQL内存使用情况:

(gdb) p thd->main_mem_root
$1 = {free = 0x7fe9c2406020, used = 0x7fe9c29b3020, pre_alloc = 0x7fea02827020, min_malloc = 32, block_size = 8160, block_num = 863,
  first_block_usage = 0, max_capacity = 0, allocated_size = 844547312, error_for_capacity_exceeded = 0 '\000', error_handler = 0x14baa8b
     <sql_alloc_error_handler()>, m_psi_key = 8}

这里看到allocated_size=844547312 大约是800M,此时mysqld进程占用内存情况:

MySQL内存不释放分析



执行source 之前的内存使用情况+800M 约等于 目前的1.1g。
输入'n'执行下一步free_root后,在查看内存:


gdb) n
1961      thd->profiling.finish_current_query();
(gdb) p thd->main_mem_root
$2 = {free = 0x7fea02827020, used = 0x0, pre_alloc = 0x7fea02827020, min_malloc = 32, block_size = 8160, block_num = 4,
  first_block_usage = 0, max_capacity = 0, allocated_size = 8208, error_for_capacity_exceeded = 0 '\000',
  error_handler = 0x14baa8b <sql_alloc_error_handler()>, m_psi_key = 8}

此时allocated_size = 8208,也就是在MySQL层,执行完SQL后内存已经释放了,top看下操作系统情况:

MySQL内存不释放分析



这里使用的是jemalloc,内存进行了释放(当前占有内存约300M),如果是默认的glibc,则内存不会释放。
这里还分析到,内存的释放是SQL语句级别的,并不是整个事务结束后才释放,而是SQL执行完后即释放。


  1. 继续将会话退出,则会触发另外一个断点

MySQL内存不释放分析



会话退出,会接受到COM_QUIT 命令,接着会释放整个线程的内存,这时再看下top中mysqld内存使用情况:

MySQL内存不释放分析



会看到随着会话退出,内存又释放了40M左右,这部分内存就是mysql中缓存的source 文件大小,在SQL执行完后,会话不退出时,可以通过查看performance 表查看线程内存使用情况:



能看到,正好是等于文件的大小。


  1. kill 也会导致NET::buff释放
    通过测试,如果SQL执行完成,在另外一个会话Kill此线程,NET::buff也会被释放

这里的NET::buff并没有随着SQL执行完成释放,查看官方文档net_buffer_length的介绍:



大概意思是:
每个客户端会话线程会分配两个缓冲区:连接缓冲区和结果集缓冲区,并且可以动态放大到 max_allowed_packet 系统变量指定的字节大小, 每个 SQL 语句执行完成 之后,结果集缓冲区自动缩小到 net_buffer_length 变量指定的大小。然后文档并没有提到连接缓冲区会被释放,也就是说会被一直缓存,直到连接断开。


结论

  1. mysql中会主动释放内存

  2. 释放内存是语句级别,不是会话级别

  3. NET::buff内存是需要会话退出后,内存才会释放

  4. kill也会释放会话所占有的内存

  5. jemalloc比glibc内存分配机制更好一些,能及时将内存释放给操作系统