共计 7034 个字符,预计需要花费 18 分钟才能阅读完成。
作者: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(以后沉闷连接数) * (Thread memory)
其中 InnoDB_Buffer_Pool 是 MySQL 中内存占用中最大的一块,为 常驻内存,也就说是不会开释,除非 MySQL 过程退出。
而另外一块比拟吃内存的就是线程缓存。例如常见的 join_buffer、sort_buffer、read_buffer 等,通常与连接数成正比。即连接数越高,并发越高,线程缓存占用总量就越高,然而这类缓存往往会 随着连贯敞开而开释,并非常驻内存。
二、内存高水位景象
- CentOS Linux release 7.3.1611 (Core)
- Server version: 5.7.27-log MySQL Community Server (GPL)
咱们先做个小测试来察看下 MySQL 的内存占用变动,首先敞开 performance_schema
与 innodb_buffer_pool_load_at_startup
,避免造成缓存烦扰。而后将 innodb_buffer_pool
设置 100M,实践上 innodb_buffer_pool
的最大仅会占用 100M,能够通过 show engine innodb status \G
进行查看。
通过 sysbench 创立一张 100W 的测试表,重启 MySQL,察看目前 MySQL 总共占用了 55536KB 物理内存,其中 innodb_buffer_pool
中占用了 432*16K=6912KB 内存,那么我就算 MySQL 默认启动后会占用 50MB 物理内存。
UID PID minflt/s majflt/s VSZ RSS %MEM Command
997 11980 0.00 0.00 1240908 55536 0.69 mysqld
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 107380736
Dictionary memory allocated 116177
Buffer pool size 6400
Free buffers 5960
Database pages 432
而后咱们开始通过 sysbench 进行 select 压测,从 4 线程开始压测,4-8-16-32-64 逐渐加大线程数,每次压测 2min,最初察看 MySQL 总物理内存占用大小变动状况。
从上图能够看到,4 线程刚开始压测的时候,内存占用飙升。次要是因为 innodb_buffer_pool
中大量涌入数据页造成。而后加大线程数时,因为 innodb_buffer_pool
曾经饱和达到 100M 下限,所以起伏不是很高。这块内存回升的起因次要是因为 all_thread_buffers 增大造成,最初 64 线程压测完,MySQL 总物理内存占用稳固在 194MB 左右,并且始终维持着,并没有开释还给操作系统。
压测完结后,再次查看 innodb_buffer_pool
,能够看到 Free buffers
为空,100M 曾经齐全占满。
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 107380736
Dictionary memory allocated 120760
Buffer pool size 6400
Free buffers 0
Database pages 5897
Old database pages 2156
减去 innodb_buffer_pool
的 100M,以及 MySQL 刚启动占用的 50M,还有 40MB+ 的内存占用,次要为 all_thread_buffers
。
通过这个测试能够看到,之前所了解的 线程缓存随着连贯敞开而开释 其实不太对。MySQL 并不会 把这部分缓存还给操作系统,而只是在 MySQL 外部开释,而后重复使用。
我把这个景象称为 内存高水位景象,因为与 Oracle 中高水位线概念十分相似。同样的,MySQL 中当 ibd 文件被撑大后,即便 delete 全表,也不会被动去开释磁盘空间返还给操作系统,而是重复使用已开释的磁盘空间,景象也十分统一。
PS:这里 sysbench 压测是走主键索引的单表 where 查问,并不会申请 sort_buffer,join_buffer 等。所以单个会话申请的线程缓存比拟少。因而最初总的线程缓存占用不是十分高,如果是压简单 SQL,内存占用应该会比拟高。
三、Linux 过程内存调配
为了搞清楚 MySQL 经常出现 内存高水位景象 的起因,先去查阅学习了 Linux 下相干的内存调用原理,具体内容总结如下:
上图是 32 位用户虚拟空间内存 的构造简图,由上到下别离是:
- 只读段:包含代码和常量等;
- 数据段:包含全局变量等;
- 堆:包含动态分配的内存,从低地址开始向上增长;
- 文件映射段:包含动静库、共享内存等,从高地址开始向下增长;
- 栈:包含局部变量和函数调用的上下文等。
其中 堆 与 文件映射段 是咱们探讨的重点,它们的内存都是动态分配的。比如说,应用 C 规范库的 malloc()
或者 mmap()
,就能够别离在 堆 和 文件映射段 动静分配内存。
那么这两者有什么区别呢?
malloc()
是 C 规范库提供的内存调配函数,对应到零碎调用上,有两种实现形式,即 brk()
和 mmap()
。
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。
4.1. 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 为根本单位进行动静增大和放大。
--MySQL 启动时初始化 innodb_buffer_pool --MySQL 敞开时开释 innodb_buffer_pool 内存
>buf_pool_init >innobase_shutdown_for_mysql
| >buf_pool_init_instance | >buf_pool_free
| | >buf_chunk_init | | >buf_pool_free_instance
| | | >allocate_large | | | >deallocate_large
| | | | >os_mem_alloc_large | | | | >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 的内存次要是通过 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()
形式间接向操作系统申请内存调配,而不须要通过内存分配器。
4.2. mem_root
MySQL Server 层中宽泛应用 mem_root
构造体来治理内存,防止频繁调用内存操作,晋升性能,对立的调配和治理内存也能够避免产生内存透露:
-- 初始化 mem_root -- 内存申请
>init_alloc_root alloc_root
| >my_malloc -- 内存开释
| | >my_raw_malloc free_alloc
| | | >malloc
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 过程内存治理,再到内存管理器,加深了集体对于内存的了解。
附录:
《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…