作者:xuty本文来源:原创投稿*爱客生开源社区出品,原创内容未经授权不得使用,转载请联系小编并注明出处。一、背景在项目中经常遇到,在没有大并发活动SQL的情况下,MySQL占用的物理内存远大于配置的InnoDB_Buffer_Pool大小。一开始怀疑是被performance_schema吃掉了或者mysql有内存泄露,后来发现不是这样的。是因为不了解MySQL和Linux的内存管理,所以这篇文章会深入探讨一下。如果有不对的地方或者不严谨的地方还请大家多多指教~下面简单说说我对MySQL内存分配的基本理解。可能存在部分认知偏差:MySQL的内存使用主要由global_buffers和all_thread_buffers两部分组成,其中global_buffers是全局共享缓存,all_thread_buffers是所有线程的独立缓存,如下图:global_buffers:sharing+InnoDB_Buffer_Poolall_thread_buffers:max_threads(当前活跃连接数)*(线程内存)其中InnoDB_Buffer_Pool是MySQL占用的最大一块内存,是常驻内存,也就是说除非MySQL启动,否则不会释放进程退出。另一个消耗更多内存的部分是线程缓存。比如常见的join_buffer、sort_buffer、read_buffer等,通常都是和连接数成正比的。即连接数越高,并发度越高,线程缓存的总使用量就越高,但这种缓存往往在连接关闭时释放,并不是常驻内存。2.高内存现象CentOSLinuxrelease7.3.1611(Core)服务器版本:5.7.27-logMySQLCommunityServer(GPL)下面做个小测试,观察一下MySQL内存占用的变化。首先关闭performance_schema和innodb_buffer_pool_load_at_startup,防止缓存干扰。然后设置innodb_buffer_pool为100M。理论上innodb_buffer_pool最大只会占用100M,可以通过showengineinnodbstatus\G查看。通过sysbench创建一个100W的测试表,重启MySQL,观察MySQL目前一共占用了55536KB的物理内存,其中innodb_buffer_pool占用了432*16K=6912KB的内存,那我就算MySQL是占用50MB的物理内存默认启动。UIDPIDminflt/smajflt/sVSZRSS%MEMCommand997119800.000.001240908555360.69mysqld--------------------缓冲池和内存----------------------Totallargememoryallocated107380736Dictionarymemoryallocated116177Bufferpoolsize6400Freebuffers5960Databasepages432然后我们开始通过sysbench进行select压测,从4线程开始,4-8-16-32-64逐渐增加线程数,每次压测2分钟,最后观察MySQL总物理内存使用量的变化。从上图可以看出,当4线程压力测试开始时,内存占用飙升。主要是innodb_buffer_pool中大量涌入的数据页。然后在增加线程数的时候,由于innodb_buffer_pool已经饱和,达到了上限100M,所以波动不是很大。内存增加的原因主要是all_thread_buffers的增加。经过最终的64线程压测,MySQL的总物理内存使用量稳定在194MB左右,并且一直保持不变,没有被释放归还给操作系统。压测结束后,再次查看innodb_buffer_pool,可以看到Freebuffer是空的,100M完全满了。----------------------BUFFERPOOLANDMEMORY------------------------总大memoryallocated107380736Dictionarymemoryallocated120760Bufferpoolsize6400Freebuffers0Databasepages5897Olddatabasepages2156减去100M的innodb_buffer_pool,还有MySQL刚启动占用的50M,还有40MB+的内存占用,主要是all_threads。通过这个测试我们可以看出,之前的理解是随着连接的关闭释放线程缓存,其实是不正确的。MySQL不会将这部分缓存归还给操作系统,而只是在MySQL内部释放并重新使用。我将这种现象称为内存高水位线现象,因为它与Oracle中高水位线的概念非常相似。同样,当MySQL中的ibd文件被放大后,即使删除了整张表,也不会主动释放磁盘空间归还给操作系统,而是重新使用释放的磁盘空间,现象非常一致。PS:这里的sysbench压测是使用主键索引的单表where查询,并没有申请sort_buffer、join_buffer等,所以对于单session应用的线程缓存比较小。因此,最终线程缓存的总使用率并不是很高。如果是复杂的SQL,内存占用应该会比较高。3.Linux进程内存分配为了找出MySQL经常内存偏高的原因,首先查阅学习了Linux下的相关内存调用原理,从上到下分别是:只读段:包括代码和常量等;数据段:包括全局变量等;堆:包括动态分配的内存,从低地址向上增长;文件映射段:包括动态库、共享内存等,从高地址向下增长;栈:包括局部变量和函数调用的上下文等。其中,堆和文件映射段是我们讨论的重点,它们的内存是动态分配的。例如,使用C标准库的malloc()或mmap(),可以分别在堆和文件映射段动态分配内存。那么两者有什么区别呢?malloc()是C标准库提供的内存分配函数,对应于系统调用,有两种实现,分别是brk()和mmap()。1.brk方法对于小块内存(<128K),C标准库使用brk()来分配。即通过移动堆顶的位置来分配内存。内存释放后,并不会马上归还给系统,而是缓存起来再利用。优缺点:brk()方法可以减少缺页异常的发生,提高内存访问效率。但是由于内存并没有归还给系统,当内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。2、mmap匿名映射方式对于大块内存(>128K),C标准库使用mmap()进行分配,即在文件映射段中找到一块空闲内存并进行分配。mmap()分配的内存在释放时会直接归还给系统,所以每次执行mmap都会产生pagefault。优缺点:mmap()方法可以及时将内存归还给系统,避免OOM。但是在工作繁忙的时候,频繁的内存分配会导致大量的pagefault异常,增加内核的管理负担。这也是malloc只对大块内存使用mmap的原因。所谓缺页异常就是进程申请内存后,只分配了虚拟内存。这些申请的虚拟内存只有在第一次被访问时才会分配真正的物理内存,也就是通过缺页异常进入内核,然后内核才会分配物理内存(实质是建立虚拟内存之间的地址映射内存和物理内存)。brk()方法申请的堆内存在内存释放后不会归还给系统,所以下次申请内存时不需要出现pagefault异常。mmap()方法申请的动态内存在内存释放后会直接归还给系统,所以下次申请内存时会出现pagefault异常(增加内核态的CPU开销)。C语言中与内存申请相关的函数主要有calloc、malloc、realloc等。malloc:根据申请内存的大小,选择在堆或文件映射段分配连续的内存,但不会初始化内存。一般内存会通过memset函数进行初始化。calloc:类似于malloc,只是这块内存空间是自动初始化的,每个字节都置0。realloc:可以调整已经申请的内存大小。和malloc一样,新申请的内存也是未初始化的。4.Linux内存分配器上面说的是Linux进程通过C标准库中的内存分配函数malloc向系统申请内存,但是真正与内核交互之间其实还有一层,就是内存分配管理器(内存分配器)。常见的内存分配器包括:ptmalloc(Glibc)、tcmalloc(Google)、jemalloc(FreeBSD)。MySQL默认使用glibc的ptmalloc作为内存分配器。内存分配器采用内存池的管理方式,介于用户程序层和内核层之间。它响应用户的分配请求,向操作系统申请内存,然后返回给用户程序。为了保持高效的分配,分配器通常会预先向操作系统申请一块内存。当用户程序申请和释放内存时,分配器会对内存进行管理,使用一些算法策略来决定是否归还给操作系统。这样做最大的好处是可以避免用户程序频繁调用系统进行内存分配,使用户程序在内存使用上更加高效和快速。关于ptmalloc的内存分配原理,我个人不是很了解,这里就不多说了。有兴趣的同学可以去华亭的《glibc 内存管理 ptmalloc 源代码分析》【文末链接】。关于如何选择这三种内存分配器,网上的资料大多推荐放弃glibc原生的ptmalloc,使用jemalloc或tcmalloc作为默认分配器。因为ptmalloc的主要问题是内存浪费、内存碎片和锁带来的性能问题,而jemalloc和tcmalloc针对内存碎片和多线程处理做了更好的优化。目前jemalloc用于Firefox、FaceBook等,是MariaDB、Redis、Tengine推荐的默认内存分配器,而tcmalloc用于WebKit、Chrome等,一般来说推荐使用jemalloc作为内存MySQL下的allocator,可以有效解决内存碎片,提高整体性能。有兴趣的同学可以进一步测试,本文不再深入探讨。5.MySQL内存管理服务器版本:5.7.27-logMySQLCommunityServer(GPL)接下来我们看一下MySQL的内部内存管理。查阅了很多资料,发现自己原来的理解不是很正确。我习惯把MySQL的内存分为三类:Innodb_buffer_pool、Sharing、Thread内存,但是根据MySQL的架构来划分内存管理更合理。即Server层和InnoDB层(Engine层),这两块内存的管理方式不同。其中,Server层由mem_root管理,包括Sharing和Thead内存;而InnoDB层主要通过FreeList、LRUList、FLUList等多个链表来管理Innodb_buffer_pool。4.1.Innodb_buffer_poolMySQL5.7开始支持Innodb_buffer_pool的动态扩容,每个buffer_pool_instance由相同数量的chunk组成,每个chunk的内存大小为innodb_buffer_pool_chunk_size,所以Innodb_buffer_pool以innodb_buffer_pool_chunk_size和innodb_buffer_pool_chunk_size为基本单位进行动态增长--MySQL启动时初始化innodb_buffer_pool--MySQL关闭时释放innodb_buffer_pool内存>buf_pool_init>innobase_shutdown_for_mysql|>buf_pool_init_instance|>buf_pool_free||||>os_mem_free_large|||||>mmap|||||>munmap可以看到Innodb_buffer_pool内存初始化是通过mmap()直接向操作系统申请内存。每次申请的大小是innodb_buffer_pool_chunk_size,最终会申请到innodb_buffer_pool_size大小的文件映射段动态内存。这部分内存空间在初始化后只是虚拟内存,实际使用时会分配物理内存。根据前面Linux下的内存分配原则,mmap()申请的内存会在文件映射段分配,释放时直接归还给系统。仔细想想,Innodb_buffer_pool的内存分配确实是这样的。Innodb_buffer_pool在初始化的时候,会慢慢填充数据页和索引页等,然后保持Innodb_buffer_pool_size大小左右的物理内存占用。除非在线减少Innodb_buffer_pool或关闭MySQL,否则内存将通过munmap()释放。这里的内存释放是直接归还给操作系统的。Innodb_buffer_pool的内存主要通过FreeList、LRUList、FLUList、UnzipLRUList4个链表进行管理和分配。FreeList:缓存空闲页LRUList:缓存数据页FLUList:缓存所有脏页UnzipLRUList:缓存所有解压后的页面操作系统申请内存,不经过内存分配器。4.2.mem_rootmem_root结构广泛用于MySQLServer层管理内存,避免频繁调用内存操作,提高性能,统一分配和管理内存,防止内存泄漏:--initializemem_root--memoryapplication>init_alloc_rootalloc_root|>my_malloc--内存释放||>my_raw_mallocfree_alloc|||>mallocMySQL首先通过init_alloc_root函数初始化了一块很大的内存空间,实际上它最终是通过malloc函数向内存分配器申请内存空间,然后每次调用alloc_root函数在这块内存空间中分配内存的目的是将多个分散的malloc操作合并为一个大型malloc操作以提高性能。一开始以为MySQLServer层完全由一个mem_root结构来管理所有Server层内存,就像Innodb_buffer_pool一样。后来发现不是。不同的线程会产生不同的mem_roots来管理自己的内存,不同的mem_roots相互之间没有影响。与InnoDB层相比,server层的内存管理要复杂很多,更容易出现内存碎片。很多MySQL的内存问题都源于此。6.总结下面用一张图来简单总结一下MySQL的内存管理:最后说一下最初的问题。为什么MySQL经常占用比InnoDB_Buffer_Pool的配置多很多的物理内存不释放?实际上,大部分占用的内存都被内存分配器吃掉了。为了更高效的内存管理,内存分配器通常会占用大量内存而不释放;当然还有一部分原因是内存碎片,这会导致内存分配器无法重用之前申请的内存。但是,内存分配器并不是永远不会释放内存,而是需要达到一定的阈值,才会释放一部分内存给操作系统。这个的原理还得看源码~这次对内存原理的探索,其实只是想知道MySQL内存占用膨胀的原因,没想到一步步深入步骤,从MySQL内存管理到Linux进程内存管理,再到内存管理器,加深了我对内存的理解。附录:《glibc 内存管理 ptmalloc 源代码分析》https://paper.seebug.org/pape...MySQL&Linux内存管理:https://blog.csdn.net/gfgdsg/...http://mysql.taobao.org/month。..http://mysql.taobao.org/month...https://blog.csdn.net/n88Lpo/...https://www.coder.work/articl...https://zhuanlan.zhihu.com/p/...https://www.cnblogs.com/zengk...内存分配器:http://blog.onecodeall.com/in...https://www.cyningsun。com/07-...http://blog.onecodeall.com/in...http://zbo.space/2016/03/08/p...https://www.fordba.com/mysql_...https://blog.nowcoder.net/n/d...https://developer.aliyun.com/...http://www.freeoa.net/osuport...
