MySQL数据库内存使用分析与优化

本文分析了MySQL数据库的内存使用情况,包括内存结构、监控方法以及内存管理机制,并提供了一些优化建议。

原文标题:MySQL数据库中内存使用分析

原文作者:牧羊人的方向

冷月清谈:

MySQL数据库的内存使用主要分为全局共享缓存和线程独立缓存两部分。全局共享缓存包括InnoDB缓冲池、REDO日志缓冲、索引缓冲区、查询缓存等,用于缓存常用数据。线程独立缓存为每个连接会话分配,包括排序缓冲、随机读缓冲、连接缓冲等。

MySQL内存占用等于全局共享缓存和所有线程缓存之和。全局共享缓存通常常驻内存,而线程缓存会随着连接释放而关闭。

监控MySQL内存使用情况可以通过`performance_schema`库中的相关表实现,例如`memory_summary_by_account_by_event_name`、`memory_summary_by_thread_by_event_name`等,可以按账户、主机、线程等维度查看内存使用情况。

MySQL的内存管理分为Server层和InnoDB层。InnoDB层通过Free List、LRU List等管理InnoDB缓冲池,使用mmap方式申请内存。Server层通过mem_root管理共享缓存和线程缓存,默认使用ptmalloc内存分配器,可能存在内存碎片和释放不及时的问题。其他内存分配器如tcmalloc和jemalloc可以提供更好的性能和内存管理。

实际生产环境中,MySQL实例占用的内存可能比配置的`innodb_buffer_pool`高很多,这是因为内存分配器或内存碎片导致的。可以使用其他内存分配器或在测试环境中使用gdb命令主动回收内存碎片。

怜星夜思:

1、文章提到了ptmalloc, tcmalloc和jemalloc三种内存分配器,它们各自的优缺点是什么?除了这三种之外,还有其他的内存分配器可以选择吗?
2、如何确定MySQL当前使用的内存分配器,以及如何更改它?
3、除了文章中提到的方法,还有哪些方法可以监控和分析MySQL的内存使用情况?

原文内容

在实际系统环境中,MySQL实例的内存使用随着业务的增长缓慢增长,有些时候并没有及时的释放。本文简要介绍下MySQL数据库中和内存相关的配置,以及分析内存的实际使用情况,以进行应急和调优处理。

1、MySQL内存结构
在MySQL中内存的占用主要由两部分组成:全局共享缓存global buffers和所有线程独立缓存thread buffers
  • Global buffers主要用于缓存数据页、索引页、数据字典等常用数据,这部分为常驻内存;

  • Thread buffers针对每个数据库连接会话会独立分配缓存,thread buffer与连接数成正比,连接数越高则总的thread buffers占用越高。这部分缓存通常会随着连接的释放而关闭。

MySQL中使用的内存大小等于全局共享缓存(Sharing+innodb_buffer_pool)和所有线程缓存之和,如下图所示:

1)global_buffers:包括Sharing buffers部分+InnoDB_Buffer_Pool
  • innodb_buffer_pool_size:InnoDB高速缓存,缓存innodb表的索引,数据,插入数据时的缓冲,以及事务锁、自适应哈希等。通过“show engine innodb status \G”和“show global status like ‘innodb_buffer_pool_%’”可以查看innodb_buffer_pool的表现。

  • innodb_log_buffer_size:InnoDB REDO日志缓冲,提高REDO日志写入效率。由于InnoDB在事务提交前,并不将改变的日志写入到磁盘中,因此在大事务中,可以减轻磁盘I/O的压力。

  • innodb_additional_mem_pool_size :用来存放Innodb的内部目录,由系统自动调整。

  • key_buffer_size:指定索引缓冲区的大小,它决定MyISAM表索引处理的速度,尤其是索引读的速度,只对MyISAM表起作用。即使不使用MyISAM表,但是内部的临时磁盘表是MyISAM表,也要使用该值。

  • query_cache_size:查询高速缓存,缓存查询结果,当打开时候,执行查询语句会进行缓存,读写都会带来额外的内存消耗,下次再次查询若命中该缓存会立刻返回结果。

  • table_open_cache:表空间文件描述符缓存,提高数据表打开效率。存放当前已经打开的表句柄,与表创建时指定的存储引擎相关。当把table_open_cache设置的过大时,如果系统处理不了这么多文件描述符,那么就会出现客户端失效、连接不上。

  • table_definition_cache:表定义文件描述符缓存,提高数据表打开效率。MySQL需要打开frm文件,并将其内容初始化为Table Share对象。这里存放与存储引擎无关的,独立的表定义相关信息。

