【MySQL】InnoDB 内存管理机制 --- Buffer Pool

时间:2021-11-13 03:47:58

InnoDB Buffer Pool

  • 是一块连续的内存,用来存储访问过的数据页面
  • innodb_buffer_pool_size 参数用来定义 innodb 的 buffer pool 的大小
  • 是 MySQL 中拥有最大的内存的模块
  • Innodb 中,数据的访问是按照页/块(默认为16KB)的方式从数据文件中读取到 buffer pool中,然后在内存中用同样大小的内存空间做一个映射

为了提高访问速度(也就是尽可能多地把数据文件中的页/块放到 buffer pool 中),MySQL 预先就分配/准备了许多这样的空间,为的就是与MySQL数据文件中的页做交换,来把数据文件中的页放到事先准备好的内存中。

buffer pool 按照最少使用算法(LRU),来管理 buffer pool ,去搜了一下这个算法:

[ LRU(Least recently used),缓存淘汰算法。比较简单的一个算法,就是根据“近期”的数据访问记录来对buffer中的数据进行淘汰。核心思想:“如果某个数据最近被访问过,则将来被访问的概率更高”。

最常见的是使用链表保存缓存数据,这也是 innodb buffer pool 的管理方式。因此,最常访问的放在链表头,最不常访问的放在链表尾。

【命中率】

当存在热点数据的时候,LRU的效果很好;但是偶然性或者周期性的访问会导致LRU的命中率急剧下降。]

当 buffer pool 满了的时候,就用新的数据代替 buffer pool 末尾的数据。

root@localhost][(none)][02:40:28]> show engine innodb status\

【MySQL】InnoDB 内存管理机制 --- Buffer Pool【MySQL】InnoDB 内存管理机制 --- Buffer Pool

InnoDB为缓冲区和控制结构保留了额外的内存,因此总分配的空间大约比指定的缓冲池大小大10%。

【MySQL】InnoDB 内存管理机制 --- Buffer Pool【MySQL】InnoDB 内存管理机制 --- Buffer Pool

Buffer Pool 有两个区域,一个是 sublist_of_new_block 区域(热数据),一个是 sublist_of_old_clocks 区域(不经常访问的)。当访问的时候,如果没有相应数据则从磁盘中先读入数据到 sublist of old blocks 区域,然后移动到 sublist of new blocks 区域(实际上所谓的 热数据区域和 old 数据区域都是在 LRU list 里面,只是 sublist 0f new blocks 在链表的前面,sublist of old blocks 在链表的尾部)。

PS:

Pages made young:从 old 区移动到 new 区有多少个页

Pages made not young:因为 innodb_old_blocks_time 的设置而导致页没有从 old 部分启动到 new 部分的操作。

Buffer pool hit rate:表示缓冲池的命中率,通常这个值不应该小于95%,如果小于95%,则应该看看是不是由于全表扫描而导致 LRU 列表有污染。

同时,show engine innodb status 显示的不是当前的实时状态。

为了避免某些全表扫描的数据进入 sublist of new blocks 区域,从5.5.x 开始,innodb_old_blocks_pct 参数可以控制进入 sublist of old blocks 区域的百分比,默认是37%(即 sublist of old blocks 占整个 LRU list 的百分比,如上图所示:189598/514065=36.88%,接近37%)。---当全表扫描或者 dump 时,可以将 innodb_old_blocks_pct 设置的小些。

【MySQL】InnoDB 内存管理机制 --- Buffer Pool【MySQL】InnoDB 内存管理机制 --- Buffer Pool

这个过程还会设计 innodb_old_blocks_time 参数,比如 sublist of old blocks 中的某个数据块被访问到了,他就会等待 innodb_old_blocks_time (毫秒),然后再移动到 sublist of new blocks 中。一般不会去动这个参数

【MySQL】InnoDB 内存管理机制 --- Buffer Pool【MySQL】InnoDB 内存管理机制 --- Buffer Pool

在5.7.4 之前,如果想要更改 innodb_buffer_pool_size 的大小,只能 kill 掉进程才行,但是5.7.5开始,不需要停止进程就可以动态更改 innodb_buffer_pool_size 的大小。

