当 GreatSQL 数据库处于高并发高负载时,可能会发现 mysqld 进程的内存消耗远远超出设置的 innodb_buffer_pool_size 时,有时候甚至会高达甚至超过系统内存的90%,遇到这种问题时,心里经常会发慌,担心下一秒内存就会爆了发生 OOM,或者数据库hang死不响应。
本文和大家试着使用 GreatSQL 中的 sys schema 和 performance_schema 进行深入分析,找出内存消耗大户的源头,并尽可能解决问题。
下面是详细的排查方法和步骤。
1. 确认实际内存消耗
1.1 操作系统层面分析
先检查确认 mysqld 进程的内存具体消耗占用情况,做到心里有数,避免真的下一秒就发生 OOM 的问题:
在上述结果中重点关注几个指标:
- RES(Resident Memory):物理内存占用,约 27.7G。
- VIRT(Virtual Memory):虚拟地址空间大小,约 30.6G。
看到 mysqld 进程当前内存消耗占比约 90%,还算可控,没到火烧眉毛的境地。
继续使用 pmap 查看 mysqld 进程中的内存分布情况:
看到大量的匿名内存(anon)消耗较多内存,这可能是由以下几个原因引起的:
- 动态分配的内存:
GreatSQL 在运行过程中会频繁地进行内存分配和释放,这些内存通常是以匿名映射的形式存在于进程的虚拟地址空间中。
例如,GreatSQL 的线程池、缓存、临时表等都会动态分配内存。
- 缓冲区和缓存:
GreatSQL 使用大量的缓冲区和缓存来提高性能,例如InnoDB Buffer Pool(以下简称 IBP)、InnoDB Log Buffer等。
这些缓冲区和缓存通常会占用大量的匿名内存。
线程堆栈:
每个线程都有自己的堆栈空间,这些堆栈空间也是匿名内存的一部分。
为了响应用户请求而创建了大量线程,那么这些线程的堆栈空间会占用不少内存。
临时文件:
GreatSQL 在处理大查询或排序操作时,可能会使用临时表、临时文件,其内存映射也会占用匿名内存。
内存泄漏:
如果 GreatSQL 存在内存泄漏问题,也会导致匿名内存不断增加。
可以针对上述各个模块/维度做进一步排查分析。
1.2 检查 IBP 内存相关配置
- 使用以下 SQL 命令查询 IBP 等内存消耗较多的相关模块内存配置
从上面可见 IBP 设置为 20G,但是 mysqld 进程的内存占用为 27.7G,超过 IBP 较多,这可能是由于用户的 SQL 请求(比如效率较低的慢查询 SQL)其他模块或线程引起。还需要继续排查。
2. 利用 Performance Schema 排查内存消耗来源
从 5.6.6 版本开始,Performance Schema 默认启用,是一个内置的性能诊断工具,用于实时监控和分析 GreatSQL 服务器的运行状态。它提供了详细的性能数据,包括 内存分配的全局视图、SQL 语句的执行时间、线程活动、锁等待等详细信息,帮助开发者和 DBA 识别和解决性能瓶颈。
2.1 按内存模块查看占用
使用 memory_summary_global_by_event_name 按模块查看内存分配情况:
- EVENT_NAME:具体内存分配的模块名称,如 memory/innodb/buffer_pool、memory/sql/temporary_table 等。
- CURRENT_NUMBER_OF_BYTES_USED:当前分配的内存总量。
例如:
- 如果 memory/innodb/buf_buf_pool 值较高,说明 InnoDB buffer pool 占用较高。其他几个包含 memory/innodb 关键字的,也都是和 InnoDB 存储引擎相关的内存模块。
- 其中 memory/mysys/MY_BITMAP::bitmap 主要用于管理位图数据结构 (bitmap) 的内存使用。它的设计初衷是为了实现高效的位存储与处理,主要用于存储和操作需要标志位(bit flag)来跟踪或控制的数据。其具体存储的数据包括但不限于:
索引或表分区的状态:位图被用来记录索引或分区的使用状态。例如,在分区表扫描时,通过位图可以高效管理哪些分区需要扫描或已经扫描。
事务或锁状态:记录事务的特定标志位或锁的使用状态,比如资源锁(resource locks)的分配状态。
InnoDB 的内部操作:位图被用于跟踪一些内部存储引擎的优化过程,例如自适应哈希索引、页的脏位(dirty bit)标记等。
线程管理:管理线程池中线程的分配和使用状态。
性能统计:在某些性能分析的场景下,位图用于记录启用或禁用的统计模块。
- 其中 memory/group_rpl/GCS_XCom::xcom_cache 是 MGR Xcom cache,在 GreatSQL 8.0.32-26 中初始默认值即为 1GB,详情参考 Xcom cache分配静态化。其他几个包含 group_rpl 关键字特征的,也是和 MGR 相关的模块。
- 其中 memory/mysys/IO_CACHE 是一个重要的内存管理模块,主要用于管理和优化文件I/O操作。IO_CACHE 提供了一个高效的缓存机制,可以显著提高文件读写操作的性能。主要存储的数据有:
临时文件数据:排序、分组、联接等操作过程中生成的中间结果。
二进制日志:binlog 的写入和读取操作中使用缓存。
文件块:GreatSQL 访问文件时,将数据块加载到缓存中,避免重复读取。
表数据:表扫描或索引扫描时,用于缓存表或索引的数据。
如果 memory/sql/temporary_table 值较高,说明内存被临时表消耗。
如果 memory/innodb/hash_index 值较高,可能是 InnoDB 的自适应哈希索引占用内存。
上面的查询结果表明,memory/innodb/buf_buf_pool(IBP) 占用内存约 20G,memory/group_rpl/GCS_XCom::xcom_cache(MGR XCom Cache) 占用内存约 1G,都是符合预期的。但是 memory/mysys/IO_CACHE 占用的内存较高,需要重点排查。
2.2 跟踪各模块内存使用变化
可以每间隔一段时间重复执行下面的 SQL 请求,观察各个模块的内存消耗变化,找出内存消耗增长较快的模块,它们可能就是导致 mysqld 进程消耗较大内存的“元凶”。
结合前面各模块当前占用的内存情况,从上述查询结果综合分析看,较大概率应该就是 memory/mysys/IO_CACHE 模块消耗内存过大。
2.3 按线程查看内存占用
接着继续查看各线程内存占用情况,确认是否有个别线程(尤其是长连接线程)消耗了过多内存资源。使用 memory_summary_by_thread_by_event_name 查看各线程的内存分配,同时关联查询 threads 视图,可以显示各线程当前正在执行的 SQL 请求及其执行耗时:
从上面的查询结果可见,当前有较多的 LOAD DATA 请求正在运行,有可能是它们导致的内存占用较高的原因。
其中
- CURRENT_NUMBER_OF_BYTES_USED 表示当前分配但尚未释放的内存块的聚合大小。CURRENT_NUMBER_OF_BYTES_USED = SUM_NUMBER_OF_BYTES_ALLOC − SUM_NUMBER_OF_BYTES_FREE。
- SUM_NUMBER_OF_BYTES_ALLOC 表示已分配内存块的聚合大小。
- SUM_NUMBER_OF_BYTES_FREE 表示已释放内存块的聚合大小。
排查分析道这里,基本上可以推断是由于有大量并发 LOAD DATA 导入数据请求导致 mysqld 内存占用较高。
3. 利用 sys schema 简化分析
相对于用 Performance Schema 排查分析,采用 sys schema 分析则更简单省事。接下来介绍如何利用 sys schema 分析。
GreatSQL sys schema 是一组视图、存储过程和函数的集合,它基于 performance_schema 提供了更易读和易用的性能数据汇总。sys schema 通过简化复杂的性能指标,帮助数据库管理员和开发人员快速诊断和优化 GreatSQL 的性能问题。
3.1 查看全局及各模块内存分布
首先,查看当前全部内存分配情况:
在 IBP 设置为 20G 的前提下,从 memory_global_total 查询到的内存分配总数并没有超过太多,说明较大可能性是由于用户的 SQL 请求(比如效率较低的慢查询 SQL)或其他模块引起。
继续查询内存使用的全局分布情况:
在 sys schema 中,大部分视图都同时存储原始数据以及格式化后可读性更强的两种视图。所以上面的 SQL 查询还可以改成查询原始未格式化的视图:
从上面两个查询结果可知,除了 IBP 和 MGR 之外,模块 memory/mysys/IO_CACHE 占用的内存最高,是重点分析排查对象。
查看 sys.memory_global_by_current_bytes 视图定义,可知它的原始数据来自 performance_schema:
从 performance_schema 中读取源数据,并进行格式化处理,大大提升了可读性。同理,其他视图也如此。
3.2 查看各线程内存分布
查看各线程的内存使用详情:
同样地,还可以和 performance_schema.threads 关联查询,就可以找到相应线程/会话中可能正在运行的 SQL 请求。
从查询结果明显可知,是由于当前有大量 root@localhost 连接会话执行 LOAD DATA 导入数据,这些会话占用了较多内存。
3.3 查看各用户内存分配
如果怀疑是某个用户的查询导致内存消耗过高,还可按用户分别统计:
看到 root 用户历史上总消耗了 3.95 TB 内存,可见它的嫌疑最大。执行 SHOW PROCESSLIST 可以看到当前 root 用户在反复执行并发导入大量数据,这个原因造成了内存总消耗超过较大,等待导入完成后,自然就会回收释放。
综合以上两种分析方法和过程,基本上可以排查定位是什么原因导致 mysqld 进程占用过多内存。
4. 检查内存分配的主要可能原因
4.1 InnoDB 内存相关设置
InnoDB 模块可能消耗大量内存,以下参数需要关注:
- innodb_buffer_pool_size IBP缓冲池。
- innodb_log_buffer_size 事务日志缓冲区。
- innodb_adaptive_hash_index 自适应哈希索引,默认开启,可能占用额外内存。建议关闭。
- innodb_buffer_pool_instances 缓冲池分区数量,过多分区可能引起额外内存开销。
分别检查确认这些参数设置情况:
4.2 临时表内存
如果查询生成大量临时表,可能会占用内存。以下参数决定了临时表的大小和行为:
- tmp_table_size 和 max_heap_table_size:
- 临时表的创建数量:
4.3 线程/会话内存
高并发会导致内存分配超标,尤其是以下参数:
- thread_stack 每个线程的栈大小,默认 256KB。
- read_buffer_size / read_rnd_buffer_size / join_buffer_size / sort_buffer_size 线程级分配的内存缓冲区。
4.4 复杂查询的内存消耗
复杂的排序、联接、子查询等操作会额外分配内存缓冲区,如果有较多的慢查询也表明可能存在一些消耗较多内存的查询请求,可以通过查询以下变量确认消耗:
4.5 表缓存与元数据缓存
表和源数据缓存 table_open_cache 和 table_definition_cache 也可能占用较多内存:
5. 分析排查方法总结
- 确认内存使用是否超标:结合系统工具与 GreatSQL 内部视图分析。
- 确定具体内存分配模块:通过 performance schema 或 sys schema 系统视图查询。
- 检查确认内存、缓冲等相关参数是否设置合理:
如果临时表消耗过高,降低 tmp_table_size 和 max_heap_table_size。
如果线程占用过多内存,调整 read_buffer_size 和 join_buffer_size。
如果 IBP 占用过多内存,则适当调低 innodb_buffer_pool_size,一般上限设置为物理内存的 70% 左右。
- 优化查询和索引设计,避免复杂查询和不必要的临时表创建。
- 优化慢查询 SQL 请求,避免低效率的 SQL 请求消耗过多CPU、内存及磁盘 I/O 资源,并对其他 SQL 请求造成间接关联影响。
相信通过以上方法,基本上可以分析定位并解决 mysqld 进程内存占用异常的问题。
6. 如何避免 GreatSQL 消耗过多内存
从上面的分析排查过程及思路中,也就知道了有哪些方法可以避免让 GreatSQL 在运行过程中消耗太多内存,以下是几条建议:
- 采用 jemalloc 代替 glibc 自带的 malloc 库,其优势在于减少内存碎片和提升高并发场景下内存的分配效率,提高内存管理效率的同时还能降低数据库运行时发生 OOM 的风险。在本案例中,原来 mysqld 进程最高跑到27.8G(占物理内存90%),改用 jemalloc 后最高只跑到24.2G(占物理内存78.7%),效果相当显著。
- 根据数据库负载以及业务特征,设置合适的 IBP 值,一般上限设置为物理内存的 70% 左右,设置过大容易造成 OOM。同时也要根据实际情况,适当调整(或调低)会话级缓冲池,包括 tmp_table_size / max_heap_table_size / read_buffer_size / read_rnd_buffer_size / join_buffer_size / sort_buffer_size / thread_stack 等多个参数,同时也要适当控制最大连接数参数 max_connection。
- 加强监控,及时发现并处理一些消耗内存较大的 SQL 操作,比如大事务(把大事务拆分成多个小事务)、长事务(长时间不提交的事务要做好监控并发出告警,甚至主动终止这些事务),以及全表扫描、分组、排序、多表联接(是否可以添加合适的索引)等类型,这种请求通常比较容易产生临时表、临时文件,通常也是慢查询 SQL,需要重点关注。要定时巡查并优化这些慢查询 SQL。
重点做好上面这几点,基本上就能避免大部分容易造成 mysqld 消耗内存过多的情况,让 GreatSQL 运行的更丝滑平稳。