【MySQL】InnoDB内存结构-Buffer Pool

前言

对于MySQL数据库来说,最重要的存储引擎就是InnoDB。我们后端程序员几乎每天都会同MySQL打交道,在使用的同时也需要熟悉其底层原理,知其然知其所以然。其实IT技术犹如练武,底层原理犹如内功,只有内功扎实才能修炼上层的武功。为什么张无忌能在几个小时练成乾坤大挪移?因为他有九阳神功这门高深的内功。IT技术也是一样,只有了解各种中间件以及框架的原理,基础扎实才能飞得更高。
本篇文章将以最通俗易懂的语言,大白话的形式说明buffer pool的原理。同时也希望各位大佬给出建议,写这篇文章不仅仅是技术分享,更重要的是自我知识的巩固以及总结能力的提升。

内容

对于innoDB存储引擎来说,数据是存储在磁盘上,而执行引擎想要操作数据,必须先将磁盘的数据加载到内存中才能操作。那么innoDB是 如何将磁盘中的数据加载到内存中?是不是用多少就加载多少?如果内存不够用了怎么办? 先带着这些问题,认真往下看

1.InnoDB整体架构

以下是摘自mysql官网的一个图,先不用深究细节,了解一个大概即可
【MySQL】InnoDB内存结构-Buffer Pool_第1张图片
是不是看的很懵?没关系,我整理了一个中文版
【MySQL】InnoDB内存结构-Buffer Pool_第2张图片

2.需要了解的一些基本理论

  • 页:innoDB从磁盘读取数据到内存的最小单位称为页,默认大小为16k。
    在操作系统存储管理中也有页的概念,操作系统中页的大小一般是4k;对于目前的硬盘来说,扇区是最小的单位一般大小也是4k(扇区也可以理解成页)。
  • 区:在 InnoDB 存储引擎中,一个区会分配 64 个连续的页。
    任何情况下每个区大小都为1MB
  • 磁盘访问时间:主要包括寻道时间、旋转延迟、数据传输时间
    (1)寻道时间: 磁头 从开始移动到数据所在 磁道 所需要的时间, 寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般硬盘的平均寻道时间在7.5~ 14ms
    (2)旋转延迟: 盘片 旋转将请求数据所在 扇区 移至读写 磁头 下方所需要的时间, 旋转延迟取决于 磁盘转速、当前服务器已经可达15000rpm。
    (3)数据传输时间:完成传输所请求的数据所需要的时间。
  • 顺序io和随机io
    (1)顺序io:访问磁盘的地址是连续的。比如我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就是顺序IO。
    (2)随机io:访问磁盘时间上是连续的,但是地址不连续。比如MySQL的一个事务,涉及到多个sql,而每个sql操作的数据分布在不同磁道的不同扇区中,假设直接刷入磁盘,这里就是随机IO
    【MySQL】InnoDB内存结构-Buffer Pool_第3张图片

了解这些前置知识后,下面可以进入今天的主角 - buffer pool

3.buffer pool(缓冲池)

3.1 什么是buffer pool?

buffer pool是MySQL服务在启动的时候向操作系统申请的一片连续地址的内存空间,其本质就是一片内存,默认大小是 128M,可以在启动服务的时候,通过 innodb_buffer_pool 这个参数设置buffer pool的大小,单位是字节(B),最小值是5MB,如果设置的小于5MB时会自动设置成5MB

3.2 buffer pool的内部组成

buffer pool被划分为某干个数据页,其数据页大小和表空间使用的页大小一致,为了描述方便下文都称buffer pool中的数据页为 缓冲页。为了更好的管理buffer pool中的缓冲页,innoDB为每个缓冲页都创建了一个控制信息。这些控制信息主要包括该 缓冲页 所属的表空间编号、页号、缓冲页在buffer pool中的地址、链表节点信息等,除这些内容外,控制信息还包括一些内容,这里就不再一一列举。
每个缓冲页对应的控制信息所占内存空间是相同的,这个内存空间称为 控制块。缓冲页和控制块是一一对应的,其中控制块在buffer pool前面,而缓冲页在buffer后面,整个buffer pool其内存结构图如下
【MySQL】InnoDB内存结构-Buffer Pool_第4张图片