2)all_thread_buffers:max_threads(当前活跃连接数) * (会话级内存分配总和)
  • read_buffer_size:顺序读缓冲,提高顺序读效率。MySQL读入缓冲区的大小,将对表进行顺序扫描的请求将分配一个读入缓冲区,MySQL会为它分配一段内存缓冲区,read_buffer_size变量控制这一缓冲区的大小,如果对表的顺序扫描非常频繁,并你认为频繁扫描进行的太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。

  • read_rnd_buffer_size:随机读缓冲,提高随机读效率。MySQL的随机读缓冲区大小,当按任意顺序读取行时(列如按照排序顺序)将分配一个随机读取缓冲区,进行排序查询时,MySQL会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要大量数据可适当的调整该值,但MySQL会为每个客户连接分配该缓冲区所以尽量适当设置该值,以免内存开销过大。

  • sort_buffer_size:排序缓冲,提高排序效率。connection级参数,在每个connection第一次需要使用这个buffer的时候,一次性分配设置的内存。

  • join_buffer_size:表连接缓冲,提高表连接效率。MySQL在完成某些Join需求的时候(all/index join),为了减少参与Join的“被驱动表”的读取次数以提高性能,需要使用到Join Buffer来协助完成 Join操作。

  • binlog_cache_size:二进制日志缓冲,提高二进制日志写入效率为每个session 分配的内存,在事务过程中用来存储二进制日志的缓存,作用是提高记录bin-log的效率。

  • tmp_table_size:内存临时表,提高临时表存储效率。当需要做类似group by操作生成的临时表大小,提高联接查询速度的效果,调整该值直到created_tmp_disk_tables/created_tmp_tables*100% <= 25%。

  • thread_stack:线程堆栈,暂时寄存SQL语句/存储过程、线程运行时的信息等。每个连接线程被创建时,MySQL给它分配的内存大小。当MySQL创建一个新的连接线程时,需要给它分配一定大小的内存堆栈空间,以便存放客户端的请求的Query及自身的各种状态和处理信息。

  • thread_cache_size:线程缓存,降低多次反复打开线程开销。当客户端断开之后,服务器处理此客户的线程将会缓存起来以响应下一个客户而不是销毁(前提是缓存数未达上限)。

  • net_buffer_length:线程持连接缓冲以及读取结果缓冲。客户发出的SQL语句期望的长度,如果语句超过这个长度,缓冲区自动地被扩大,直到max_allowed_packet个字节。

  • bulk_insert_buffer_size:MyISAM表批量写入数据缓冲。如果进行批量插入,可以增加bulk_insert_buffer_size变量值的方法来提高速度,只对myisam表使用。

