看了两遍,过段时间还是会忘,索性自己手打一遍,加深记忆
整理内容来自:
掘金 和 狸猫技术窝
BufferPool
当我们对数据库中的数据进行修改时,不可能每次都将数据从磁盘加载到内存,改完后在写回磁盘。所以当我们要访问某个页的数据时,就会把完整的页的数据加载到内存缓存起来。这块缓存就是BufferPool。
- 如何调整大小?
[server]
// 设置为256M
innodb_buffer_pool_size = 268435456
如何划分BufferPool?
为了管理缓存页,Innodb会为每个缓存页创建一些控制信息,包括表空间编号、页号、缓存页在BufferPool中的地址、链表节点信息、一些锁信息以及LSN信息等。这些控制信息占缓存页的5%,也就是808字节。所以在申请BufferPool时,会比实际大5%。
BufferPool
如何知道BufferPool中哪些缓存页可用?
当数据库执行我们的CRUD命令时,会有很多的数据,不断缓存到BufferPool中,那我们如何确定哪些可以,哪些不可用?
-
free链表
它是一个双向链表,里面记录着空闲的控制块指针。
free链表
如何确定BufferPool是否缓存此记录页?
- 缓存页的哈希处理
用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
如何知道BufferPool哪里与磁盘不一致?
当修改了BufferPool中某个缓存页的数据后,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页。假设每次修改完之后,就把当前数据所在的页刷新到磁盘,但是频繁的磁盘写操作会严重影响性能。所以我们还需要记录一下这些被改变的缓存页(脏页)。此时就出现了flush链表
,类似free链表
。
如何处理BufferPool不够用的场景?
普通LRU链表
最简单的办法就是再创建一个链表,当我们使用到某个缓存页,就把该缓存页调整到链表的头部,这样链表尾部就是最近最少使用的缓存页。所以当BufferPool中的空闲缓存页使用完时,到链表的尾部找些缓存页淘汰就行了。这个链表就是LRU链表。
普通LRU链表存在的问题
- 预读
- 线性预读
根系统变量innodb_read_ahead_threshold
有关,如果顺序访问了某个区(extent)的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到Buffer Pool的请求,注意异步读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。这个innodb_read_ahead_threshold
系统变量的值默认是56,我们可以在服务器启动时通过启动参数或者服务器运行过程中直接调整该系统变量的值,不过它是一个全局变量,注意使用SET GLOBAL命令来修改。 - 随机预读
如果BufferPool中已经缓存了某个区的13个连续的页面(其实还要求这13个页面是非常热的页面,所谓的非常热,指的是这些页面在整个young区域的头1/4处),不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到BufferPool的请求。这个机制和innodb_random_read_ahead
系统遍历有关,它的默认值为OFF,也就意味着InnoDB并不会默认开启随机预读的功能,如果我们想开启该功能,可以通过修改启动参数或者直接使用SET GLOBAL命令把该变量的值设置为ON。
- 全表扫描
select * from user
直接导致BufferPool中的缓存全部失效。
基于冷热数据分离的LRU链表
针对普通的LRU链表无法解决的问题,Innodb设计者把普通LRU链表做了改进,按照一定的比例(这个比例是由innodb_old_blocks_pct
来设定的,默认冷数据占37%)分为两部分。一部分存储使用频率高的数据叫热数据(yong区域)、一部分存储使用频率低的数据叫冷数据(old区域)。
改进之后,当磁盘上的某个页面在初次加载到BufferPool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。
冷数据成为热数据的条件
在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。这个时间间隔由系统变量innodb_old_blocks_time
控制,默认1s。意思就是如果第一次和最后一次访问该页面的时间间隔小于1s就不会被加入到yong区域。
综上所述,正是因为将LRU链表划分为young和old区域这两个部分,又添加了innodb_old_blocks_time
这个系统变量,才使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,因为用不到的预读页面以及全表扫描的页面都只会被放到old区域,而不影响young区域中的缓存页。
进一步优化基于冷热数据分离的LRU链表
对于young区域的缓存页来说,我们每次访问一个缓存页就要把它移动到LRU链表的头部,这样开销太大,毕竟在young区域的缓存页都是热点数据,为了解决这个问题其实我们还可以提出一些优化策略,比如只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,这样就可以降低调整LRU链表的频率,从而提升性能(也就是说如果某个缓存页对应的节点在young区域的1/4中,再次访问该缓存页时也不会将其移动到LRU链表头部)。
何时BufferPool的数据会刷新到磁盘?
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。
- 从LRU链表的冷数据中刷新一部分页面到磁盘。
后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth
来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU。 - 从flush链表中刷新一部分页面到磁盘。
后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST。
如何优化系统的BufferPool?
多个线程同时访问BufferPool时,不应该并发的去修改各个链表等内容,这样会出现问题。所以,访问BufferPool中的各种链表都需要加锁处理啥的,但单一的BufferPool可能会影响请求的处理速度。所以在BufferPool特别大的时候,我们可以把它们拆分成若干个小的BufferPool,每个BufferPool都称为一个实例,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,独立的吧啦吧啦,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。我们可以在服务器启动的时候通过设置innodb_buffer_pool_instances
的值来修改BufferPool实例的个数。但是要注意如果BufferPool分配的内存小于1G,那么innodb_buffer_pool_instances
无论怎么设置InnoDB都是默认为1。
如何在系统运行期间调整BufferPool?
在MySQL 5.7.5之前,BufferPool的大小只能在服务器启动时通过配置innodb_buffer_pool_size
启动参数来调整大小。不过在5.7.5以及之后的版本中支持了在服务器运行过程中调整BufferPool大小的功能。
如何调整呢?假设调整BufferPool的大小就是每次当我们要重新调整BufferPool大小时,都需要重新向操作系统申请一块连续的内存空间,然后将旧的BufferPool中的内容复制到这一块新空间。这么做合适吗?很不合适,因为这是极其耗时的。
所以设计者决定不再一次性为某个BufferPool实例向操作系统申请一大片连续的内存空间,而是以一个所谓的chunk为单位向操作系统申请空间。也就是说一个BufferPool实例其实是由若干个chunk组成的,一个chunk就代表一片连续的内存空间,里边儿包含了若干缓存页与其对应的控制块。而chunk的大小是由innodb_buffer_pool_chunk_size
的值决定的,默认128M,并且只能在服务器启动时指定。
注意事项
-
innodb_buffer_pool_size
必须是innodb_buffer_pool_chunk_size
×innodb_buffer_pool_instances
的倍数(这主要是想保证每一个Buffer Pool实例中包含的chunk数量相同)。
假设我们指定的innodb_buffer_pool_chunk_size
的值是128M,innodb_buffer_pool_instances
的值是16,那么这两个值的乘积就是2G,也就是说innodb_buffer_pool_size
的值必须是2G或者2G的整数倍。
如果我们指定的innodb_buffer_pool_size
大于2G并且不是2G的整数倍,那么服务器会自动的把innodb_buffer_pool_size
的值调整为2G的整数倍 - 如果在服务器启动时,
innodb_buffer_pool_chunk_size
×innodb_buffer_pool_instances
的值已经大于innodb_buffer_pool_size
的值,那么innodb_buffer_pool_chunk_size
的值会被服务器自动设置为innodb_buffer_pool_size
/innodb_buffer_pool_instances
的值。
比方说我们在启动服务器时指定的innodb_buffer_pool_size
的值为2G,innodb_buffer_pool_instances
的值为16,innodb_buffer_pool_chunk_size
的值为256M。服务器会自动将innodb_buffer_pool_chunk_size
的值调整为128M。
网友评论