3.3 free链表、flush链表和LRU链表

  • free链表
    对于查询操作,先看buffer pool中有没有想要的数据,如果有就直接使用;如果没有,需要将磁盘中的该数据加载到buffer pool中。那么该放到buffer pool中的哪个缓冲页呢?怎么区分哪些页是空闲,哪些页已经被使用了?这里就引出了free链表。我们将所有空闲的缓冲页对应的控制块信息作为一个节点放到一个链表中,这就是free链表
    当刚初始化完的buffer pool时所有的页都是空闲页,这时所有控制块信息都会放到free链表,假设buffer pool缓存页的数量是n,那么free链表的长度也是n,如下图所示。
    【MySQL】InnoDB内存结构-Buffer Pool_第5张图片
    有了free链表之后,当需要加载磁盘中的页到buffer pool中时,就去free链表中取一个空闲页所对应的控制块信息,根据控制块信息中的表空间号、页号找到buffer pool里对应的缓冲页,再将数据加载到该缓冲页中,随后删掉free链表该控制块信息对应的节点
  • 缓冲页的Hash处理
    我们已经知道,当操作某一页的数据时,首先要看buffer pool中有没有,如果有直接使用即可;如果没有的话就需要将磁盘中的该页加载到buffer pool中。那么现在的问题是,我们怎么知道该页在不在buffer pool里?难道是要遍历整个buffer pool吗?这样的话肯定是有问题的,那么就需要快速的查找需要用到的数据页。这里就可以对缓冲页进行Hash处理,用表空间号、页号做为Key,缓冲页的控制块就是value维护一个Hash表。当操作某一页数据时,根据表空间号、页号做为Key去查找有没有对应的缓冲信息,如果有直接使用即可,如果没有就需要去free 链表中取一个空闲的缓冲页控制快信息,随后将磁盘中的数据加载到该缓冲页位置
  • flush链表
    如果我们修改了buffer pool中缓冲页的数据,那么该页和磁盘就不一致了,这样的页就称为脏页。每次修改缓冲页中的数据,就立即刷新到磁盘,这样频繁磁盘io,性能会急剧下降,所以每次修改完数据不是立马刷新到磁盘中,那么我们怎么知道哪些缓冲页被修改了呢?这里就引出了fush链表。所有被修改过的缓冲页,会将其对应的控制信息作为一个节点放入一个链表中,这样的链表就是fush链表
  • LRU链表
    对于查询和更新操作,都离不开buffer pool,那么buffer pool空间满了该怎么办?这里又需要维护一个链表,用于淘汰最近最少使用的缓冲页。那么这里有了LRU链表是不是就根据LRU算法来淘汰缓冲页就可以了呢?带着这个问题继续往下看

3.4 LRU算法说明

LRU不清楚的童鞋,请看这篇文章 https://blog.csdn.net/belongtocode/article/details/10298968

3.4.1 传统LRU算法

需要操作的页如果在buffer pool中,那么每次访问的时候直接将其对应的控制块异动到LRU链表的表头,如果操作的页不在buffer pool中,则先从磁盘加载数据到该缓冲页中,其对应的控制信息直接放到LRU链表的头部,如果没有空闲页,则淘汰LRU链表尾部对应的缓冲页。传统LRU链表如下图所示:
【MySQL】InnoDB内存结构-Buffer Pool_第6张图片
由于innoDB存储引擎每次都是按页读,即便每次只读一行记录,也会将该行所在的页全部加载到buffer pool中,而数据访问遵循“集中读写”原则。使用一些数据,大概率会使用附近的数据这就是 局部性原理【因此,将下次可能用到的页加载到Buffer Pool,顺序访问N个页后触发预读】
(1)线性预读:如果顺序访问同一个区的页,当大于等于innodb_read_ahead_threshold变量指定的值时,会触发线性预读,即会将下一个区的所有页加载到buffer中,Mysql5.4版本默认开启,默认值是56
【MySQL】InnoDB内存结构-Buffer Pool_第7张图片
(2)随机预读 :如果访问某一个区的连续页超过13,那么会触发随机预读,即将该区的所有页都加载到buffer pool中,MySQL 5.5之后的版本随机预读默认是关闭的,通过innodb_random_read_ahead变量设置为ON表示开启
预读是后台线程异步完成的,由参数 innodb_read_io_threads 控制数量。默认 4 个

3.4.2 传统LRU算法可能存在的问题

由于InnoDB的预读机制,在有些情况下可能回出现预读的页并没有被使用;如果有非常多的使用频率低的页同时加载buffer pool中,这样使用频率高的页会被淘汰掉。基于这两个原因,InnoDB将传统LRU算法做了改进。

3.4.3 改进后的LRU算法

