作者:孤独烟
某日,烟哥前去面试(纯属瞎编),有了如下对话
面试官:"懂mysql吧,知道CPU在读硬盘上数据的时候,是怎么解决CPU和硬盘速度不一致问题么?" 烟哥:"懂啊,mysql先把数据页加载到内存里,然后读内存中的数据啊!" 面试官:"你们用的是哪个存储引擎?" 烟哥:"嗯,innodb,因为需要用事务功能!" 面试官:"嗯,好。那既然需要把数据页加载到内存里,这里必然涉及到LRU算法,当这块区域满了后,将一些不常用的数据页淘汰掉,innodb具体怎么做的?" 烟哥尴尬的笑了笑,回答道:"我只知道redis中LRU怎么做的..balabala" 面试官:"停,我只想知道innodb怎么做的?" 烟哥:"我还是回去等通知吧~"
接下来烟哥回去
于是就有了本文诞生
Innodb为了解决磁盘上磁盘速度和CPU速度不一致的问题,在操作磁盘上的数据时,先将数据加载至内存中,在内存中对数据页进行操作。
Mysql在启动的时候,会向内存申请一块连续的空间,这块空间名为Bufffer Pool
,也就是缓冲池,默认情况下Buffer Pool只有128M。
那缓冲池长什么样的呢,如下图所示
图片出自《Mysql运维内参》
如图所示,有三部分组成:
这个控制体ctl在mysql源码中长这样的
struct buf_block_t{
//省略
buf_page_t page;
byte* frame;
//省略
}
嗯,懂C语言的自然知道,frame是一个指针啦,指向缓存页。 而page存储的就是该页所属的表空间编号、页号等。
在BufferPool中有三大链表,需要重点关注,它们存储的元素都是buf_page_t
。
比如,我总要知道那些页是可以用,是空闲的吧。
OK,这些信息在free链表
中维护。
再比如,CPU肯定是不会去修改磁盘上的数据。那么,CPU修改了BufferPool中的数据后,Innodb总要知道要把哪一块信息刷到磁盘上吧。
OK,这些信息在flush链表
中维护。
最后,当free链表
里没多余的空闲页啦,innodb要淘汰一些缓存页啦。怎么淘汰?
这还用问,一定是淘汰最近最少使用的缓存页啊。
怎么知道这些页是最近最少使用的呢?
嗯,那就是要借助传说中的LRU链表
啦。
我们先来说一个简单的LRU算法。LRU嘛,全称吧啦吧啦…英文名忘了。反正就是一个淘汰最近最少使用的算法。 然后,烟哥去百度了一下,我发现百度是这么说的 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
嗯,完美!很完美!反正innodb中不可能这样设计~
那么为什么不能这么设计呢?
原因一
假设有一张表叫yan_ge_hao_shuai
,(请将表名多看几遍),回到正题,这张表什么索引都木有,有着几千万数据,反正就是很多很多数据页。然后,执行下面的语句
select * from yan_ge_hao_shuai
因为没有任何索引嘛,那就进行全表扫描了。那么按照上面说的算法,这些数据页也会被全部塞入LRU链表
,并且通通加载到BufferPool中,从而迅速清空其他查询语句留下来的高频的数据页。那么此时,你的BufferPool里全是低频的数据页,就会发现缓存命中率大大滴降低了。
于是你就会觉得:"我勒个去,设计这个Innodb的人,怕不是脑袋有问题…(以下省略一万字)"
原因二 这里先说以下innodb的预读机制,是这样子滴!这个预读细说起来可以分为线性预读和随机预读。借一张姜承尧大大的图,innodb的表逻辑结构如下图所示
从 InnoDB存储引擎的逻辑存储结构看,所有数据都被逻辑地存放在一个空间中,称之为表空间( tablespace)。表空间又由段(segment)、区( extent)、页(page)组成。页在一些文档中有时也称为块( block), InnoDB存储引擎的逻辑存储结构大致如图所示。
其实借这张图,我只想说一件事。数据页(page)是放在区(extent)里的。 那么
innodb_random_read_ahead
控制。好了,上面那一堆其实看不懂也没事。我只想说一件事,预读机制会预读一些额外的页到到BufferPool中。
那么,如果这些预读页并不是高频的页呢?
OK,如果这些页并不是高频的页,按照上面的算法,也会被加入LRU 链表
,就会将链表末端一些高频的数据页给淘汰掉,从而导致命中率下降。
于是你会觉得:"唉,自己写一个都比他强…(此处略过一万字)"
OK,为了解决上面的两个缺点。Innodb将这个链表分为两个部分,也就是所谓的old区
和young区
。
天啦噜,这两个区干嘛用的?
ok,young区
在链表的头部,存放经常被访问的数据页,可以理解为热数据!
ok,old区
在链表的尾部,存放不经常被访问的数据页,可以理解为冷数据!
这两个部分的交汇处称为midpoint
,往下看!
怎么知道两个区的比例?
执行下面的命令
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37 |
+-----------------------+-------+
1 row in set (0.01 sec)
这说明了old区
的比例为37%,也就是冷数据大概占LRU链表
的3/8。剩下的就是young区
的热数据。
于是可以得到一张大概的LRU链表
图,如下所示(图片出自网络)
ps:
一般生产的机器,内存比较大。我们会把innodb_old_blocks_pct
值调低,防止热数据被刷出内存。
数据何时在old区,何时进入young区?
好,数据页第一次被加载进BufferPool时在old区
头部。
当这个数据页在old区
,再次被访问到,会做如下判断
old区
存在的时间超过了1秒,就把它移动到young区
innodb_old_blocks_time
控制我们来看看innodb_old_blocks_time
的值,如下所示
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000 |
+------------------------+-------+
1 row in set (0.01 sec)
那怎么解决这些缺点的?
针对原因一
也就是所谓的全表扫描导致Bufferpool中的高频数据页快速被淘汰的问题。
Innodb这么做的:
(1)扫描过程中,需要新插入的数据页,都被放到old区
(2)一个数据页会有多条记录,因此一个数据页会被访问多次
(3)由于是顺序扫描,数据页的第一次被访问和最后一次被访问的时间间隔不会超过1S,因此还是会留在old区
(4)继续扫描,之前的数据页再也不会被访问到,因此也不会被移到young区
,最终很快被淘汰
针对原因二
也就是预读到的页,可能不是高频次的页。
你看,你预读到的页,是存在old区
的。如果这个页后续不会被继续访问到,是会在old区
逐步被淘汰的。因此不会影响young区
的热数据。
执行下面命令即可
mysql> show engine innnodb status\G
……
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
1、数据页从冷到热,称为young;not young就是数据在没有成为热数据情况下就被刷走的量(累计值)。
2、non-youngs/s,这个数值如果很高,一般情况下就是系统存在严重的全表扫描,自然意味着很高的物理读。(上面分析过)
3、youngs/s,如果这个值相对较高,最好增加一个innodb_old_blocks_time
,降低innodb_old_blocks_pct
,保护热数据。
本文总结了Innodb中的LRU是如何做的,希望大家有所收获。 另外,唉,最近有一番新的感慨。
心累,社会呀~
你可能会喜欢