写点什么

告别内存 OOM,解决 MySQL 内存增长问题

  • 2024-06-04
    广东
  • 本文字数:2835 字

    阅读完需:约 9 分钟

告别内存OOM,解决MySQL内存增长问题

本文分享自华为云社区《【华为云MySQL技术专栏】MySQL内存增长问题分析案例》,作者:GaussDB 数据库。

前言


在现网环境中,偶尔会遇到客户实例内存 OOM(Out Of Memory,即内存耗尽或溢出)的情况。MySQL 数据库作为一款面向高并发应用场景的系统软件,因其应用场景复杂且函数调用链极长,导致分析内存异常问题变得非常困难。


MySQL 提供了 Performance Schema 功能,用于跟踪其性能指标,包括内存占用情况。但该工具是有一定的局限性,只能监控通过 MySQL 提供的内存分配接口分配的内存,不能监控直接使用 malloc 或者 new 函数分配的内存。这种情况我们就可以借助 jeprof 工具(jeprof 是 jemalloc 配套的内存分析工具)来定位,以下是借助 jeprof 定位 MySQL 内存问题的分析过程。

问题描述


在生产环境中,某客户的实例频繁出现 OOM,通过排查客户业务,发现是大事务导致的内存异常增长,该实例的 MySQL 版本为 8.0.22(MySQL 8.0.25 以后的版本解决了内存异常增长问题),我们通过如下方式在本地进行复现:


1. 采用 sysbench 往一张表中压入 100000000 条数据,如下:


sysbench --mysql-host=xxx  --mysql-port=xxx --mysql-user=xxx  --mysql-password=xxx  \/usr/share/sysbench/oltp_read_write.lua  --tables=1  \--table_size=100000000 prepare
复制代码


2. 随后开启一个大事务,比如更新某一列数据,如下:


begin;  update sbtest1 set  k=k+1;
复制代码


该语句执行前,通过 top 命令查看到的内存使用情况如下:



语句执行过程中,内存持续增长,执行完毕时,其内存占用如下:



执行过程中,物理内存增长了近 2.6g,显然占用异常,若并发量较大的情况下,势必会 OOM。

定位过程


1. 查看 performance_schema 相关的内存监控数据


遇到 MySQL 的内存问题,首先想到的是打开 performance_schema 开启 MySQL 自带的内存监控,这些监控在相当一部分场景下的确发挥了重要作用。


例如,可以使用如下的命令查看内存情况:


select event_name, current_alloc from sys.memory_global_by_current_bytes limit 20;
复制代码


但 performance_schema 监控数据也有一些局限性。比如当前的这个场景,MySQL 自带的内存监控并没有采样到任何的内存变化。这是因为自带的内存监控只能监控到通过 MySQL 内存分配函数分配的内存,比如以 mem_block_info_t 为单位的 MySQL 内存分配函数等。所以,直接通过 malloc 或者 new 分配的函数,自带内存监控是无法监控到的


2. 通过 jeprof 等工具采样内存


jeprof 可以捕获所有的 malloc 和 free 函数的调用,不过由于 glibc 默认的分配器是 ptmalloc,因此,需要一些配置将默认的 ptmalloc 替换成 jemalloc。


下载 jemalloc 源码,启用--enable-prof,编译出对应的 libjemalloc.so.2,jeprof 工具即可,具体如下:


步骤一:下载 jemalloc 源码,进入解压后的目录,执行如下命令编译出对应的 libjemalloc.so.2,jeprof 工具;


cd {jemalloc解压后的目录}  ./autogen.sh --enable-prof  ./configure --enable-prof  
复制代码


步骤二:配置 jeprof 内存采样,配置相关的选项参数, 具体的参数配置参考;http://jemalloc.net/jemalloc.3.html


export MALLOC_CONF="background_thread:true,narenas:4,dirty_decay_ms:5000,prof:true,prof_prefix:/opt/workdir/logs/log,lg_prof_interval:30,lg_prof_sample:19"  
复制代码


步骤三:将 ptmalloc 替换为 jemalloc,若通过 ldd 命令能看到如下结果则证明配置成功。


export LD_PRELOAD='/jemalloc/libjemalloc.so.2' // 步骤一编译出来的libjemalloc.so.2路径  
复制代码



步骤四:启动进程,随后就可以在配置的 prof_prefix 路径下查看到生成的内存采样数据。