调整对应的配置后,相应的内存使用大小可以通过计算得到(参考网址http://www.mysqlcalculator.com/):

另外,也可以通过SQL语句获得各个配置的大小(假设最大连接为200),如下所示:

select VARIABLE_NAME,VARIABLE_VALUE/1024/1024 MB from performance_schema.global_variables WHERE VARIABLE_NAME in ('key_buffer_size','query_cache_size','tmp_table_size','innodb_buffer_pool_size','innodb_additional_mem_pool_size','innodb_log_buffer_size')
union all
SELECT 'sort_buffer_size',(v1.VARIABLE_VALUE*v2.vv) MB
FROM ( select 200 as VARIABLE_VALUE from performance_schema.global_variables WHERE VARIABLE_NAME='MAX_CONNECTIONS' ) AS v1,
( SELECT VARIABLE_VALUE/1024/1024 vv FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'sort_buffer_size' ) AS v2
union all
SELECT 'read_buffer_size',(v1.VARIABLE_VALUE*v2.vv) MB
FROM ( select 200 as VARIABLE_VALUE from performance_schema.global_variables WHERE VARIABLE_NAME='MAX_CONNECTIONS' ) AS v1,
( SELECT VARIABLE_VALUE/1024/1024 vv FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'read_buffer_size' ) AS v2
union all
SELECT 'read_rnd_buffer_size',(v1.VARIABLE_VALUE*v2.vv) MB
FROM ( select 200 as VARIABLE_VALUE from performance_schema.global_variables WHERE VARIABLE_NAME='MAX_CONNECTIONS' ) AS v1,
( SELECT VARIABLE_VALUE/1024/1024 vv FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'read_rnd_buffer_size' ) AS v2
union all
SELECT 'join_buffer_size',(v1.VARIABLE_VALUE*v2.vv) MB
FROM ( select 200 as VARIABLE_VALUE from performance_schema.global_variables WHERE VARIABLE_NAME='MAX_CONNECTIONS' ) AS v1,
( SELECT VARIABLE_VALUE/1024/1024 vv FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'join_buffer_size' ) AS v2
union all
SELECT 'thread_stack',(v1.VARIABLE_VALUE*v2.vv) MB
FROM ( select 200 as VARIABLE_VALUE from performance_schema.global_variables WHERE VARIABLE_NAME='MAX_CONNECTIONS' ) AS v1,
( SELECT VARIABLE_VALUE/1024/1024 vv FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'thread_stack' ) AS v2
union all
SELECT 'binlog_cache_size',(v1.VARIABLE_VALUE*v2.vv) MB
FROM ( select 200 as VARIABLE_VALUE from performance_schema.global_variables WHERE VARIABLE_NAME='MAX_CONNECTIONS' ) AS v1,
( SELECT VARIABLE_VALUE/1024/1024 vv FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'binlog_cache_size' ) AS v2

对于连接配置,可以查看系统中历史最大连接情况进行评估

mysql> show global status like 'Max_used_connections';
+----------------------+-------+

| Variable_name | Value |
+----------------------+-------+

| Max_used_connections | 28 |
+----------------------+-------+

1 row in set (0.00 sec)
2、MySQL中内存相关表

在MySQL中有内存相关的表监控内存的使用情况

mysql> show tables like '%memory%';
+-----------------------------------------+

| Tables_in_performance_schema (%memory%) |
+-----------------------------------------+

| memory_summary_by_account_by_event_name |
| memory_summary_by_host_by_event_name |
| memory_summary_by_thread_by_event_name |
| memory_summary_by_user_by_event_name |
| memory_summary_global_by_event_name |
+-----------------------------------------+

5 rows in set (0.00 sec)

以上表的监控统计,分别统计帐户、主机、线程、用户和全局事件执行内存操作等信息。

1)memory_summary_by_account_by_event_name

按MySQL账户和事件名称分类的内存使用情况摘要,识别哪些账户正在使用最多的内存,以及这些内存是如何分配的,如查询、系统锁等特定事件。

select USER, HOST, EVENT_NAME, SUM_NUMBER_OF_BYTES_ALLOC from performance_schema.memory_summary_by_account_by_event_name order by SUM_NUMBER_OF_BYTES_ALLOC desc limit 10;

2)memory_summary_by_host_by_event_name

按连接到MySQL的主机和事件名称分类的内存使用情况摘要,用来识别识别哪些远程主机或IP地址正在消耗最多的内存。

select HOST, EVENT_NAME, SUM_NUMBER_OF_BYTES_ALLOC from performance_schema.memory_summary_by_host_by_event_name order by SUM_NUMBER_OF_BYTES_ALLOC desc limit 10;

3)memory_summary_by_thread_by_event_name

按MySQL线程ID和事件名称分类的内存使用情况摘要,对于特定的长时间运行查询或事务的内存使用情况特别有用,因为每个线程通常与一个特定的查询或事务相关联。

select thread_id, event_name, SUM_NUMBER_OF_BYTES_ALLOC from performance_schema.memory_summary_by_thread_by_event_name order by SUM_NUMBER_OF_BYTES_ALLOC desc limit 10;

得到thread_id后,查找performance_schema.threads表获得对应的processlist_id和processlist_info:

