作者: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_Pool
all_thread_buffers:max_threads(当前活跃连接数)*(Threadmemory)
其中InnoDB_Buffer_Pool是MySQL中内存占用中最大的一块,为常驻内存,也就说是不会释放,除非MySQL进程退出。
而另外一块比较吃内存的就是线程缓存。例如常见的join_buffer、sort_buffer、read_buffer等,通常与连接数成正比。即连接数越高,并发越高,线程缓存占用总量就越高,但是这类缓存往往会随着连接关闭而释放,并非常驻内存。
二、内存高水位现象
CentOSLinuxrelease7.3.(Core)Serverversion:5.7.27-logMySQLCommunityServer(GPL)我们先做个小测试来观察下MySQL的内存占用变化,首先关闭performance_schema与innodb_buffer_pool_load_at_startup,防止造成缓存干扰。然后将innodb_buffer_pool设置M,理论上innodb_buffer_pool的最大仅会占用M,可以通过showengineinnodbstatus\G进行查看。
通过sysbench创建一张W的测试表,重启MySQL,观察目前MySQL总共占用了KB物理内存,其中innodb_buffer_pool中占用了*16K=KB内存,那么我就算MySQL默认启动后会占用50MB物理内存。
UIDPIDminflt/smajflt/sVSZRSS%MEMCommand..0012409080.69mysqld----------------------BUFFERPOOLANDMEMORY----------------------TotallargememoryallocatedDictionarymemoryallocatedBufferpoolsizeFreebuffersDatabasepages
然后我们开始通过sysbench进行select压测,从4线程开始压测,4-8-16-32-64逐步加大线程数,每次压测2min,最后观察MySQL总物理内存占用大小变化情况。
从上图可以看到,4线程刚开始压测的时候,内存占用飙升。主要是由于innodb_buffer_pool中大量涌入数据页造成。而后加大线程数时,由于innodb_buffer_pool已经饱和达到M上限,所以起伏不是很高。这块内存上升的原因主要是由于all_thread_buffers增大造成,最后64线程压测完,MySQL总物理内存占用稳定在MB左右,并且一直维持着,并没有释放还给操作系统。
压测结束后,再次查看innodb_buffer_pool,可以看到Freebuffers为空,M已经完全占满。
----------------------BUFFERPOOLANDMEMORY----------------------TotallargememoryallocatedDictionarymemoryallocatedBufferpoolsizeFreebuffers0DatabasepagesOlddatabasepages
减去innodb_buffer_pool的M,以及MySQL刚启动占用的50M,还有40MB+的内存占用,主要为all_thread_buffers。
通过这个测试可以看到,之前所理解的线程缓存随着连接关闭而释放其实不太对。MySQL并不会把这部分缓存还给操作系统,而只是在MySQL内部释放,然后重复使用。
我把这个现象称为内存高水位现象,因为与Oracle中高水位线概念非常类似。同样的,MySQL中当ibd文件被后,即使delete全表,也不会主动去释放磁盘空间返还给操作系统,而是重复使用已释放的磁盘空间,现象也非常一致。
PS:这里sysbench压测是走主键索引的单表where查询,并不会申请sort_buffer,join_buffer等。所以单个会话申请的线程缓存比较少。因此最后总的线程缓存占用不是非常高,如果是压复杂SQL,内存占用应该会比较高。
三、Linux进程内存分配
为了搞清楚MySQL经常出现内存高水位现象的原因,先去查阅学习了Linux下相关的内存调用原理,具体内容总结如下:
上图是32位用户虚拟空间内存的结构简图,由上到下分别是:
1.只读段:包括代码和常量等;
2.数据段:包括全局变量等;
3.堆:包括动态分配的内存,从低地址开始向上增长;
4.文件映射段:包括动态库、共享内存等,从高地址开始向下增长;
5.栈:包括局部变量和函数调用的上下文等。
其中堆与文件映射段是我们讨论的重点,它们的内存都是动态分配的。比如说,使用C标准库的malloc()或者mmap(),就可以分别在堆和文件映射段动态分配内存。
那么这两者有什么区别呢?
malloc()是C标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即brk()和mmap()。
1.brk方式
对于小块内存(K),C标准库使用brk()来分配。也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,重复使用。优缺点:brk()方式可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,所以在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。2.mmap匿名映射方式
对于大块内存(K),C标准库使用mmap()来分配,也就是在文件映射段找一块空闲内存分配出去。mmap()方式分配的内存,会在释放时直接归还系统,所以每次mmap都会发生缺页异常。优缺点:mmap()方式可以将内存及时返回给系统,避免OOM。但是工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc只对大块内存使用mmap的原因。所谓的缺页异常是指进程申请内存后,只分配了虚拟内存。这些所申请的虚拟内存,只有在首次访问时才会分配真正的物理内存,也就是通过缺页异常进入内核中,再由内核来分配物理内存(本质就是建立虚拟内存与物理内存的地址映射)。
brk()方式申请的堆内存由于释放内存后并不会归还给系统,所以下次申请内存时,并不需要发生缺页异常。mmap()方式申请的动态内存会在释放内存后直接归还系统,所以下次申请内存时,会发生缺页异常(增加内核态CPU开销)。C语言跟内存申请相关的函数主要有calloc,malloc,realloc等。
malloc:根据内存申请大小,选择在堆或文件映射段中分配连续内存,但是不会初始化内存,一般会再通过memset函数来初始化这块内存。calloc:与malloc类似,只不过会自动初始化这块内存空间,每个字节置为0。realloc:可以对已申请的内存进行大小调整,同malloc一样新申请的内存也是未初始化的。
四、Linux内存分配器
上述所说的是Linux进程通过C标准库中的内存分配函数malloc向系统申请内存,但是到真正与内核交互之间,其实还隔了一层,即内存分配管理器(memoryallocator)。常见的内存分配器包括: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等。
总体来说,MySQL下更推荐使用jemalloc作为内存分配器,可以有效解决内存碎片与提高整体性能,有兴趣的同学可以进一步测试下,本篇就不深入探究了。
五、MySQL内存管理
接着我们再来看下MySQL内部是管理内存的,查阅大量资料后,发现我原先的理解不是很正确,之前我习惯性的把MySQL的内存划分为Innodb_buffer_pool、Sharing、Threadmemory等三大类,但实际应该以MySQL的架构来划分内存管理比较合理。即Server层与InnoDB层(Engine层),而这两块内存是由不同的方式进行管理的。
其中Server层是由mem_root来进行内存管理,包括Sharing与Theadmemory;而InnoDB层则主要由FreeList、LRUList、FLUList等多个链表来统一管理Innodb_buffer_pool。
4.1.Innodb_buffer_pool
MySQL5.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内存初始化是通过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、UnzipLRUList等4个链表来进行管理分配。
FreeList:缓存空闲页LRUList:缓存数据页FLUList:缓存所有UnzipLRUList:缓存所有解压页PS:源码全局遍历下来,只有innodb_buffer_pool与onlineddl的内存管理是采用mmap()方式直接向操作系统申请内存分配,而不需要经过内存分配器。
4.2.mem_root
MySQLServer层中广泛使用mem_root结构体来管理内存,避免频繁调用内存操作,提升性能,统一的分配和管理内存也可以防止发生内存泄漏:
MySQL首先通过init_alloc_root函数初始化一块较大的内存空间,实际上最终是通过malloc函数向内存分配器申请内存空间,然后每次再调用alloc_root函数在这块内存空间中分配出内存进行使用,其目的就是将多次零散的malloc操作合并成一次大的malloc操作,以提升性能。
刚开始我以为MySQLServer层是完全由一个mem_root结构体来管理所有的Server层内存,就像Innodb_buffer_pool一样。后来发现并不是,不同的线程会产生不同的mem_root来管理各自的内存,不同的mem_root之间互相没有影响。
Server层的内存管理相较于InnoDB层来说复杂的多,也更容易产生内存碎片,很多MySQL内存问题都出自于此。
六、总结
下面简单用一张图来总结下MySQL的内存管理:
最后再来捋一下最初的疑问,为啥经常出现MySQL实际占用物理内存比InnoDB_Buffer_Pool的配置高很多而且不释放的现象?
其实多占用的内存大多都是被内存分配器吃掉了。为了更高效的内存管理,内存分配器通常都会占着很多内存不释放;当然还有另一部分原因是内存碎片,会导致内存分配器无法重新利用之前所申请的内存。
不过内存分配器并非永远不释放内存,而是需要达到某个阈值,它才会释放一部分内存给操作系统,个中原理则需要大家去源码中找了~
此次内存原理探索,其实一开始只是想知道MySQL内存占用虚高的原因,没想到一步一步,越挖越深,从MySQL内存管理到Linux进程内存管理,再到内存管理器,加深了个人对于内存的理解。