Innodb之buffer pool 图文详解
介绍
数据通常都是持久化在磁盘中的,innodb如果在处理客户端的请求时直接访问磁盘,无论是IO压力还是响应速度都是无法接受的;所以innodb在处理请求时,如果需要某个页的数据就会把整个页都加载到缓存中,数据页会长时间待在缓存中,根据一定策略进行淘汰,后续如果在缓存中的数据就不需要再次加载数据页了,这样即可提高响应时间又可以节省磁盘IO.
buffer pool
上述介绍中我们有提到一个缓存,这个缓存就指的是buffer pool
,通过innodb_buffer_pool_size
进行设置它的大小,默认为128MB,最小为5MB;可以根据各自线上机器的情况来设置它的大小,设置几个G甚至上百个G都是合理的。
内部组成
buffer pool中包含数据页、索引页、change buffer、自适应hash等内容;数据页、索引页在buffer pool中占用了大部分,不能简单的认为缓冲池中只有数据页和索引页;change buffer在较老的版本中叫insert buffer,后面对其进行了升级形成了现在的change buffer;自适应hash可以方便我们快速查询数据;锁信息、数据字典都是占用比较小的一部分;以上就是buffer pool的内部组成。
页数据
数据页、索引页数据在mysql启动的时候,会直接给申请一块连续的内存空间;如图:
上图中的缓冲页对应的就是磁盘中的数据,默认每个页大小为16KB,并且innodb为每个缓冲页都创建了一些控制块,每个控制块占用大小是800字节左右,需要额外付出百分之5的内存,它记录页所属的表空间编号、页号、缓存页在buffer pool中的地址、链表节点信息等。内存中间可能会有碎片进行对齐。
注意:这里只有缓冲页占用的空间是计算在buffer pool中的。
free链表
根据上面的图可以了解到,buffer pool中有一堆缓冲页,但innodb从磁盘中读取数据页时,由于不能直接知道哪些缓冲页是空闲的、哪些页已经被使用了,导致了不知道把要读取的数据页存放到哪里;此时就引入了一个free链表的概念。如图:
上图中可以看到free链表靠一个free节点连接到控制块中,其中free头节点仅占用40字节空间,但它也不计算在buffer pool中;有了这个free链表后每当需要从磁盘中加载一个页到buffer pool中时就可以从free链表上取一个控制块,把控制块所需信息填充上,同时把从磁盘上加载的数据放到对应的缓冲页上,并把该控制块从free链表中移除。此时就把磁盘中的页加载到内存中了,后续查询数据时就会优先查询该内存页,但每次查询时没办法立刻知道该页是在内存中还是磁盘中,上述操作后还会把这个页信息放到一个散列表中,以(表空间号+页号)作为key,以控制块地址作为value。
flush链表
上述介绍了读数据时通过优先读取内存页可以提高我们的响应速度以及节省磁盘io,那么如果是写数据呢?其实在innodb中,更改也会优先在内存中更改,在后续会根据一定规则(会在后续redolog文章中详细介绍)进行刷盘,在刷盘时只需要刷被更改的缓冲页即可,那么哪些缓存页被更改了innodb是不知道的,此时innodb就设计了flush链表,它和free链表几乎一样,如图:
当需要刷盘时会从flush链表中拿出一部分控制块对应的缓冲页进行刷盘,刷盘后控制块会从flush链表中移除,并放到free链表中。
LRU链表
由于buffer pool的内存区域是有限的,如果当来不及刷盘时内存就不够用了;此时innodb采用了LRU淘汰策略,标准的LRU算法:
- 如果数据页已经被加载到buffer pool中了,则直接把对应的控制块移动到LRU链表的头部;
- 如果数据页不在buffer pool中,则从磁盘中加载到buffer pool中,并把其对应的控制块放到LRU头部;此时内存空间已经满了的话,就会从链表中移除最后一个内存页。
但直接采用lru方案,在内存较小或者临时一次性加载到内存中的页过多时会把真正的热点数据刷掉。如预读和全表扫描。
- 线性预读:如果顺序访问的某个区的页面数超过
innodb_read_ahead_threshold
(默认值为56)就会触发一次异步预加载下一个区中的全部页到内存中; - 随机预读:如果某个区的13个连续的页都被加载到young区前1/4的位置中,会触发一次异步预加载本区中的全部页到内存中;
- 全表扫描:把整张表的数据都加载到内存中。
为了解决上述问题,innodb对这个淘汰策略做了一点改变。如图:
innodb根据innodb_old_blocks_pct
(默认37)参数把整个lru分成一定比例,具体的淘汰策略:
- 当数据页第一次加载进来时会先放到old head处,当链表满时会把old tail刷盘并从链表中移除。
- 当再次使用一个数据页时,并且该页在old区,会先判断在old区的停留时间是否超过
innodb_old_blocks_time
(默认1000ms),如果超过则把该数据页移动到young head处,反之移动到old head处。 - 当再次使用一个数据页时,并且该页young区为了节省移动的操作,会判断该缓冲页是否在young区前1/4中,如果在就不进行移动,如果不在则移动到young head处。
多buffer pool实例
对于buffer pool设置很大内存的实例,由于操作各种链表都需要进行加锁这样就比较影响整体的性能,为了提高并发处理能力,可以通过innodb_buffer_pool_instances
来设置buffer pool的实例个数。在mysql5.7.5版本中,会以chunk为单位向系统申请内存空间,每个buffer pool中包含了N个chunk。如图:
可以通过innodb_buffer_pool_chunk_size
(默认128M)来设置chunk的大小,只能在启动前设置好,启动后可以更改innodb_buffer_pool_size
的大小,但必须时innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的整数倍。
自适应hash
对于b+数来讲,整体的查询时间复杂度为O(logN),而innodb为了进一步提升性能,引入了自适应hash,对热点数据可以做到O(1)的时间复杂度就可以定位到数据在具体哪个页中。
innodb会自动根据访问频率和模式自动为某些热点页在内存中建立自适应哈希索引,规则:
- 模式:例如有一个联合索引(a,b);查询条件
where a = xxx
与where a = xxx and b = xxx
这就属于两种模式,如果交叉使用这两种查询,不会为其建立自适应哈希索引;- 频率:使用一种默认访问的次数大于Math.min(100,页中数据/16)。
根据官方数据,启动自适应哈希索引读写速度可以提升2倍,辅助索引的连接性能可以提升5倍。
总结
通过上述的介绍,希望能帮助大家对buffer pool有一个基础的了解,想进一步深入了解可以通过执行show engine innodb status
观察下各种参数,通过对每个参数的细致研究可以全方面的掌握buffer pool。
创作不易,觉得文章写得不错帮忙点个赞吧!如转载请标明出处~
来源:juejin.cn/post/7413196978601295899