mysql> select * from performance_schema.threads where THREAD_ID=49\G
*************************** 1. row ***************************
THREAD_ID: 49
NAME: thread/sql/one_connection
TYPE: FOREGROUND
PROCESSLIST_ID: 10
PROCESSLIST_USER: root
PROCESSLIST_HOST: tango-DB01
PROCESSLIST_DB: tango
PROCESSLIST_COMMAND: Query
PROCESSLIST_TIME: 84
PROCESSLIST_STATE: User sleep
PROCESSLIST_INFO: select count(1),sleep(2000) from tango.t2 for update
PARENT_THREAD_ID: NULL
ROLE: NULL
INSTRUMENTED: YES
HISTORY: YES
CONNECTION_TYPE: SSL/TLS
THREAD_OS_ID: 1657
RESOURCE_GROUP: USR_default
1 row in set (0.00 sec)

也可以通过THREAD_ID,查找performance_schema.events_statements_current和performance_schema.events_statements_history获得对应的历史记录信息,定位到具体的SQL语句信息,以后续应急或优化。

4)memory_summary_by_user_by_event_name

按MySQL用户和事件名称分类的内存使用情况摘要,更侧重于基于用户名而不是完整的账户信息(包括主机)的内存使用。

select USER, EVENT_NAME, SUM_NUMBER_OF_BYTES_ALLOC from performance_schema.memory_summary_by_user_by_event_name order by SUM_NUMBER_OF_BYTES_ALLOC desc limit 10;

5)memory_summary_global_by_event_name

包括全局的、不按任何特定账户、主机、线程或用户分类的内存使用情况摘要,提供整个MySQL服务器内存使用的概览,并允许按事件名称来查看这些内存是如何分配的。

select event_name, SUM_NUMBER_OF_BYTES_ALLOC from performance_schema.memory_summary_global_by_event_name order by SUM_NUMBER_OF_BYTES_ALLOC desc LIMIT 10;
3、MySQL内存分配管理
MySQL的内存虽然划分为sharing buffers+innodb_buffer_pool和thread buffers,但实际上是按照Server层和innodb层进行内存管理,而且这两部分是按照不同的内存管理方式进行管理的。
  • InnoDB层则主要由Free List、LRU List、FLU List等多个链表来统一管理Innodb_buffer_pool。Innodb_buffer_pool内存初始化是通过「mmap()」方式直接向操作系统申请内存,最终会申请Innodb_buffer_pool_size大小的文件映射段动态内存。「mmap()」方式申请的内存会在文件映射段分配内存,而且在释放时会直接归还系统。这部分内存空间初始化后仅仅是虚拟内存,等真正使用时,才会分配物理内存。

  • Server层是由mem_root来进行内存管理,包括Sharing与Thead buffers。「mem_root」结构体使用内存分配管理器进行内存统一管理,避免频繁调用内存操作,提升性能,统一的分配和管理内存也可以防止发生内存泄漏。

MySQL首先通过「init_alloc_root」函数初始化一块较大的内存空间,实际上最终是「通过malloc函数向内存分配器申请内存空间」,然后每次再调用alloc_root函数在这块内存空间中分配出内存进行使用,其目的就是将多次零散的malloc操作合并成一次大的malloc操作,以提升性能。

在Linux中常用的内存分配管理器有三种:ptmalloc(Glibc)、tcmalloc(Google)、jemalloc(FreeBSD)。MySQL 默认使用的是glibc的ptmalloc作为内存分配器。
  • ptmalloc(glibc):ptmalloc是GNU C库(glibc)中的默认内存分配器,广泛用于Linux系统。它提供了一套标准的内存分配和释放函数,如malloc、free、realloc等。ptmalloc通过维护内存池来优化内存分配,并尝试减少内存碎片。然而,由于它是glibc的一部分,因此在某些情况下,其性能可能不如其他专为性能而设计的内存分配器。

  • tcmalloc(Google):tcmalloc用于多线程环境,并通过减少锁的竞争和内存碎片来提高性能。tcmalloc使用线程本地缓存(Thread-Caching Malloc)的概念,将内存分配的任务分散到不同的线程中,以减少对共享数据结构的竞争。此外,tcmalloc还采用了一些优化策略,如小对象合并、高效的分配器缓存等,以进一步提高性能。

  • jemalloc(FreeBSD):jemalloc是由FreeBSD社区开发的一款通用的内存分配器,并逐渐被其他系统广泛采用。jemalloc的设计目标是提供高度可扩展性和低碎片化的内存分配。它使用了一种称为arena的并发数据结构来管理内存,每个arena都有自己的内存池和缓存,以减少锁的竞争。jemalloc还采用了一种称为“大小类”(size classes)的机制来管理不同大小的内存分配请求。它将内存分配请求划分为不同的大小类,并为每个大小类维护一个独立的内存池和缓存。这种机制使得jemalloc在处理不同大小的内存分配请求时具有更高的灵活性和效率。