同时,如果 innodb_buffer_pool_size 的大小超过1G,则需要通过调整 innodb_buffer_pool_instances=N(即开启多个内存缓冲池,每个缓冲池的大小相同,把需要缓冲的数据 hash 到不同的缓冲池中,这样就可以进行并行的内存读写),把 buffer pool 分成若干个 instance 可以提高 MySQL 处理请求的并发能力,因为 buffer pool 通过链表管理页,同时为了保护页,需要在存取的时候对链表加锁,因此多线程下,并发去读写 buffer pool 需要锁的竞争和等待。

因此,修改为多个 instance ,每个 instance 管理自己的内存和链表,可以提升请求的并发能力。

Buffer Pool 实现原理

server启动后也会启动所有的内嵌引擎,Innodb 启动后会通过 buf_pool_init 初始化所有的子系统(从命名来看貌似只是初始化buffer pool的,不确定是否里面有调用其他的函数来初始化其他的系统)。

InnoDB用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 InnoDB 的索引块,而且也能用来缓存 InnoDB 的数据块,这于 MyISAM 不同。

Buffer Pool 逻辑上由 freelist、flush list 和 LRU list 组成。free list 是空闲缓存块列表,由下面说的 - FREE链表 来管理;flush list 是需要刷新到磁盘的缓存块列表,由下面所说的 flush_list链表 来管理;LRU list 是 InnoDB 正在使用的缓存块,也是 Bffer Pool的核心。

在代码中,Buffer Pool 用 buf_pool_t 结构体来描述,也是管理 buffer_pool 的核心工具,包含四个部分:

- FREE链表:存储这个实例中所有空闲的页面

- flush_list链表:存储所有被修改过且需要刷到文件中的页面

- mutex:保护实例,因为同一时刻,只能被一个线程访问

- chunks:存储第一个页面的物理地址(因为 buffer_pool 中的页面都是放在一块连续的内存中,即顺序存储,所以知道了第一个页面的地址指针,就可以通过这个指针访问所有的其他页面)

上面说的 FREE链表 和 lush_list链表 又是用来管理的 buf_page_t 结构体的,而 buf_page_t 结构体又被 buf_block_t 所管理。buf_page_t 和 buf_block_t 是一一对应的,都对应 Buffer Pool 中的一个 Page ,只是 buf_page_t 是逻辑的,而 buf_block_t 包含一部分物理的概念,比如这个页面的首地址指针 frame等。

初始化一个 Buffer Pool 的实例空间的函数是 buf_chunk_init。一个 Buffer Pool 实例空间按数据结构来看分为两部分:buf_block_t(buffer pool中页面控制头结构信息,每一个控制头信息管理一物理页面,这些控制头信息的存储占用了部分空间,所以 innodb_buffer_pool_size 总是比 innodb_buffer_pool_bytes_data 大。);另外一部分就是存储 page 的,由 buf_page_t 来管理。

【MySQL】InnoDB 内存管理机制 --- Buffer Pool【MySQL】InnoDB 内存管理机制 --- Buffer Pool

Buffer Pool 初始化过程:Buffer Pool 页面(buf_page_t)在这个实例池中从后向前分配 page,每次分配一个页面,同时控制结构(buf_block_t)从前向后分配来和刚刚分配的页面一一对应(也就是上文说的 buf_block_t 与 buf_page_t 一一对应)。这样前后同时分配也会导致一个问题:因为 block 和 page 是成对的,所以很大可能也很正常地就会“剩下”一些空间(不足以再分配一对 block 与 page)。

buf_pool_t 结构体用来管理整个 buffer pool 实例,而 buf_block_t 用来管理页(buf_page_t),buf_block_t 主要包括一下四个部分:

其对应的页面的 frame

其所对应的 buf_page_t 的信息(所属表空间的ID号、页面号等)

保护这个页面的互斥量mutex()

访问页面时对这个页面上的 lock(read/write)等

以上,初始化页面完成后,这些页面的状态还是未使用(buf_block_not_used),因此将页面加入到 -FREE链表 中,以供使用。至此,缓冲池的一个实例就初始化完成了,其余实例初始化过程相同。

对于整个 Buffer Pool 而言,各个实例之间是完全独立的,相互之间没有任何关系,单独申请、单独管理、单独刷盘。

在具体使用中,针对同的页面,当一条 SQL 从客户端发往服务端时,会通过一个 HASH 算法,来映射到一个具体的实例中。

以上,就是整个 Buffer Pool 多实例的管理机制,通过多实例,可以减少系统运行过程中不同页面之间一些操作的相互影响,从而很好地解决了由于页面之间的资源争抢导致的性能低下问题。