结果分析


关于 jeprof 结果解析的命令,这里不再赘述,具体可以通过 jeprof -h 查看。


就本文的问题而言,实际上只需要关心从语句执行开始到语句执行结束内存的变化部分就可。那么,就可以通过如下命令对比第一次生成的 profing 数据以及最后一次的 profling 数据:


/jemalloc/jeprof  --pdf ./mysqld  --base=log.start.heap log.end.heap > ../xxx.pdf
复制代码


这里取部分结果的截图做分析:




采样出来的数据是非常直观的,3072M 的内存全部是在 Rpl_transaction_write_set_ctx add_write_set 函数中分配的,通过查看 Rpl_transaction_write_set_ctx add_write_set 函数的实现如下:


void Rpl_transaction_write_set_ctx::add_write_set(uint64 hash) {    DBUG_TRACE;    DBUG_EXECUTE_IF("add_write_set_crash_no_memory", throw std::bad_alloc(););    write_set.push_back(hash);  }
复制代码


参考《STL 源码剖析》一书, vector 的底层数据结构其实就是一段连续的线性空间,以 start 指针和 fininsh 指针分别指向已申请的连续空间中目前已被使用的范围,以 end_of_storage 指针指向连续空间的尾端,当原空间使用完,也即 fininsh==end_of_storage 时,vector 会执行的动态扩容,也就是_M_realloc_insert 过程, 其步骤如下:


1) 以原空间大小的 2 倍申请一块新的空间;


2) 将原空间的内容拷贝到新空间;


3) 释放原空间。


而 write_set 恰是一个 vector, 因此可以确定 write_set 占用了 3072M 内存从而导致内存的异常增长。这其实也是 vector 错误使用的一个典型案例,对于这种大量的 push_back 场景,由于 vector 的 2 倍扩容,不仅会导致内存占用过多,扩容的过程中反复的申请新内存、释放旧内存也会导致性能问题。若仅考虑当前的问题场景,显然 list 是更优的选择。

注意事项


1. 编译 jemalloc 时,注意./autogen.sh --enable-prof 以及./configure --enable-prof 都需要加上--enable-prof 选项,若仅在./autogen.sh 是加上--enable-prof 参数,这种情况下你需要在启动的时候以如下方式启动 mysqld, 否则无法生成 profiling 文件:


LD_PRELOAD='/jemalloc/libjemalloc.so.2' bin/mysqld &
复制代码


2. 注意 MALLOC_CONF 的参数中 lg_prof_interval 参数。该参数设置过小,会严重影响 mysqld 的性能。当执行性能下降后,某些场景可能就不会复现。比如本文所涉及的问题场景,lg_prof_interval 设置的过小,就几乎观察不到明显的内存变化。


3. 通过 jeprof 采样到的数据没有捕获到 buffer pool 的内存分配,这是因为 jeprof 是通过在 jemalloc 中设置采样点来采集数据的,只有应用程序通过 malloc, free 分配释放内存才会被采集到,而 MySQL 的 buffer pool 内存是直接通过 mmap 系统调用分配的,不经过 jemalloc, 可以参考 MySQL 的 large_page_aligned_alloc 函数,所有的大内存均是通过该函数分配的。


inline void *large_page_aligned_alloc(size_t n_bytes) {    int mmap_flags = MAP_PRIVATE | MAP_ANON;    void *ptr = mmap(nullptr, n_bytes, PROT_READ | PROT_WRITE, mmap_flags, -1, 0);    return (ptr != (void *)-1) ? ptr : nullptr;  } 
复制代码

总结


上述客户业务中出现的问题,归根结底是代码中未对 vector 的内存进行限制,才有了大事务场景下内存无限增长最终导致 OOM 发生。华为云 RDS for MySQL 和 GaussDB(for MySQL)完全兼容 MySQL,华为云数据库在产品中对该问题进行了提前修复,后来开源 MySQL 在高版本中也对该问题也进行了修复。因此,MySQL 内存问题可以结合 MySQL 提供的 performance schema 内存表和 jeprof 工具来辅助定位。


点击关注,第一时间了解华为云新鲜技术~

发布于: 9 小时前阅读数: 12
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
告别内存OOM,解决MySQL内存增长问题_数据库_华为云开发者联盟_InfoQ写作社区