在实际系统环境中,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数据库内存配置和管理的相关知识。
参考资料:
- https://dev.mysql.com/doc/refman/8.0/en/memory-use.html
- https://blog.csdn.net/dc666/article/details/78901341
- https://blog.csdn.net/n88Lpo/article/details/126925517/
- https://zhuanlan.zhihu.com/p/264916825
- https://www.modb.pro/db/60336