将LUR分为两个区域,一部分使用频率高的在链表的头部称为“young区”,一部分使用频率低的在链表的后部分称为“old区”。主要示意图如下:
【MySQL】InnoDB内存结构-Buffer Pool_第8张图片
所有新加载的页都先放到old区的Head,无论是预读的还是普通的读操作;新加入到old的数据,如果没有后续访问,会被逐渐从old区域淘汰,不会影响young区使用频率比较高的缓冲页。即使是使用了分区,那么还会出现一些问题,对于LRU链表的优化措施,还非常多的内容,由于篇幅限制,这里不一一赘述。那么一些列举了两个比较重要的优化点

  1. 如果再次被访问,是不是就直接移动到young区的头部呢?对于一个全表扫描的查询,它会扫描到主键索引树的所有叶子节点,这样的话,所有叶子节点会被加载到buffer pool中,而加载到buffer pool中又会马上被访问,如果马上移动到young区的头部,那么后续又不会使用,这样造成大量的高频率缓冲页被淘汰。 基于这个问题,InnoDB设计者就规定,在old区的缓冲页第一次访问时,在其控制块里记下第一次访问时间,如果第二次访问,会判断与第一次访问的时间间隔是否超过一个特定值,如果超过了,那么就将该页的控制信息移动到LUR链表的young区,如果没有,那么该页依然是在old区,不会移动。 该时间间隔的值通过系统变量 innodb_old_blocks_time来设置
  2. 对于young区每次访问时,都移动到链表的头部,这样频繁移动节点也是一种损耗,那么就规定如果是访问young区1/4的后面时,才会移动节点到头部。 这个1/4是young区节点个数的比例,并不是某个特定节点之前的部分,1/4区域会随着节点数量变化而变化。例如young区的总节点假设是40个(意味着buffer pool中40个缓冲页是热数据),那么你访问前10个节点时(实际上是访问该节点控制信息对应的buffer pool中的缓冲页,后续为描述方便,访问某个节点也表示访问buffer pool中相应的缓存页),这样不会移动节点到链表的头部,只有访问后30个节点是才会移动节点到链表的头部

3.5 多buffer实例

一个buffer pool在多线程访问的时候,各个链表都会加锁处理,这样一来,多线程访问时,性能就会降低,所以buffer pool是有多个实例的,每个实例都有其对应的链表管理。可以通过innodb_buffer_pool_instances参数来设置实例的个数。每个buffer pool实例的大小计算公式:innodb_buffer_pool_size / innodb_buffer_pool_instances
【MySQL】InnoDB内存结构-Buffer Pool_第9张图片
如果innodb_buffer_pool_size 小于1G时,设置innodb_buffer_pool_instances是无效的,都会是1
【MySQL】InnoDB内存结构-Buffer Pool_第10张图片

3.6 buffer pool整体架构图

【MySQL】InnoDB内存结构-Buffer Pool_第11张图片

3.7 运行中修改buffer pool大小

如果修改innobd中buffer pool的大小,就需要重新申请一片大的内存块,随后将老的buffer pool中的内容复制到新的中,这样的开销是非常大的,那么innoBD中肯定不是这么做的。
MySQL 5.7.5之前,是不允许在运行时调整buffer pool大小的,只能在服务器启动之前,通过innodb_buffer_pool_size大小来调整
MySQL 5.7.5及以后的版本中,MySQL以chunk为单位来申请内存空间,即buffer pool由多个buffer pool实例组成,而一个实例由多个chunk组成,一个chunk就代表一片连续的内存空间
在运行的时候调整buffer pool大小,只需要以chunk为单位来增加或减少buffer pool大小,这样就不用每次复制了。chunk的大小是在MySQL服务启动时,通过innodb_buffer_pool_chunk_size来指定,默认大小是128M
innodb_buffer_pool_chunk_size的大小只能在服务器启动的时候指定,不能在运行时指定。

3.8 Adaptive Hash Index :自适应Hash索引

属于buffer pool中的一片内存。当InnoDB观察到某个索引的值频繁被使用,那么会创建自适应hash索引,可以提高查询速度,可以通过参数innodb_adaptive_hash_index来禁用或启动此特性,默认为开启

3.9 redo log buffer

redo log buffer也属于buffer pool的一部分,redo log也不是每次写入都刷新到磁盘,也有一个专门的缓冲区redo log buffer(默认16M,通过innodb_log_buffer_size来设置)来记录,等待时机再刷新到磁盘。关于redo log后面会专门用一篇博客来讲解,这里先简单描述下。


你可能感兴趣的:(MySQL,mysql,数据库,java)