邢台做网站的价格究竟多少钱?,wordpress的首页,百度明星人气排行榜,蚌埠市网站建设6.2.InnoDB 的表空间
表空间是一个抽象的概念#xff0c;对于系统表空间来说#xff0c;对应着文件系统中一个或多个实际文件#xff1b;对于每个独立表空间来说#xff0c;对应着文件系统中一个名为表名.ibd 的实际文件。大家可以把表空间想象成被切分为许许多多个页的池…6.2.InnoDB 的表空间
表空间是一个抽象的概念对于系统表空间来说对应着文件系统中一个或多个实际文件对于每个独立表空间来说对应着文件系统中一个名为表名.ibd 的实际文件。大家可以把表空间想象成被切分为许许多多个页的池子当我们想为某个表插入一条记录的时候就从池子中捞出一个对应的页来把数据写进去。
再回忆一次InnoDB 是以页为单位管理存储空间的我们的聚簇索引也就是完整的表数据和其他的二级索引都是以 B树的形式保存到表空间的而B树的节点就是数据页。
任何类型的页都有 File Header 这个部分File Header 中专门的地方 FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID保存页属于哪个表空间同时表空间中的每一个页都对应着一个页FIL_PAGE_OFFSET这个页号由 4 个字节组成也就是 32 个比特位所以一个表空间最多可以拥有 2³²个页如果按照页的默认大小 16KB 来算一个表空间最多支持 64TB 的数据。
6.2.1. 独立表空间结构
6.2.1.1. 区extent
表空间中的页可以达到 2³²个页实在是太多了为了更好的管理这些页面InnoDB 中还有一个区英文名extent的概念。对于 16KB 的页来说连续的64 个页就是一个区也就是说一个区默认占用 1MB 空间大小。 不论是系统表空间还是独立表空间都可以看成是由若干个区组成的每256 个区又被划分成一个组。
第一个组最开始的 3 个页面的类型是固定的用来登记整个表空间的一些整体属性以及本组所有的区被称为 FSP_HDR也就是 extent 0 ~ extent 255 这 256个区整个表空间只有一个 FSP_HDR。 其余各组最开始的 2 个页面的类型是固定的一个 XDES 类型用来登记本组 256 个区的属性FSP_HDR 类型的页面其实和 XDES 类型的页面的作用类似只不过 FSP_HDR 类型的页面还会额外存储一些表空间的属性。 引入区的主要目的是什么我们每向表中插入一条记录本质上就是向该表的聚簇索引以及所有二级索引代表的 B树的节点中插入数据。而 B树的每一层中的页都会形成一个双向链表如果是以页为单位来分配存储空间的话双向链表相邻的两个页之间的物理位置可能离得非常远。 我们介绍 B树索引的适用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录然后沿着双向链表一直扫描就可以了而如果链表中相邻的两个页物理位置离得非常远就是所谓的随机 I/O。再一次强调磁盘的速度和内存的速度差了好几个数量级随机 I/O 是非常慢的所以我们应该尽量让链表中相邻的页的物理位置也相邻这样进行范围查询的时候才可以使用所谓的顺序 I/O。
一个区就是在物理位置上连续的 64 个页。在表中数据量大的时候为某个索引分配空间的时候就不再按照页为单位分配了而是按照区为单位分配甚至在表中的数据十分非常特别多的时候可以一次性分配多个连续的区从性能角度看可以消除很多的随机 I/O。
6.2.1.2. 段segment
我们提到的范围查询其实是对 B树叶子节点中的记录进行顺序扫描而如果不区分叶子节点和非叶子节点统统把节点代表的页面放到申请到的区中的话进行范围扫描的效果就大打折扣了。所以 InnoDB 对 B树的叶子节点和非叶子节点进行了区别对待也就是说叶子节点有自己独有的区非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段segment存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成 2 个段一个叶子节点段一个非叶子节点段。
段其实不对应表空间中某一个连续的物理区域而是一个逻辑上的概念。 6.2.2. 系统表空间
6.2.2.1. 整体结构
系统表空间的结构和独立表空间基本类似只不过由于整个 MySQL 进程只有一个系统表空间在系统表空间中会额外记录一些有关整个系统信息的页面所以会比独立表空间多出一些记录这些信息的页面相当于是表空间之首所以它的表空间 IDSpace ID是 0。
系统表空间和独立表空间的前三个页面的类型是一致的只是页号为 37的页面是系统表空间特有的。分别包括
页号 页面类型 英文描述 描述 3 SYS Insert Buffer Header 存储 Insert Buffer 的头部信息 4 INDEX Insert Buffer Root 存储 Insert Buffer 的根页面 5 TRX_SYS Transction System 事务系统的相关信息 6 SYS First Rollback Segment 第一个回滚段的页面 7 SYS Data Dictionary Header 数据字典头部信息 系统表空间的 extent 1 和 extent 2 这两个区也就是页号从 64~191 这 128 个页面被称为 Doublewrite buffer也就是双写缓冲区。 双写缓冲区/双写机制 双写缓冲区/双写机制是 InnoDB 的三大特性之一还有两个是 Buffer Pool、自适应 Hash 索引。
它是一种特殊文件 flush 技术带给 InnoDB 存储引擎的是数据页的可靠性。 它的作用是在把页写到数据文件之前InnoDB 先把它们写到一个叫 doublewrite buffer双写缓冲区的连续区域内在写 doublewrite buffer 完成后InnoDB 才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃InnoDB 在稍后的恢复过程中在 doublewrite buffer 中找到完好的 page 副本用于恢复。
所以虽然叫双写缓冲区但是这个缓冲区不仅在内存中有更多的是属于MySQL 的系统表空间属于磁盘文件的一部分。那为什么要引入一个双写机制呢 InnoDB 的页大小一般是 16KB其数据校验也是针对这 16KB 来计算的将数据写入到磁盘是以页为单位进行操作的。而操作系统写文件是以 4KB 作为单位的那么每写一个 InnoDB 的页到磁盘上操作系统需要写 4 个块。
而计算机硬件和操作系统在极端情况下比如断电往往并不能保证这一操作的原子性16K 的数据写入 4K 时发生了系统断电或系统崩溃只有一部分写是成功的这种情况下会产生 partial page write部分页写入问题。这时页数据出现不一样的情形从而形成一个断裂的页使数据产生混乱。在InnoDB 存储引擎未使用 doublewrite 技术前曾经出现过因为部分写失效而导致数据丢失的情况。
doublewrite buffer 是 InnoDB 在表空间上的 128 个页2 个区extend1 和extend2大小是 2MB。为了解决部分页写入问题当 MySQL 将脏数据 flush 到数据文件的时候, 先使用 memcopy 将脏数据复制到内存中的一个区域也是2M之后通过这个内存区域再分 2 次每次写入 1MB 到系统表空间然后马上调用 fsync 函数同步到磁盘上。在这个过程中是顺序写开销并不大在完成 doublewrite 写入后再将数据写入各数据文件文件这时是离散写入。 所以在正常的情况下, MySQL 写数据页时会写两遍到磁盘上第一遍是写到 doublewrite buffer第二遍是写到真正的数据文件中。如果发生了极端情况断电InnoDB 再次启动后发现了一个页数据已经损坏那么此时就可以从 doublewrite buffer 中进行数据恢复了。 前面说过位于系统表空间上的 doublewrite buffer 实际上也是一个文件写系统表空间会导致系统有更多的 fsync 操作, 而硬盘的 fsync 性能因素会降低 MySQL 的整体性能。不过在存储上doublewrite 是在一个连续的存储空间, 所以 硬盘在写数据的时候是顺序写而不是随机写这样性能影响不大相比不双写 降低了大概 5-10%左右。
所以在一些情况下可以关闭 doublewrite 以获取更高的性能。比如在 slave上可以关闭因为即使出现了 partial page write 问题数据还是可以从中继日志中恢复。比如某些文件系统 ZFS 本身有些文件系统本身就提供了部分写失效的防范机制也可以关闭。 在数据库异常关闭的情况下启动时都会做数据库恢复redo操作恢复的过程中数据库都会检查页面是不是合法校验等等如果发现一个页面校验结果不一致则此时会用到双写这个功能。 有经验的同学也许会想到如果发生写失效可以通过重做日志(Redo Log)进行恢复啊但是要注意重做日志中记录的是对页的物理操作如偏移量 800, 写’ aaaa’记录而不是页面的全量记录而如果发生 partial page write部分页写入问题时出现问题的是未修改过的数据此时重做日志(Redo Log)无能为力。 写 doublewrite buffer 成功了这个问题就不用担心了。 如果是写 doublewrite buffer 本身失败那么这些数据不会被写到磁盘InnoDB 此时会从磁盘载入原始的数据然后通过 InnoDB 的事务日志来计算出正确的数据重新写入到 doublewrite buffer这个速度就比较慢了。如果 doublewrite buffer 写成功的话,但是写数据文件失败innodb 就不用通过事务日志来计算了, 而是直接用 doublewrite buffer 的数据再写一遍速度上会快很多。
总体来说doublewrite buffer 的作用有两个: 提高 innodb 把缓存的数据写到硬盘这个过程的安全性间接的好处就是innodb 的事务日志不需要包含所有数据的前后映像,而是二进制变化量这可以节省大量的 IO。
6.2.2.2. InnoDB 数据字典(Data Dictionary Header)
我们平时使用 INSERT 语句向表中插入的那些记录称之为用户数据MySQL只是作为一个软件来为我们来保管这些数据提供方便的增删改查接口而已。但是每当我们向一个表中插入一条记录的时候MySQL 先要校验一下插入语句对应 的表存不存在插入的列和表中的列是否符合如果语法没有问题的话还需要 知道该表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面然后把记录插入对应索引的 B树中。所以说MySQL 除了保存着我们插入的用户 数据之外还需要保存许多额外的信息比方说
某个表属于哪个表空间表里边有多少列表对应的每一个列的类型是什么该表有多少索引每个索引对应哪几个字段该索引对应的根页面在哪个表空间的哪个页面该表有哪些外键外键对应哪个表的哪些列某个表空间对应文件系统上文件路径是什么。 上述这些数据并不是我们使用 INSERT 语句插入的用户数据实际上是为了更好的管理我们这些用户数据而不得已引入的一些额外数据这些数据也称为元数据。InnoDB 存储引擎特意定义了一些列的内部系统表internal system table 来记录这些这些元数据
表名 描述 SYS_TABLES整个 InnoDB 存储引擎中所有的表的信息 SYS_COLUMNS 整个 InnoDB 存储引擎中所有的列的信息 SYS_INDEXES 整个 InnoDB 存储引擎中所有的索引的信息 SYS_FIELDS 整个 InnoDB 存储引擎中所有的索引对应的列的信息 SYS_FOREIGN 整个 InnoDB 存储引擎中所有的外键的信息 SYS_FOREIGN_COLS 整个 InnoDB 存储引擎中所有的外键对应列的信息 SYS_TABLESPACES 整个 InnoDB 存储引擎中所有的表空间信息 SYS_DATAFILES 整个 InnoDB 存储引擎中所有的表空间对应文件系统的文件 路径信息 SYS_VIRTUAL 整个 InnoDB 存储引擎中所有的虚拟生成列的信息
这些系统表也被称为数据字典它们都是以 B树的形式保存在系统表空间的某些页面中其中 SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS 这四个表尤其重要称之为基本系统表。 这 4 个表是表中之表那这 4 个表的元数据去哪里获取呢只能把这 4 个表的元数据就是它们有哪些列、哪些索引等信息硬编码到代码中然后 InnoDB的又拿出一个固定的页面来记录这 4 个表的聚簇索引和二级索引对应的 B树位置这个页面就是页号为 7 的页面 Data Dictionary Header类型为 SYS记录了数据字典的头部信息。除了这 4 个表的 5 个索引的根页面信息外这个页号为 7的页面还记录了整个 InnoDB 存储引擎的一些全局属性比如 Row ID。 数据字典头部信息中有个 Max Row ID 字段我们说过如果我们不显式的为表定义主键而且表中也没有 UNIQUE 索引那么 InnoDB 存储引擎会默认为我们生成一个名为 row_id 的列作为主键。因为它是主键所以每条记录的 row_id列的值不能重复。
原则上只要一个表中的 row_id 列不重复就可以了也就是说表 a 和表 b 拥有一样的 row_id 列也没啥关系不过 InnoDB 只提供了这个 Max Row ID 字段不论哪个拥有 row_id 列的表插入一条记录时该记录的 row_id 列的值就是 Max Row ID 对应的值然后再把 Max Row ID 对应的值加 1也就是说这个 Max Row ID是全局共享的。
用户是不能直接访问 InnoDB 的这些内部系统表的除非你直接去解析系统表空间对应文件系统上的文件。不过 InnoDB 考虑到查看这些表的内容可能有助于大家分析问题所以在系统数据库 information_schema 中提供了一些以 innodb_sys 开头的表 在 information_schema 数据库中的这些以 INNODB_SYS 开头的表并不是真正的内部系统表内部系统表就是我们上边唠叨的以 SYS 开头的那些表而是在存储引擎启动时读取这些以 SYS 开头的系统表然后填充到这些以 INNODB_SYS开头的表中。
6.3.InnoDB 的 Buffer Pool
6.3.1. 缓存的重要性
我们知道对于使用 InnoDB 作为存储引擎的表来说不管是用于存储用户数据的索引包括聚簇索引和二级索引还是各种系统数据都是以页的形式存放在表空间中的而所谓的表空间只不过是 InnoDB 对文件系统上一个或几个实际文件的抽象也就是说我们的数据说到底还是存储在磁盘上的。
但是磁盘的速度慢所以 InnoDB 存储引擎在处理客户端的请求时当需要访问某个页的数据时就会把完整的页的数据全部加载到内存中也就是说即使我们只需要访问一个页的一条记录那也需要先把整个页的数据加载到内存中。 将整个页加载到内存中后就可以进行读写访问了在进行完读写访问之后并不着急把该页对应的内存空间释放掉而是将其缓存起来这样将来有请求再次访问该页面时就可以省去磁盘 IO 的开销了。
6.3.2. Buffer Pool
InnoDB 为了缓存磁盘中的页在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存他们给这片内存起了个名叫做 Buffer Pool中文名是缓冲池。那它有多大呢这个其实看我们机器的配置默认情况下 Buffer Pool 只有 128M 大小这个值其实是偏小的。
show variables like innodb_buffer_pool_size;可以在启动服务器的时候配置 innodb_buffer_pool_size 参数的值它表示 Buffer Pool 的大小就像这样 [server] innodb_buffer_pool_size 268435456 其中268435456 的单位是字节也就是指定 Buffer Pool 的大小为 256M。 需要注意的是Buffer Pool 也不能太小最小值为 5M(当小于该值时会自动设置 成 5M)。
Buffer Pool 内部组成: Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的都是16KB。为了更好的管理这些在 Buffer Pool 中的缓存页InnoDB 为每一个缓存页都创建了一些所谓的控制信息这些控制信息包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息当然还有一些别的控制信息。
每个缓存页对应的控制信息占用的内存大小是相同的我们称为控制块。控制块和缓存页是一一对应的它们都被存放到 Buffer Pool 中其中控制块被存放到 Buffer Pool 的前边缓存页被存放到 Buffer Pool 后边所以整个 Buffer Pool 对应的内存空间看起来就是这样的 每个控制块大约占用缓存页大小的 5%而我们设置的 innodb_buffer_pool_size 并不包含这部分控制块占用的内存空间大小也就是说InnoDB 在为 Buffer Pool 向操作系统申请连续的内存空间时这片连续的内存空 间一般会比 innodb_buffer_pool_size 的值大 5%左右。
6.3.2.1. free 链表的管理
最初启动 MySQL 服务器的时候需要完成对 Buffer Pool 的初始化过程就是先向操作系统申请 Buffer Pool 的内存空间然后把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到 Buffer Pool 中因为还没有用到之后随着程序的运行会不断的有磁盘上的页被缓存到 Buffer Pool 中。
那么问题来了从磁盘上读取一个页到 Buffer Pool 中的时候该放到哪个缓存页的位置呢或者说怎么区分 Buffer Pool 中哪些缓存页是空闲的哪些已经被使用了呢最好在某个地方记录一下 Buffer Pool 中哪些缓存页是可用的这个时候缓存页对应的控制块就派上大用场了我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中这个链表也可以被称作 free 链表或者说空闲链表。刚刚完成初始化的 Buffer Pool 中所有的缓存页都是空闲的所以每一个缓存页对应的控制块都会被加入到 free 链表中假设该 Buffer Pool 中可容纳的缓存页数量为 n那增加了 free 链表的效果图就是这样的 有了这个 free 链表之后每当需要从磁盘中加载一个页到 Buffer Pool 中时就从 free 链表中取一个空闲的缓存页并且把该缓存页对应的控制块的信息填上就是该页所在的表空间、页号之类的信息然后把该缓存页对应的 free链表节点从链表中移除表示该缓存页已经被使用了。
6.3.2.2. 缓存页的哈希处理
我们前边说过当我们需要访问某个页中的数据时就会把该页从磁盘加载到 Buffer Pool 中如果该页已经在 Buffer Pool 中的话直接使用就可以了。那么问题也就来了我们怎么知道该页在不在 Buffer Pool 中呢难不成需要依次遍历。
Buffer Pool 中各个缓存页么 我们其实是根据表空间号 页号来定位一个页的也就相当于表空间号 页号是一个 key缓存页就是对应的 value怎么通过一个 key 来快速找着一个value 呢
所以我们可以用表空间号 页号作为 key缓存页作为 value 创建一个哈希表在需要访问某个页的数据时先从哈希表中根据表空间号 页号看看有没有对应的缓存页如果有直接使用该缓存页就好如果没有那就从 free 链表中选一个空闲的缓存页然后把磁盘中对应的页加载到该缓存页的位置。
6.3.2.3. flush 链表的管理
如果我们修改了 Buffer Pool 中某个缓存页的数据那它就和磁盘上的页不一致了这样的缓存页也被称为脏页英文名dirty page。当然最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后我们并不着急立即把修改同步到磁盘上而是在未来的某个时间点进行同步。
但是如果不立即同步到磁盘的话那之后再同步的时候我们怎么知道 BufferPool 中哪些页是脏页哪些页从来没被修改过呢总不能把所有的缓存页都同步到磁盘上吧假如 Buffer Pool 被设置的很大比方说 300G那一次性同步会非常慢。
所以需要再创建一个存储脏页的链表凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的所以也叫 flush 链表。链表的构造和 free 链表差不多。
6.3.2.4. LRU 链表的管理
缓存不够的窘境 Buffer Pool 对应的内存大小毕竟是有限的如果需要缓存的页占用的内存大小超过了 Buffer Pool 大小也就是 free 链表中已经没有多余的空闲缓存页的时 候该咋办当然是把某些旧的缓存页从 Buffer Pool 中移除然后再把新的页放进来那么问题来了移除哪些缓存页呢 为了回答这个问题我们还需要回到我们设立 Buffer Pool 的初衷我们就是想减少和磁盘的 IO 交互最好每次在访问某个页的时候它都已经被缓存到 Buffer Pool 中了。假设我们一共访问了 n 次页那么被访问的页已经在缓存中的次数除
以 n 就是所谓的缓存命中率我们的期望就是让缓存命中率越高越好。 从这个角度出发回想一下我们的微信聊天列表排在前边的都是最近很 频繁使用的排在后边的自然就是最近很少使用的假如列表能容纳下的联系人有限你是会把最近很频繁使用的留下还是最近很少使用的留下呢当然是留下最近很频繁使用的了。 简单的 LRU 链表
管理 Buffer Pool 的缓存页其实也是这个道理当 Buffer Pool 中不再有空闲的缓存页时就需要淘汰掉部分最近很少使用的缓存页。不过我们怎么知道哪些缓存页最近频繁使用哪些最近很少使用呢 再创建一个链表由于这个链表是为了按照最近最少使用的原则去淘汰缓存页的所以这个链表可以被称为 LRU 链表LRU 的英文全称Least Recently Used。 当我们需要访问某个页时可以这样处理 LRU 链表 如果该页不在 Buffer Pool 中在把该页从磁盘加载到 Buffer Pool 中的缓存页时就把该缓存页对应的控制块作为节点塞到 LRU 链表的头部。 如果该页已经缓存在 Buffer Pool 中则直接把该页对应的控制块移动到 LRU链表的头部。 也就是说只要我们使用到某个缓存页就把该缓存页调整到 LRU 链表的头部这样 LRU 链表尾部就是最近最少使用的缓存页。所以当 Buffer Pool 中的空闲缓存页使用完时到 LRU 链表的尾部找些缓存页淘汰就行了。 划分区域的 LRU 链表 但是这种实现存在两种比较尴尬的情况 情况一InnoDB 提供了预读英文名read ahead。所谓预读就是 InnoDB认为执行当前的请求可能之后会读取某些页面就预先把它们加载到 Buffer Pool 中。根据触发方式的不同预读又可以细分为下边两种 线性预读 InnoDB 提供了一个系统变量 innodb_read_ahead_threshold如果顺序访问了某个区extent的页面超过这个系统变量的值就会触发一次异步读取下一个区中全部的页面到 Buffer Pool 的请求。
这个 innodb_read_ahead_threshold 系统变量的值默认是 56我们可以在服务器启动时通过启动参数或者服务器运行过程中直接调整该系统变量的值。 随机预读 如果 Buffer Pool 中已经缓存了某个区的 13 个连续的页面不论这些页面是不是顺序读取的都会触发一次异步读取本区中所有其他的页面到 Buffer Pool的请求。InnoDB 同时提供了 innodb_random_read_ahead 系统变量它的默认值为 OFF。
如果预读到 Buffer Pool 中的页成功的被使用到那就可以极大的提高语句执行的效率。可是如果用不到呢这些预读的页都会放到 LRU 链表的头部但是如果此时 Buffer Pool 的容量不太大而且很多预读的页面都没有用到的话这就会导致处在 LRU 链表尾部的一些缓存页会很快的被淘汰掉也就是所谓的劣币驱逐良币会大大降低缓存命中率。 情况二应用程序可能会写一些需要扫描全表的查询语句比如没有建立合 适的索引或者压根儿没有 WHERE 子句的查询) 。
扫描全表意味着什么意味着将访问到该表所在的所有页假设这个表中记录非常多的话那该表会占用特别多的页当需要访问这些页时会把它们统统都加载到 Buffer Pool 中这也就意味着 Buffer Pool 中的所有页都被换了一次血其他查询语句在执行时又得执行一次从磁盘加载到 Buffer Pool 的操作。而这种全表扫描的语句执行的频率也不高每次执行都要把 Buffer Pool 中的缓存页换一次血这严重的影响到其他查询对 Buffer Pool 的使用从而大大降低了缓存命中率。
总结一下上边说的可能降低 Buffer Pool 的两种情况 加载到 Buffer Pool 中的页不一定被用到。 如果非常多的使用频率偏低的页被同时加载到 Buffer Pool 时可能会把那些 使用频率非常高的页从 Buffer Pool 中淘汰掉。
因为有这两种情况的存在所以 InnoDB 把这个 LRU 链表按照一定比例分成 两截分别是 一部分存储使用频率非常高的缓存页所以这一部分链表也叫做热数据或者称 young 区域。 另一部分存储使用频率不是很高的缓存页所以这一部分链表也叫做冷数据或者称 old 区域。 我们是按照某个比例将 LRU 链表分成两半的不是某些节点固定是 young 区域的某些节点固定是 old 区域的随着程序的运行某个节点所属的区域也可能发生变化。那这个划分成两截的比例怎么确定呢对于 InnoDB 存储引擎来说我们可以通过查看系统变innodb_old_blocks_pct的值来确定old区域在LRU 链表中所占的比例比方说这样
SHOW VARIABLES LIKE innodb_old_blocks_pct;从结果可以看出来默认情况下old 区域在 LRU 链表中所占的比例是 37%也 就是说 old 区域大约占 LRU 链表的 3/8。这个比例我们是可以设置的我们可以 在启动时修改 innodb_old_blocks_pct 参数来控制 old 区域在 LRU 链表中所占的比 例。在服务器运行期间我们也可以修改这个系统变量的值不过需要注意的是 这个系统变量属于全局变量。
有了这个被划分成 young 和 old 区域的 LRU 链表之后InnoDB 就可以针对我们上边提到的两种可能降低缓存命中率的情况进行优化了
针对预读的页面可能不进行后续访问情况的优化 InnoDB 规定当磁盘上的某个页面在初次加载到 Buffer Pool 中的某个缓存页时该缓存页对应的控制块会被放到 old 区域的头部。这样针对预读到 Buffer Pool 却不进行后续访问的页面就会被逐渐从 old 区域逐出而不会影响 young 区 域中被使用比较频繁的缓存页。
针对全表扫描时短时间内访问大量使用频率非常低的页面情况的优化 在进行全表扫描时虽然首次被加载到 Buffer Pool 的页被放到了 old 区域的头部但是后续会被马上访问到每次进行访问的时候又会把该页放到 young 区域的头部这样仍然会把那些使用频率比较高的页面给顶下去。
有同学会想可不可以在第一次访问该页面时不将其从 old 区域移动到young 区域的头部后续访问时再将其移动到 young 区域的头部。回答是行不通因为 InnoDB 规定每次去页面中读取一条记录时都算是访问一次页面而一个页面中可能会包含很多条记录也就是说读取完某个页面的记录就相当于访问了这个页面好多次。
全表扫描有一个特点那就是它的执行频率非常低出现了全表扫描的语句也是我们应该尽快优化的对象。而且在执行全表扫描的过程中即使某个页面中有很多条记录也就是去多次访问这个页面所花费的时间也是非常少的。
所以在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间如果后续的访问时间与第一次访问的时间在某个时间间隔内那么该页面就不会被从 old 区域移动到 young 区域的头部否则将它移动到 young 区域的头部。上述的这个间隔时间是由系统变量 innodb_old_blocks_time 控制的
SHOW VARIABLES LIKE innodb_old_blocks_time;这个 innodb_old_blocks_time 的默认值是 1000它的单位是毫秒也就意味着对于从磁盘上被加载到 LRU 链表的 old 区域的某个页来说如果第一次和最后一次访问该页面的时间间隔小于 1s很明显在一次全表扫描的过程中多次访问一 个页面中的时间不会超过 1s那么该页是不会被加入到 young 区域的 当然 像 innodb_old_blocks_pct 一样我们也可以在服务器启动或运行时设置 innodb_old_blocks_time 的值这里需要注意的是如果我们把 innodb_old_blocks_time 的值设置为 0那么每次我们访问一个页面时就会把该页面放到 young 区域的头部。
综上所述正是因为将 LRU 链表划分为 young 和 old 区域这两个部分又添加了 innodb_old_blocks_time 这个系统变量才使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制因为用不到的预读页面以及全表扫描的页面都只会被放到 old 区域而不影响 young 区域中的缓存页。 更进一步优化 LRU 链表 对于 young 区域的缓存页来说我们每次访问一个缓存页就要把它移动到LRU 链表的头部这样开销是不是太大
毕竟在 young 区域的缓存页都是热点数据也就是可能被经常访问的这样频繁的对 LRU 链表进行节点移动操作也会拖慢速度为了解决这个问题MySQL中还有一些优化策略比如只有被访问的缓存页位于 young 区域的 1/4 的后边才会被移动到 LRU 链表头部这样就可以降低调整 LRU 链表的频率从而提升性能还有没有什么别的针对 LRU 链表的优化措施呢当然还有我们这里不继续说了更多的需要看 MySQL 的源码但是不论怎么优化出发点就是尽量高 效的提高 Buffer Pool 的缓存命中率。
其他的一些链表 为了更好的管理 Buffer Pool 中的缓存页除了我们上边提到的一些措施InnoDB 们还引进了其他的一些链表比如 unzip LRU 链表用于管理解压页zip clean 链表用于管理没有被解压的压缩页zip free 数组中每一个元素都代表一个 链表它们组成所谓的伙伴系统来为压缩页提供内存空间等等。
6.3.2.5. 刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘这样可以不影响用户线程处理正常的请求。主要有两种刷新路径
1、从 LRU 链表的冷数据中刷新一部分页面到磁盘。 后台线程会定时从 LRU 链表尾部开始扫描一些页面扫描的页面数量可以通 过系统变量 innodb_lru_scan_depth 来指定如果从里边儿发现脏页会把它们 刷新到磁盘。这种刷新页面的方式被称之为 BUF_FLUSH_LRU。
2、从 flush 链表中刷新一部分页面到磁盘 后台线程也会定时从 flush 链表中刷新一部分页面到磁盘刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为 BUF_FLUSH_LIST。 有时候后台线程刷新脏页的进度比较慢导致用户线程在准备加载一个磁盘页到 Buffer Pool 时没有可用的缓存页这时就会尝试看看 LRU 链表尾部有没有可以直接释放掉的未修改页面如果没有的话会不得不将 LRU 链表尾部的一个脏页同步刷新到磁盘和磁盘交互是很慢的这会降低处理用户请求的速度。这种刷新单个页面到磁盘中的刷新方式被称之为 BUF_FLUSH_SINGLE_PAGE。
当然有时候系统特别繁忙时也可能出现用户线程批量的从 flush 链表中 刷新脏页的情况很显然在处理用户请求过程中去刷新脏页是一种严重降低处理 速度的行为这属于一种迫不得已的情况。
6.3.2.6. 多个 Buffer Pool 实例
我们上边说过Buffer Pool 本质是 InnoDB 向操作系统申请的一块连续的内存空间在多线程环境下访问 Buffer Pool 中的各种链表都需要加锁处理在Buffer Pool 特别大而且多线程并发访问特别高的情况下单一的 Buffer Pool 可能会影响请求的处理速度。所以在 Buffer Pool 特别大的时候我们可以把它们拆分成若干个小的 Buffer Pool每个 Buffer Pool 都称为一个实例它们都是独立的独立的去申请内存空间独立的管理各种链表所以在多线程并发访问时并不会 相互影响从而提高并发处理能力。
我们可以在服务器启动的时候通过设置 innodb_buffer_pool_instances 的值 来修改 Buffer Pool 实例的个数那每个 Buffer Pool 实例实际占多少内存空间呢其实使用这个公式算出来的innodb_buffer_pool_size/innodb_buffer_pool_instances也就是总共的大小除以实例的个数结果就是每个 Buffer Pool 实例占用的大小。
不过也不是说 Buffer Pool 实例创建的越多越好分别管理各个 Buffer Pool 也是需要性能开销的InnoDB 规定当 innodb_buffer_pool_size默认 128M的值小于 1G 的时候设置多个实例是无效的InnoDB 会默认把 innodb_buffer_pool_instances 的值修改为 1。所以 Buffer Pool 大于或等于 1G 的 时候设置应该多个 Buffer Pool 实例。
6.3.2.7. innodb_buffer_pool_chunk_size
在 MySQL 5.7.5 之前Buffer Pool 的大小只能在服务器启动时通过配置innodb_buffer_pool_size 启动参数来调整大小在服务器运行过程中是不允许调整该值的。不过 MySQL 在 5.7.5 以及之后的版本中支持了在服务器运行过程中调整 Buffer Pool 大小的功能
但是有一个问题就是每次当我们要重新调整 Buffer Pool 大小时都需要重新向操作系统申请一块连续的内存空间然后将旧的 Buffer Pool 中的内容复制到这一块新空间这是极其耗时的。所以 MySQL 决定不再一次性为某个 Buffer Pool 实例向操作系统申请一大片连续的内存空间而是以一个所谓的 chunk 为单位向操作系统申请空间。也就是说一个Buffer Pool实例其实是由若干个chunk组成的一个 chunk 就代表一片连续的内存空间里边儿包含了若干缓存页与其对应的控制块 正是因为发明了这个 chunk 的概念我们在服务器运行期间调整 Buffer Pool的大小时就是以 chunk 为单位增加或者删除内存空间而不需要重新向操作系统申请一片大的内存然后进行缓存页的复制。这个所谓的 chunk 的大小是我们在启动操作 MySQL 服务器时通过 innodb_buffer_pool_chunk_size 启动参数指定的它的默认值是 134217728也就是 128M。
不过需要注意的是innodb_buffer_pool_chunk_size 的值只能在服务器启动时指定在服务器运行过程中是不可以修改的。
Buffer Pool 的缓存页除了用来缓存磁盘上的页面以外还可以存储锁信息、自适应哈希索引等息。
6.3.2.8. 查看 Buffer Pool 的状态信息
MySQL 给我们提供了 SHOW ENGINE INNODB STATUS 语句来查看关于 InnoDB 存储引擎运行过程中的一些状态信息其中就包括 Buffer Pool 的一些信息我们 看一下为了突出重点我们只把输出中关于 Buffer Pool 的部分提取了出来
SHOW ENGINE INNODB STATUS\G这里边的每个值都代表什么意思如下知道即可
Total memory allocated代表 Buffer Pool 向操作系统申请的连续内存空间大小包括全部控制块、缓存页、以及碎片的大小。Dictionary memory allocated为数据字典信息分配的内存空间大小注意这个内存空间和 - — Buffer Pool 没啥关系不包括在 Total memory allocated 中。 Buffer pool size代表该 Buffer Pool 可以容纳多少缓存页注意单位是页Free buffers代表当前 Buffer Pool 还有多少空闲缓存页也就是 free 链表中还有多少个节点。Database pages代表 LRU 链表中的页的数量包含 young 和 old 两个区域的节点数量。Old database pages代表 LRU 链表 old 区域的节点数量。Modified db pages代表脏页数量也就是 flush 链表中节点的数量。Pending reads正在等待从磁盘上加载到 Buffer Pool 中的页面数量。 当准备从磁盘中加载某个页面时会先为这个页面在 Buffer Pool 中分配一个缓存页以及它对应的控制块然后把这个控制块添加到 LRU 的 old 区域的头部但是这个时候真正的磁盘页并没有被加载进来Pending reads 的值会跟着加 1。Pending writes LRU即将从 LRU 链表中刷新到磁盘中的页面数量。Pending writes flush list即将从 flush 链表中刷新到磁盘中的页面数量。Pending writes single page即将以单个页面的形式刷新到磁盘中的页面数量。Pages made young代表 LRU 链表中曾经从 old 区域移动到 young 区域头部的节点数量。Page made not young在将 innodb_old_blocks_time 设置的值大于 0 时首次访问或者后续访问某个处在old区域的节点时由于不符合时间间隔的限制而不能将其移动到 young 区域头部时Page made not young 的值会加 1。youngs/s代表每秒从 old 区域被移动到 young 区域头部的节点数量。non-youngs/s代表每秒由于不满足时间限制而不能从 old 区域移动到 young区域头部的节点数量。Pages read、created、written代表读取创建写入了多少页。后边跟着 读取、创建、写入的速率。Buffer pool hit rate表示在过去某段时间平均访问 1000 次页面有多少次该页面已经被缓存到 Buffer Pool 了。young-making rate表示在过去某段时间平均访问 1000 次页面有多少次访问使页面移动到 young 区域的头部了。not (young-making rate)表示在过去某段时间平均访问 1000 次页面有多少次访问没有使页面移动到 young 区域的头部。LRU len代表 LRU 链表中节点的数量。unzip_LRU代表 unzip_LRU 链表中节点的数量。I/O sum最近 50s 读取磁盘页的总数。I/O cur现在正在读取的磁盘页数量。I/O unzip sum最近 50s 解压的页面数量。I/O unzip cur正在解压的页面数量。