在实际生产系统中,遇到MySQL实例实际占用的内存比innodb_buffer_pool配置的高很多,但是没有及时释放的现象。大部分是因为内存分配管理器占用很多内存不释放,另一部分是因为内存碎片。另外在部分场景下,原生的MySQL使用的ptmalloc内存管理存在内存释放不及时的问题,所以在基于MySQL系列的国产数据库在内存管理上进行了部分优化,比如使用tcmalloc替代ptmalloc进行内存管理。通过以下命令,也可以查看实际使用的内存管理方式:

pt-mysql-summary  -S /tmp/mysql.sock --user root --password xxxxxx|grep -A 5 "Memory management"
# Memory management library ##################################
jemalloc is not enabled in mysql config for process with id 1339
# The End ####################################################

针对ptmalloc(glibc)内存管理的缺陷(调用glibc申请的内存使用完毕后,归还给OS时没有被正常回收,而变成了碎片,随着碎片的不断增长,就能看到mysqld进程占用的内存不断上升)。在测试环境,可以调用gdb函数主动回收释放内存碎片空间:

gdb --batch --pid `pidof mysqld` --ex 'call malloc_trim(0)'

以上是MySQL数据库内存配置和管理的相关知识。

参考资料:

  1. https://dev.mysql.com/doc/refman/8.0/en/memory-use.html

  2. https://blog.csdn.net/dc666/article/details/78901341

  3. https://blog.csdn.net/n88Lpo/article/details/126925517/

  4. https://zhuanlan.zhihu.com/p/264916825

  5. https://www.modb.pro/db/60336

可以使用一些监控工具,比如Prometheus、Grafana,结合MySQL的性能模式,可以实时监控内存的使用情况,并设置告警。还可以用一些分析工具,比如pt-query-digest,分析慢查询和内存消耗,找出潜在的性能瓶颈。

我推荐使用Performance Schema,它提供非常详细的内存使用信息,可以按账户、主机、线程等维度进行分析。不过,Performance Schema本身也会消耗一些性能,需要根据实际情况进行配置和调优。

更改内存分配器可不是小事,一定要做好充分的测试,包括功能测试、性能测试和稳定性测试。最好先在测试环境跑一段时间,观察一下内存使用情况和性能指标,确认没问题再上线。

是的,编译时指定分配器是最常用的方法。一些发行版可能也提供了预编译的、使用不同分配器的MySQL版本,可以直接安装。另外,有些第三方工具也可能允许在运行时动态加载不同的分配器,但这种方法不太常见,而且风险更大。

确定MySQL使用的内存分配器,可以用pt-mysql-summary命令,或者直接查看MySQL的编译选项。更改分配器的话,需要重新编译MySQL,在configure的时候指定--with-malloc=system|tcmalloc|jemalloc等选项。需要注意的是,更改分配器可能会影响MySQL的稳定性,建议先在测试环境 thoroughly 测试。

top 命令可以查看MySQL进程的内存占用情况。不过,top显示的是整个进程的内存使用,无法细分到各个内存区域。如果需要更详细的信息,还是得用性能模式或者其他专业的监控工具。

关于这个问题,ptmalloc作为glibc的默认分配器,胜在稳定和兼容性好,但是容易产生内存碎片,而且多线程环境下效率较低。tcmalloc是Google开发的,多线程性能优秀,内存碎片也控制得不错。jemalloc是FreeBSD的,注重可扩展性和低碎片,分配速度也很快。至于其他的分配器,当然还有,比如mimalloc,据说性能也很强,可以研究一下。

我之前也遇到过ptmalloc内存碎片的问题,后来换了tcmalloc,效果确实不错,MySQL的内存占用稳定了很多,性能也有提升。不过,换分配器之前最好先测试一下,确保兼容性没问题。

补充一下,选择内存分配器要看具体场景。比如,如果你的MySQL负载主要是单线程的,ptmalloc可能就够用了。但如果是高并发、多线程的,tcmalloc或者jemalloc就更合适。另外,还要考虑操作系统的兼容性,以及分配器本身的配置和调优。