InnoDB Buffer Pool 浅析b
阿里云查看Buffer PoolBuffer Pool简介主要组件介绍Buffer Pool Instance:Buffer chunks:页链表:Mutex:Page_hash:Buffer Pool代码分析Buffer Pool 数据结构Buffer Pool 初始化Buffer Pool 读取和写入页链表的访问:图解介绍buffer pool内部组成页数据free链表flush链表LRU链表多buffer pool实例自适应hash总结
阿里云
查看Buffer Pool
Buffer Pool简介
InnoDB中的数据访问是以Page为单位的,每个Page的大小默认为16KB,Buffer Pool是用来管理和缓存这些Page的。InnoDB将一块连续的内存大小划分给Buffer Pool来使用,并将其划分为多个Buffer Pool Instance来更好地管理这块内存,每个Instance的大小都是相等的,通过算法保证一个Page只会在一个特定的Instance中,划分为多个Instance的模式提升了Buffer Pool的并发性能。
在每一个Buffer Pool Instance中,实际都会维护一个自己的Buffer Pool模块,InnoDB通过16KB Page的方式将数据从文件中读取到Buffer Pool中,并通过一个LRU List来缓存这些Page,经常访问的Page在LRU List的前面,不经常访问的Page在后面。InnoDB访问一个Page时,首先会从Buffer Pool中获取,如果未找到,则会访问数据文件,读取到Page,并将其put到LRU List中,当一个Instance的Buffer Pool中没有可用的空闲Page时,会对LRU List中的Page进行淘汰。
由于Buffer Pool中夹杂了很多Page压缩的逻辑,即将实际的16KB Page压缩为8KB、4KB、2KB、1KB,这块逻辑暂时先跳过不去做分析,我们先按照默认Page就是16KB的逻辑来梳理Buffer Pool相关的逻辑。
主要组件介绍
Buffer Pool Instance:
InnoDB启动时会加载配置srv_buf_pool_size和srv_buf_pool_instances,分别是Buffer Pool总大小和需要划分的Instance数量,当srv_buf_pool_size小于1G时,srv_buf_pool_instances会被重置为1,单个Buffer Pool Instance的大小计算规则为:size=srv_buf_pool_size/srv_buf_pool_instances,每个Buffer Pool Instance的大小均相等。在Mysql 8.0中,最大支持64个Buffer Pool Instance,实际Instance在初始化时,为了加快分配速度,会根据运行环境进行调整并行初始化的数量,详细流程见Buffer Pool初始化。
在每个Buffer Pool Instance中都有包含自己的锁,mutex,Buffer chunks,各个页链表(如下面所介绍),每个Instance之间都是独立的,支持多线程并发访问,且一个page只会被存放在一个固定的Instance中,后续会详细介绍这个算法。
在每个Buffer Pool Instance中还包含一个page_hash的hash table,通过这个page_hash能快速找到LRU List中的page,避免扫描整个LRU List,极大提升了Page的访问效率。
Buffer chunks:
Buffer chunks是每个Buffer Pool Instance中实际的物理存储块数组,一个Buffer Pool Instance中有一个或多个chunk,每个chunk的大小默认为128MB,最小为1MB,且这个值在8.0中时可以动态调整生效的。每个Buffer chunk中包含一个buf_block_t的blocks数组(即Page),Buffer chunk主要存储数据页和数据页控制体,blocks数组中的每个buf_block_t是一个数据页控制体,其中包含了一个指向具体数据页的*frame指针,以及具体的控制体buf_page_t,后面在数据结构中详细阐述他们的关系。
页链表:
以下所有的链表中的每个节点都是数据页控制体(buf_page_t)。
- Free List:如其名,Free List中存放的都是未曾使用的空闲Page,InnoDB需要Page时从Free List中获取,如果Free List为空,即没有任何空闲Page,则会从LRU List和Flush List中通过淘汰旧Page和Flush脏Page来回收Page。在InnoDB初始化时,会将Buffer chunks中的所有Page加入到Free List中。
- LRU List:所有从数据文件中新读取进来的Page都会缓存在LRU List,并通过LRU策略对这些Page进行管理。LRU List实际划分为Young和Old两个部分,其中Young区保存的是较热的数据,Old区保存的是刚从数据文件中读取出来的数据,如果LRU List的长度小于512,则不会将其拆分为Young和Old区。当InnoDB读取Page时,首先会从当前Buffer Pool Instance的page_hash查找,并分为三种情况来处理:
- 如果在page_hash找到,即Page在LRU List中,则会判断Page是在Old区还是Young区,如果是在Old区,在读取完Page后会把它添加到Young区的链表头部
- 如果在page_hash找到,并且Page在Young区,需要判断Page所在Young区的位置,只有Page处于Young区总长度大约1/4的位置之后,才会将其添加到Young区的链表头部
- 如果未能在page_hash找到,则需要去数据文件中读取Page,并将其添加到Old区的头部
LRU List采用非常精细的LRU淘汰策略来管理Page,并且用以上机制避免了频繁对LRU 链表的调整。
- Flush List:所有被修改过且还没来得及被flush到磁盘上的Page(脏页),都会被保存在这个链表中。所有保存在Flush List上的数据都会在LRU List中,但在LRU List中的数据不一定都在Flush List中。在Flush List上的每个Page都会保存其最早修改的lsn,即oldest_modification,虽然一个Page可能被修改多次,但只记录最早的修改。Flush List上的Page会按照其各自的oldest_modification进行降序排序,链表尾部保存oldest_modification最小的Page,在需要从Flush List中回收Page时,从尾部开始回收。
Mutex:
为保证各个页链表访问时的互斥,Buffer Pool中提供了对几个List的Mutex,如LRU_list_mutex用来保护LRU List的访问,free_list_mutex用来保护Free List的访问,flush_list_mutex用来保护Flush List的访问。
Page_hash:
在每个Buffer Pool Instance中都会包含一个独立的Page_hash,其作用主要是为了避免对LRU List的全链表扫描,通过使用space_id和page_no就能快速找到已经被读入Buffer Pool的Page。
Buffer Pool代码分析
初步了解了Buffer Pool在InnoDB中扮演的角色后,接下来我们从以下几个方面来探讨一下在Mysql 8.0中InnoDB Buffer Pool的具体实现:
- Buffer Pool 数据结构
- Buffer Pool 初始化
- Buffer Pool 读取和写入
- 页链表的访问
Buffer Pool 数据结构
Buffer Pool主要包含三个核心的数据结构buf_pool_t、buf_block_t和buf_page_t,其定义都在include/buf0buf.h中,分别看一下其具体实现:
主要的三个数据结构就都已经罗列在上面了,还有个比较重要的buf_page_state,这是一个枚举类型,标识了每个Page所处的状态,在读取和访问时都会对应不同的状态转换,接下来我们简单看一下:
在不考虑压缩Page的情况下,buf_page_state的状态转换一般为:
Buffer Pool 初始化
要说起Buffer Pool的初始化,就不得不先提到InnoDB的启动流程,我们首先从srv/srv0start.cc的srv_start函数看起,这里是整个InnoDB启动的地方。
在buf0buf.cc::buf_pool_create()函数中会完成对Buffer Pool Instance的初始化,主要是Buffer Chunks的初始化,即调用buf_chunk_init()函数:
Buffer Pool 读取和写入
Buffer Pool的读取逻辑和写入逻辑是混合在一起的,InnoDB需要访问一个Page时,必须要通过Buffer Pool进行获取,主要需要以下几个步骤:
- 获取Page对应的Buffer Pool Instance
- 从对应的Buffer Pool Instance的Page_hash中查找是否存在该Page,如存在,直接返回该Page的地址,并可能需要修改LRU List中的数据
- 如果未能查找到,则需要读取数据文件,并从Free List中申请新的Page将其添加到LRU List中
接下来我们围绕这个主题逻辑,来分析一下Buffer Pool 读取和写入流程,实际读取Page的函数为buf0buf.cc::buf_page_get_gen():
其中未在Page_hash中找到Page,且mode不为BUF_GET_IF_IN_POOL时,需要调用buf0rea.cc::buf_read_page()区文件中读取Page。
至此,Buffer Pool的读写操作大致流程就分析完了,但细节性的页链表的访问,如LRU List和Flush List的管理和淘汰,以及关于随机预读和线性预读操作的部分,还需要分析一下。我们先从页链表的访问看起:
页链表的访问:
获取一个新的Page,并对其完成初始化工作,以便于后续的fil_io(…)将从数据文件中读取到的数据填充到该Page,其中会涉及到从Free List中获取空闲Page,如果无空闲Page则需要对LRU List和Flush List进行淘汰操作,我们先从buf_page_init_for_read(…)函数看起:
当Buffer Pool Instance去获取一个空闲Page时,大多数情况下都会直接从Free List中获取一个空闲Page直接返回,除非Free List是空的,则需要去进行回收LRU List和Flush List中的Page,在进行查找和回收Page时,在buf_LRU_get_free_block()函数中,定义了一个n_iterations,这个参数用于标识是第几次进行迭代获取空闲Page,当第一次来获取Page时,n_iterations为0,总共分为三种情况作处理,具体如下:
- n_iterations = 0:
- 直接调用buf_LRU_get_free_only()函数从Free List中获取Page;
- 如果未Free List中获取到空闲Page,且try_LRU_scan设置为True,则开始扫描LRU List尾部的BUF_LRU_SEARCH_SCAN_THRESHOLD(默认为100)数量个Page,找到一个可以被回收的Page(即没有事务在使用这个Page),调用buf_LRU_free_page()函数回收Page,并将其加入到Free List中,然后再调用buf_LRU_get_free_only()函数从Free List中获取Page;
- 如果在上一步操作中还是未找到空闲Page,则尝试从LRU List的尾部Flush一个Page到数据文件中,调用buf_flush_single_page_from_LRU()来完成对Page的Flush,并将其加入到Free List中,然后再调用buf_LRU_get_free_only()函数从Free List中获取Page;
- 如果还是未找到空闲Page,则将n_iterations++,并重复1-3的步骤从最开始继续循环获取Page。
- n_iterations = 1:
此时和n_iterations = 0的执行流程几乎是一样的,只是在扫描LRU List时是扫描整个链表而不是只扫描尾部的一部分了,其余流程完全一致。如果未找到则将n_iterations++,并重复n_iterations = 0中1-3的步骤从最开始继续循环获取Page。
- n_iterations > 1:
此时和n_iterations = 1流程完全一致,只是会在在flush之前每次sleep 10ms。如果还是找不到空闲Page,则继续将n_iterations++,并重复n_iterations = 0中1-3的步骤从最开始继续循环获取Page。
当n_iterations > 20时,会打印一条频繁获取不到空闲Page的log。
到此,Buffer Pool的介绍就暂时告一段落了,后续会继续尝试从源码的角度来剖析压缩Page相关的逻辑,敬请关注。
图解
介绍
数据通常都是持久化在磁盘中的,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。作者:想打游戏的程序猿链接:https://juejin.cn/post/7413196978601295899来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Loading...