技术分享 | MySQL 内存管理初探
作者:xuty
一、背景
CentOS Linux release 7.3.1611 (Core)
Server version: 5.7.27-log MySQL Community Server (GPL)
UID PID minflt/s majflt/s VSZ RSS %MEM Command997 11980 0.00 0.00 1240908 55536 0.69 mysqld----------------------BUFFER POOL AND MEMORY----------------------Total large memory allocated 107380736Dictionary memory allocated 116177Buffer pool size 6400Free buffers 5960Database pages 432
----------------------BUFFER POOL AND MEMORY----------------------Total large memory allocated 107380736Dictionary memory allocated 120760Buffer pool size 6400Free buffers 0Database pages 5897Old database pages 2156
PS:这里 sysbench 压测是走主键索引的单表 where 查询,并不会申请 sort_buffer,join_buffer 等。所以单个会话申请的线程缓存比较少。因此最后总的线程缓存占用不是非常高,如果是压复杂 SQL,内存占用应该会比较高。
三、Linux 进程内存分配
那么这两者有什么区别呢?
1. brk 方式
对于小块内存(<128K),C 标准库使用 brk() 来分配。也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,重复使用。
优缺点:brk() 方式可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,所以在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
2. mmap 匿名映射方式
对于大块内存(>128K),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 向系统申请内存,但是到真正与内核交互之间,其实还隔了一层,即内存分配管理器(memory allocator)。常见的内存分配器包括: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 内存管理
Server version: 5.7.27-log MySQL Community Server (GPL)
接着我们再来看下 MySQL 内部是管理内存的,查阅大量资料后,发现我原先的理解不是很正确,之前我习惯性的把 MySQL 的内存划分为 Innodb_buffer_pool、Sharing 、Thread memory 等三大类,但实际应该以 MySQL 的架构来划分内存管理比较合理。即 Server 层与 InnoDB 层(Engine 层),而这两块内存是由不同的方式进行管理的。
其中 Server 层是由 mem_root 来进行内存管理,包括 Sharing 与 Thead memory;而 InnoDB 层则主要由 Free List、LRU List、FLU List 等多个链表来统一管理 Innodb_buffer_pool。
MySQL 5.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 的内存主要是通过 Free List、LRU List、FLU List、Unzip LRU List 等 4 个链表来进行管理分配。
Free List:缓存空闲页
LRU List:缓存数据页
FLU List:缓存所有脏页
Unzip LRU List:缓存所有解压页
PS:源码全局遍历下来,只有 innodb_buffer_pool 与 online ddl 的内存管理是采用 mmap() 方式直接向操作系统申请内存分配,而不需要经过内存分配器。
MySQL Server 层中广泛使用 mem_root 结构体来管理内存,避免频繁调用内存操作,提升性能,统一的分配和管理内存也可以防止发生内存泄漏:
MySQL 首先通过 init_alloc_root 函数初始化一块较大的内存空间,实际上最终是通过 malloc 函数向内存分配器申请内存空间,然后每次再调用 alloc_root 函数在这块内存空间中分配出内存进行使用,其目的就是将多次零散的 malloc 操作合并成一次大的 malloc 操作,以提升性能。
刚开始我以为 MySQL Server 层是完全由一个 mem_root 结构体来管理所有的 Server 层内存,就像 Innodb_buffer_pool 一样。后来发现并不是,不同的线程会产生不同的mem_root来管理各自的内存,不同的 mem_root 之间互相没有影响。
Server 层的内存管理相较于 InnoDB 层来说复杂的多,也更容易产生内存碎片,很多 MySQL 内存问题都出自于此。
六、总结
下面简单用一张图来总结下 MySQL 的内存管理:
最后再来捋一下最初的疑问,为啥经常出现 MySQL 实际占用物理内存比 InnoDB_Buffer_Pool 的配置高很多而且不释放的现象?
其实多占用的内存大多都是被内存分配器吃掉了。为了更高效的内存管理,内存分配器通常都会占着很多内存不释放;当然还有另一部分原因是内存碎片,会导致内存分配器无法重新利用之前所申请的内存。
不过内存分配器并非永远不释放内存,而是需要达到某个阈值,它才会释放一部分内存给操作系统,个中原理则需要大家去源码中找了~
此次内存原理探索,其实一开始只是想知道 MySQL 内存占用虚高的原因,没想到一步一步,越挖越深,从 MySQL 内存管理到 Linux 进程内存管理,再到内存管理器,加深了个人对于内存的理解。
MySQL&Linux 内存管理:
内存分配器:
技术分享 | 改写 mysqldump 解决 DEFINER 问题
故障分析 | MySQL 优化案例 - select count(*)
社区近期动态
本文分享自微信公众号 - 爱可生开源社区(ActiontechOSS)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

