Innodb缓冲池(buffer pool)
Innodb缓冲池缓存行的数据、自适应索引、插入缓冲、锁、索引页、数据字典以及其他的一些内部数据结构,所以对于Inndo来说是严重的依赖于缓冲池的,当mysql启动的时候,Innodb引擎会想操作系统申请一块连续的内存空间,然后按照页的大小(默认16kb)来划分出一个个空白页,当磁盘上的页缓存到内存的buffer pool 中会对空页进行填充。
查看和设置缓冲池大小
InnoDB 引擎通过 innodb_buffer_pool_size 变量查看缓冲池的大小,默认大小128M。一般建议设置成可用物理内存的 60%~80%。
show variables like 'innodb_buffer_pool_size' -- 查看(单位字节) set global innodb_buffer_pool_size -- 修改
对于以上的估值需要考虑到物理机器其他软件的内存使用,比如需要考虑到MYSQL实例是否是唯一运行在该物理机上的程序,以及mysql自身内存使用比如日志
如果数据量不是很大的情况,就没有必要为缓冲池分配较大的内存,很大的缓冲区会带来一些不可避免的麻烦,比如预热和关闭的时候。
- 例如数据库启动完成之后需要进行很长时间的数据预热,不过我们可以选择在关闭服务器的时候保存一下buffer pool中的内容,并在服务器启动时将buffer_pool恢复到关闭前的状态,避免数据库刚开始运行的一段时间内业务访问所有请求数据页都需要重新从磁盘上读取,减小数据库重启对系统带来的性能影响。
- 例如如果有很多脏页在缓冲池里,Innodb关闭时可能会花费较长的时间,因为在关闭之前需要把脏页写回数据文件,即使强制进行关闭了重启的时候就必须多做更多的恢复工作。
缓存页的管理
Innodb管理这些缓冲页方式就是使用LRU(Least recently used页面置换算法),这个算法的核心就是淘汰最久未使用的数据,innodb通过链表来实现。
LRU链表:
链表头节点存储最近使用的数据,链表尾部节点存储最久未被使用的数据。淘汰时删除链表尾部节点。
参数:innodb_old_blocks_pct
介绍:老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。
传统的LRU算法(最近最久未使用算法)
常见的手法就是将入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里有两种情况:
1、页已经在缓冲池里面,那就只需要做将其移动到LRU头部位置的动作,而没有页淘汰。
图中管理缓冲池中的LRU长度为10,缓冲了页号为1-7的页。假设要访问的数据在页号为4页中:
页号为4的页本来就在缓冲池中,所以只需要把页号为4的页放入到LRU的头部即可,不需要淘汰页。
2、页不在缓冲池中,除了要做放入LRU头部的动作,还要做淘汰LRU尾部页的动作。
假设要访问的数据在页号为50的页中:
页号为50的页原来不在缓冲池里面,所以需要把页号为50的页放到LRU头部,同时淘汰尾部页号为7的页。
考虑的问题
传统的LRU缓冲池算法十分直观简单易懂,但是MySQL却没有采用,这里涉及到两个问题:
1、预读失效
Innodb预读机制
Innodb在I/O的优化上有个比较重要的特性为预读,预读请求是一个I/O请求,它会异步地在缓冲池中预存多个数据页,且自身预计很快后续请求将访问这些数据页。
数据库在请求数据的时候,会将读请求交由文件系统,放入到请求队列中去。相关进程就会从请求队列中将读请求取出,根据需求到相关数据区(内存、磁盘)去读取数据,取出数据并放入到响应队列中去,最后数据库就会从响应队列中将数据取走,完成一次数据读操作过程。
接着进程继续处理请求队列,(如果数据库是全表扫描的话,数据读请求将会占满请求队列)
判断后面几个数据读请求的数据是否相邻,在根据自身系统IO带宽处理量来进行预读,进行读请求的合并操作,一次性读取多快数据放入响应队列中,然后在被数据库取走。
预读算法
mysql有两种预读算法线性预读(linear read-ahead)和随机预读(randomread-ahead),但是其5.5版本之后默认是使用线性预读禁用随机预读
- 线性预读
线性预读的单位是extend,一个extend中有64个page,线性预读的一个重要参数 innodb_read_ahead_threshold 是指在连续访问多个页面之后,把下一个extend读入到
buffer pool 中,不过预读是一个异步操作,有个限制就是这个参数不能超过64,因为一个
extend最多只能有64个page。
mysql> show variables like 'innodb_read_ahead_threshold'; +-----------------------------+-------+ | Variable_name | Value | +-----------------------------+-------+ | innodb_read_ahead_threshold | 56 | +-----------------------------+-------+ /** innodb_read_ahead_threshold = 56,就是指在连续访问了一个为extend的56个页面之后就把下一个 extend读入到buffer pool中来, **/
- 随机预读
随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中。
预读失败即提前把页放入了缓冲池中,但最终并没有从页中读取数据,称为预读失败。
要优化预读失败就要考虑以下两点,①让预读失败的页,停留在缓冲池LRU里的时间尽可能的短。②让真正被读取的热数据留在缓冲池里的时间尽可能长。
逻辑实现:
- 将LRU分为两部分即新生代【newSublist】和老生代【oldSublist】
- 新生代首尾相连,即新生代的尾(tail)连接着老生代的头(head)
- 新页(被预读的页)加入缓冲池时,只加入到老生代的头部。如果数据被真正的读取即预读成功,才会被加入到新生代的头部,如果数据没有被读取即预读失败,则会比新生代里的热数据页更早地从缓冲池中淘汰。
例如当前整个缓冲池LRU如上图所示,我们分析出如下信息:
- 整个LRU长度是10;
- 前70%是新生代;
- 后30%是老生代;
- 新老生代首尾相连;
现在假设有一个页号为50的新页被预读加入到缓冲池,如下所示:
- 页号为50的新页只会从老生代头部插入,老生代尾部的页会直接被淘汰掉。
- 假设为50的这一页不会被真正读取即预读失败,它将被新生代的数据更早淘汰出缓冲池。
现在假如50这一页立刻被读取到,例如当前请求SQL读取到了这一页的行数据,如下所示:
- 它会被立刻加入到新生代的头部位置。
- 新生代的页会被挤入到老生代的头部位置,此时也不会有页被真正淘汰。
1、缓冲池污染
缓冲池污染就是当系统中执行某一个SQL语句,要批量臊面大量数据时,可能导致把缓冲池中的所有页都替换出去,大量热数据被挤出去,mysql性能就会急剧下降,这种情况就被称之为缓冲池污染。
例如,当前有一个数据量较大的用户表,当执行一个无法命中索引的而最终导致走全表扫描的SQL语句时,就需要访问大量的数据页,即使结果集中可能最终只有少量数据。
而缓冲池要做的就是:
① 将页加到缓冲池中,即插入到老生代头部位置
② 当页预读成功后插入到新生代头部位置
③ 根据SQL语句的where条件进行比较将符合条件的数据加入到结果集中,直到扫描完成所有页数据。
这样下来,大量的数据页都会被加载到新生代的头部中,但是可能只会被访问一次,而真正的热数据被大量挤出
逻辑实现:
参数:innodb_old_blocks_time
介绍:老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件,才会被插入到新生代头部。
mysql缓冲池加入了一个老生代停留时间窗口的机制:
- 假设T= 老生代停留时间窗口
- 插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部
- 只有满足“被访问”且“老生代停留时间”大于T,才会被放入新生代头部
现在假设批量数据扫描,有51、52、53、54、55 等五个页面将要被依次访问,如下所示:
继续假设如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会换出大量热数据:
加入【老生代停留时间窗口】策略后,短时间内被大量加的页,并不会立刻插入新生代头部,而是优先淘汰那些短期内仅被访问了一次页:
而只有在老生代呆的时间足够久且页被访问,停留时间大于T,才会被插入新生代头部:
脏页
即内存页(缓冲池中存储的),对数据做修改时首先更改内存中的数据页,如果没有就会去磁盘中进行IO读写数据页到buffer pool中。由于缓存中的数据跟磁盘中的数据不一致,所以我们就称为“脏页”反之,内存数据写入到m
假设要修改页号为4的索引页,而正好这个页就在缓冲池内:(图1)
- 直接修改缓冲池中的页,一次内存操作。
- 写入redo log日志,一次磁盘顺序写操作。
是否会出现一致性问题呢?不会。
- 读取的时候会命中缓冲池的页
- 缓冲池LRU数据淘汰,会将脏页刷回磁盘
- 数据库异常崩溃,能够从redo log中恢复数据
假设要修改页号为40的索引页,而这个页正好不在缓冲池内:(图2)
- 先把需要为40的索引页,从磁盘加载到缓冲池,一次磁盘随机读操作
- 修改缓冲池中的页,一次内存操作
- 写入redo log,一次磁盘顺序写操作
写缓存(Change Buffer)在5.5之前叫做 插入缓存(insert Buffer),因为只支持插入的缓存,在随后版本又添加了 update、delete,所以改名 change Buffer。因为直接对磁盘进行IO操作会比较耗时,如果我们的程序在高并发的场景,同时某段时间写操作非常多,那么如果直接更新到磁盘上数据库的压力就会非常大,甚至崩溃。为了避免这种情况,可以错开高峰期,让数据在系统空闲时再更新到磁盘,那么该如何实现,Change Buffer就起到这样的作用。
当加入缓冲池优化后,再次来进行图二的操作流程就变为如下了:
- 在写缓冲中记录这个操作,一次内存操作
- 写入redo log,一次磁盘顺序写操作
是否会出现一致性问题呢?不会。
- 数据库异常奔溃,能够从redo log中恢复数据
- 写缓冲不只是一个内存结构,它也会被定期刷盘到写缓冲系统表空间
- 数据读取时,有另外的流程,将数据合并到缓冲池
在写操作语句进来时,首先会判断缓冲池是否存在这条写操作对应的数据页,如果存在直接更新数据页中对应的数据,然后将对数据页的操作逻辑记入 redo log;如果不存在,那么会将对数据页的修改逻辑写入 Change Buffer 以及 redo log,等到下次读取未修改的数据页到缓冲池中会触发 Merge ,将 Change Buffer 中对应的写操作更新至该数据页。(如果是插入操作且包含唯一索引那么当缓冲池中不存在对应数据页时直接将数据页读取到缓冲池然后判断唯一性,然后再把对数据页的修改逻辑写入 redo log,不会用到 change buffer)
redo log 的作用是保证修改数据不会丢失,因为此时这些写操作是没有更新到磁盘的,先持久化到文件中,如果发生断电异常重启后还可以通过 redo log 来还原写操作来修改到内存缓冲池中的数据,然后再通过缓冲池的数据页更新到磁盘。
Innodb缓冲池(buffer pool)
Innodb缓冲池缓存行的数据、自适应索引、插入缓冲、锁、索引页、数据字典以及其他的一些内部数据结构,所以对于Inndo来说是严重的依赖于缓冲池的,当mysql启动的时候,Innodb引擎会想操作系统申请一块连续的内存空间,然后按照页的大小(默认16kb)来划分出一个个空白页,当磁盘上的页缓存到内存的buffer pool 中会对空页进行填充。
查看和设置缓冲池大小
InnoDB 引擎通过 innodb_buffer_pool_size 变量查看缓冲池的大小,默认大小128M。一般建议设置成可用物理内存的 60%~80%。
show variables like 'innodb_buffer_pool_size' -- 查看(单位字节) set global innodb_buffer_pool_size -- 修改
对于以上的估值需要考虑到物理机器其他软件的内存使用,比如需要考虑到MYSQL实例是否是唯一运行在该物理机上的程序,以及mysql自身内存使用比如日志
如果数据量不是很大的情况,就没有必要为缓冲池分配较大的内存,很大的缓冲区会带来一些不可避免的麻烦,比如预热和关闭的时候。
- 例如数据库启动完成之后需要进行很长时间的数据预热,不过我们可以选择在关闭服务器的时候保存一下buffer pool中的内容,并在服务器启动时将buffer_pool恢复到关闭前的状态,避免数据库刚开始运行的一段时间内业务访问所有请求数据页都需要重新从磁盘上读取,减小数据库重启对系统带来的性能影响。
- 例如如果有很多脏页在缓冲池里,Innodb关闭时可能会花费较长的时间,因为在关闭之前需要把脏页写回数据文件,即使强制进行关闭了重启的时候就必须多做更多的恢复工作。
缓存页的管理
Innodb管理这些缓冲页方式就是使用LRU(Least recently used页面置换算法),这个算法的核心就是淘汰最久未使用的数据,innodb通过链表来实现。
LRU链表:
链表头节点存储最近使用的数据,链表尾部节点存储最久未被使用的数据。淘汰时删除链表尾部节点。
参数:innodb_old_blocks_pct
介绍:老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。
传统的LRU算法(最近最久未使用算法)
常见的手法就是将入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里有两种情况:
1、页已经在缓冲池里面,那就只需要做将其移动到LRU头部位置的动作,而没有页淘汰。
图中管理缓冲池中的LRU长度为10,缓冲了页号为1-7的页。假设要访问的数据在页号为4页中:
页号为4的页本来就在缓冲池中,所以只需要把页号为4的页放入到LRU的头部即可,不需要淘汰页。
2、页不在缓冲池中,除了要做放入LRU头部的动作,还要做淘汰LRU尾部页的动作。
假设要访问的数据在页号为50的页中:
页号为50的页原来不在缓冲池里面,所以需要把页号为50的页放到LRU头部,同时淘汰尾部页号为7的页。
考虑的问题
传统的LRU缓冲池算法十分直观简单易懂,但是MySQL却没有采用,这里涉及到两个问题:
1、预读失效
Innodb预读机制
Innodb在I/O的优化上有个比较重要的特性为预读,预读请求是一个I/O请求,它会异步地在缓冲池中预存多个数据页,且自身预计很快后续请求将访问这些数据页。
数据库在请求数据的时候,会将读请求交由文件系统,放入到请求队列中去。相关进程就会从请求队列中将读请求取出,根据需求到相关数据区(内存、磁盘)去读取数据,取出数据并放入到响应队列中去,最后数据库就会从响应队列中将数据取走,完成一次数据读操作过程。
接着进程继续处理请求队列,(如果数据库是全表扫描的话,数据读请求将会占满请求队列)
判断后面几个数据读请求的数据是否相邻,在根据自身系统IO带宽处理量来进行预读,进行读请求的合并操作,一次性读取多快数据放入响应队列中,然后在被数据库取走。
预读算法
mysql有两种预读算法线性预读(linear read-ahead)和随机预读(randomread-ahead),但是其5.5版本之后默认是使用线性预读禁用随机预读
- 线性预读
线性预读的单位是extend,一个extend中有64个page,线性预读的一个重要参数 innodb_read_ahead_threshold 是指在连续访问多个页面之后,把下一个extend读入到
buffer pool 中,不过预读是一个异步操作,有个限制就是这个参数不能超过64,因为一个
extend最多只能有64个page。
mysql> show variables like 'innodb_read_ahead_threshold'; +-----------------------------+-------+ | Variable_name | Value | +-----------------------------+-------+ | innodb_read_ahead_threshold | 56 | +-----------------------------+-------+ /** innodb_read_ahead_threshold = 56,就是指在连续访问了一个为extend的56个页面之后就把下一个 extend读入到buffer pool中来, **/
- 随机预读
随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中。
预读失败即提前把页放入了缓冲池中,但最终并没有从页中读取数据,称为预读失败。
要优化预读失败就要考虑以下两点,①让预读失败的页,停留在缓冲池LRU里的时间尽可能的短。②让真正被读取的热数据留在缓冲池里的时间尽可能长。
逻辑实现:
- 将LRU分为两部分即新生代【newSublist】和老生代【oldSublist】
- 新生代首尾相连,即新生代的尾(tail)连接着老生代的头(head)
- 新页(被预读的页)加入缓冲池时,只加入到老生代的头部。如果数据被真正的读取即预读成功,才会被加入到新生代的头部,如果数据没有被读取即预读失败,则会比新生代里的热数据页更早地从缓冲池中淘汰。
例如当前整个缓冲池LRU如上图所示,我们分析出如下信息:
- 整个LRU长度是10;
- 前70%是新生代;
- 后30%是老生代;
- 新老生代首尾相连;
现在假设有一个页号为50的新页被预读加入到缓冲池,如下所示:
- 页号为50的新页只会从老生代头部插入,老生代尾部的页会直接被淘汰掉。
- 假设为50的这一页不会被真正读取即预读失败,它将被新生代的数据更早淘汰出缓冲池。
现在假如50这一页立刻被读取到,例如当前请求SQL读取到了这一页的行数据,如下所示:
- 它会被立刻加入到新生代的头部位置。
- 新生代的页会被挤入到老生代的头部位置,此时也不会有页被真正淘汰。
1、缓冲池污染
缓冲池污染就是当系统中执行某一个SQL语句,要批量臊面大量数据时,可能导致把缓冲池中的所有页都替换出去,大量热数据被挤出去,mysql性能就会急剧下降,这种情况就被称之为缓冲池污染。
例如,当前有一个数据量较大的用户表,当执行一个无法命中索引的而最终导致走全表扫描的SQL语句时,就需要访问大量的数据页,即使结果集中可能最终只有少量数据。
而缓冲池要做的就是:
① 将页加到缓冲池中,即插入到老生代头部位置
② 当页预读成功后插入到新生代头部位置
③ 根据SQL语句的where条件进行比较将符合条件的数据加入到结果集中,直到扫描完成所有页数据。
这样下来,大量的数据页都会被加载到新生代的头部中,但是可能只会被访问一次,而真正的热数据被大量挤出
逻辑实现:
参数:innodb_old_blocks_time
介绍:老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件,才会被插入到新生代头部。
mysql缓冲池加入了一个老生代停留时间窗口的机制:
- 假设T= 老生代停留时间窗口
- 插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部
- 只有满足“被访问”且“老生代停留时间”大于T,才会被放入新生代头部
现在假设批量数据扫描,有51、52、53、54、55 等五个页面将要被依次访问,如下所示:
继续假设如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会换出大量热数据:
加入【老生代停留时间窗口】策略后,短时间内被大量加的页,并不会立刻插入新生代头部,而是优先淘汰那些短期内仅被访问了一次页:
而只有在老生代呆的时间足够久且页被访问,停留时间大于T,才会被插入新生代头部:
脏页
即内存页(缓冲池中存储的),对数据做修改时首先更改内存中的数据页,如果没有就会去磁盘中进行IO读写数据页到buffer pool中。由于缓存中的数据跟磁盘中的数据不一致,所以我们就称为“脏页”反之,内存数据写入到m
假设要修改页号为4的索引页,而正好这个页就在缓冲池内:(图1)
- 直接修改缓冲池中的页,一次内存操作。
- 写入redo log日志,一次磁盘顺序写操作。
是否会出现一致性问题呢?不会。
- 读取的时候会命中缓冲池的页
- 缓冲池LRU数据淘汰,会将脏页刷回磁盘
- 数据库异常崩溃,能够从redo log中恢复数据
假设要修改页号为40的索引页,而这个页正好不在缓冲池内:(图2)
- 先把需要为40的索引页,从磁盘加载到缓冲池,一次磁盘随机读操作
- 修改缓冲池中的页,一次内存操作
- 写入redo log,一次磁盘顺序写操作
写缓存(Change Buffer)在5.5之前叫做 插入缓存(insert Buffer),因为只支持插入的缓存,在随后版本又添加了 update、delete,所以改名 change Buffer。因为直接对磁盘进行IO操作会比较耗时,如果我们的程序在高并发的场景,同时某段时间写操作非常多,那么如果直接更新到磁盘上数据库的压力就会非常大,甚至崩溃。为了避免这种情况,可以错开高峰期,让数据在系统空闲时再更新到磁盘,那么该如何实现,Change Buffer就起到这样的作用。
当加入缓冲池优化后,再次来进行图二的操作流程就变为如下了:
- 在写缓冲中记录这个操作,一次内存操作
- 写入redo log,一次磁盘顺序写操作
是否会出现一致性问题呢?不会。
- 数据库异常奔溃,能够从redo log中恢复数据
- 写缓冲不只是一个内存结构,它也会被定期刷盘到写缓冲系统表空间
- 数据读取时,有另外的流程,将数据合并到缓冲池
在写操作语句进来时,首先会判断缓冲池是否存在这条写操作对应的数据页,如果存在直接更新数据页中对应的数据,然后将对数据页的操作逻辑记入 redo log;如果不存在,那么会将对数据页的修改逻辑写入 Change Buffer 以及 redo log,等到下次读取未修改的数据页到缓冲池中会触发 Merge ,将 Change Buffer 中对应的写操作更新至该数据页。(如果是插入操作且包含唯一索引那么当缓冲池中不存在对应数据页时直接将数据页读取到缓冲池然后判断唯一性,然后再把对数据页的修改逻辑写入 redo log,不会用到 change buffer)
redo log 的作用是保证修改数据不会丢失,因为此时这些写操作是没有更新到磁盘的,先持久化到文件中,如果发生断电异常重启后还可以通过 redo log 来还原写操作来修改到内存缓冲池中的数据,然后再通过缓冲池的数据页更新到磁盘。