文章目录
InnoDB Buffer Pool
在前面的文章中,通过对读写操作的调试,大致梳理了执行这些SQL语句过程中主要调用的一些函数,但并没有提及直接操作磁盘的相关函数。由于磁盘相对于处理器的速度较慢,因此数据库会在内存中维护一个缓存池。在InnoDB存储引擎中,与Buffer Pool 相关的代码主要在storage/innobase/buf
目录下。一个具备最基本功能的Buffer Pool 包含一个保存大量页面的内存空间以及记录这些内存页面状态的控制变量,比如记录空闲页面的链表、用于页面替换的LRU。而InnoDB中实现的Buffer Pool 则具有更多的功能,包括double write buffer、预读预写、压缩页面等。
接下来首先从源码文件的主要结构体出发,梳理一下Buffer Pool的内存组织方式以及相关的一些主要函数。之后通过一个project 实现一个具备基本功能的Buffer Pool。
源码组织
在MySQL的源码目录storage/innobase/buf
以及storage/innobase/include
目录下包含了Buffer Pool实现的源码文件。
buf0buddy
:用于压缩页面管理的伙伴内存管理系统buf0buf
: Buffer Pool 的核心代码,包含结构体buf_pool_t
、控制块、空闲块链表、预读等相关结构体与函数buf0checksum
:数据落盘时的校验码计算buf0dblwr
:doule write buffer,用于解决宕机导致页面写入不完整问题buf0dump
:Buffer Pool 的load/dump操作,将页面的space id 和 page no 写入外部文件以及加载,用于数据库重启时状态的快速恢复buf0flu
:包含flush list,用于处理将脏页刷到磁盘上buf0rea
:最底层的文件读写/预读相关的操作
Buffer Pool 核心数据结构
在源码文件storage/innobase/buf/buf0buf.cc
中有两百行左右的注释,总体上介绍了Buffer Pool实现包含的主要结构以及对应的功能。
buf_pool_t
InnoDB中Buffer Pools保存在一个动态数组中,extern buf_pool_t* buf_pool_ptr; /*!< The buffer pools of the database */
,该数组长度的最大值通过宏定义 #define MAX_BUFFER_POOLS (1 << MAX_BUFFER_POOLS_BITS)
指定,其中MAX_BUFFER_POOLS_BITS
的默认值为 6。
buf_pool_t 中主要的变量包括:
ib_mutex_t mutex // buffer pool 数组中当前位置的互斥量
uint instance_no // buffer pool 编号
ulint n_chunks // 当前buffer pool 包含的chunks 个数
buf_chunk_t *chunks // 保存内存中的frames 以及对应的控制块,内存中的frame 对应磁盘上的页面
ulint curr_size // buffer pool 中当前保存的页面个数
hash_table_t *page_hash // 通过 (table space )space id 以及offset 定位页面
hash_table_t *zip_hash // 用于定位分配给buddy的内存页面
/*========================*/
ib_mutex_t flush_list_mutex // 用于控制flush list访问的互斥量
const buf_page_t *flush_list_hp // hazard pointer,为了提升刷脏页的效率
/*==========================*/
UT_LIST_BASE_NODE_T(buf_page_t) free // 空闲块链表
UT_LIST_BASE_NODE_T(buf_page_t) LRU // LRU 链表节点
buf_page_t *LRU_old // 指向LRU链表old 部分的指针,如果LRU的长度小于 BUF_LRU_OLD_MIN_LEN,则该指针为NULL
每个Buffer Pool 都有一个互斥量控制访问,其中chunks 保存内存页面以及对应的控制块,内存中的页面数据与磁盘上的对应,不过控制块上的信息并不需要写入到磁盘上面。page_hash
用于通过space id 以及页面偏移快速定位到内存中的页面,同样对于负责管理压缩页面的buddy 系统也有一个zip_hash
。
Buffer Pool 中还包含了刷脏页用的flush list
以及用于页面替换的 LRU。其中flush list
使用了 hazard
指针用于解决并行情况下普通方式需要每次重新从list尾部扫描导致的效率问题,而hazard
指针会在释放锁之前调整指针到 list 中的下一个节点[1],这样另外一个线程无需从list 尾部扫描寻找起始位置。
InnoDB 中Buffer Pool的实现,当LRU 的长度大于 BUF_LRU_OLD_MIN_LEN
之后,LRU划分为 old 和 young 两部分。当free
中的空闲块用完之后,优先从LRU 的old部分替换页面。在buf_pool_t
结构体中,指针 buf_page_t *LRU_old
指向LRU的old 部分。
buf_chunk_t
在结构体buf_pool_t
中,chunks
变量中保存内存中的页面frames 以及对应的控制块。结构体buf_chunk_t
的定义如下:
struct buf_chunk_t{
ulint mem_size; /*!< allocated size of the chunk */
ulint size; /*!< size of frames[] and blocks[] */
void* mem; /*!< pointer to the memory area which
was allocated for the frames */
buf_block_t* blocks; /*!< array of buffer control blocks */
};
其中void *mem
就是内存中的frames,blocks
中是对应每个frame的控制块。
源码 storage/innobase/buf/buf0buf.cc
buf_chunk_init
函数用于初始化一个chunk,其中包括为chunk中的frames分配内存空间。函数的声明如下:
static
buf_chunk_t*
buf_chunk_init(
/*===========*/
buf_pool_t* buf_pool, /*!< in: buffer pool instance */
buf_chunk_t* chunk, /*!< out: chunk of buffers */
ulint mem_size) /*!< in: requested size in bytes */
其中buf_pool
为chunk 所属的Buffer Pool 实例,chunk
返回初始化并分配内存的frames以及控制块,mem_size
是以字节为单位的请求分配的内存空间大小。该函数的执行过程中主要调用的函数以及说明如下:
buf_chunk_init --
|
|
-- ut_2pow_round(mem_size, UNIV_PAGE_SIZE) # 按照页面的大小,计算需要的页面个数,这里UNIV_PAGE_SIZE 默认为 14,即页面的大小16KB,如果这里mem_size如果不是刚好为2的次方,则计算页面时会向下取
|
|
-- ut_2pow_round((mem_size / UNIV_PAGE_SIZE) * (sizeof *block) + (UNIV_PAGE_SIZE - 1), UNIV_PAGE_SIZE) # 这里为控制块分配空间
|
|
-- chunk->mem = os_mem_alloc_large(&chunk->mem_size) # 为frames 以及控制块分配内存空间
|
|
|
-- frame = (byte*) ut_align(chunk->mem, UNIV_PAGE_SIZE) # 按照页面的大小内存对齐,之后会执行 chunk->size = chunk->mem_size / UNIV_PAGE_SIZE - (frame != chunk->mem),即如果分配的内存空间是需要对齐的,则页面个数减1
|
|
-- buf_block_init(buf_pool, block, frame) # 在前面的步骤完成frames的内存分配之后,初始化frames对应的控制块
|
|
-- buf_pool_index(buf_pool) # 函数的操作为当前buf_pool 指针减去系统buffer pools 指针得到当前buf_pool的序号
其中,函数os_mem_alloc_large
根据宏定义HAVE_LARGE_PAGES
和 UNIV_LINUX
决定是否使用大页内存。系统默认的内存页面大小一般是4KB,不同架构的处理器所支持的页面大小也不完全相同。比如x86架构的CPU支持4KB、2MB甚至1GB大小的内存页[3]。使用更大内存页面可以减少内存表项,并且可以减少缺页中断的次数。如果系统是基于 Unix System V的,可以使用 shmget
、shmat
[4]等操作,或者也可以通过mmap[5] 完成。
buf_page_t
首先来看内存页面的八种状态
BUF_BLOCK_POOL_WATCH
:处于该状态的页面会被purge线程删除BUF_BLOCK_ZIP_PAGE
:未修改过的压缩页面BUF_BLOCK_ZIP_DIRTY
:修改过的压缩页面BUF_BLOCK_NOT_USED
:处于该状态的页面在空闲链表中BUF_BLOCK_READY_FOR_USE
:从free list 中获取页面,页面处于该状态。从LRU替换页面也需要先放到free list里面BUF_BLOCK_FILE_PAGE
:表示已加载了磁盘上的数据,被解压的页面也会被设置为该状态BUF_BLOCK_MEMORY
:用于存储InnoDB行锁,压缩页面数据等BUF_BLOCK_REMOVE_HASH
:加入到free list之前的过渡状态,在加入到free list 之前需要先把 page hash 移除,此时页面处于该状态
结构体 buf_page_t
中包含的主要成员变量包括:
space
: tablespace idoffset
:页号io_fix
:当前的I/O 操作状态,当前页面没有读写操作为BUF_IO_NONE
,有读操作为BUF_IO_READ
,有写操作为BUF_IO_WRITE
,处于BUF_IO_PIN
状态时,不能更改在内存中的位置以及从flush list中删除buf_pool_index
:当前页面所属的Buffer Pool instance 序号newest_modification
:页面最早的修改操作的lsn(log sequence number),如果没有被修改过则为0oldest_modification
:未被写入到磁盘上的最后一次修改操作的起始lsn,如果所有的修改都更新到磁盘,则值为0
在buf0buf.cc
中,函数buf_block_alloc
用于创建新的block,该函数的执行流程如下,在获得一个可用的block之前可能需要多次迭代,这取决于free list以及LRU的状况。调用该函数需要传入一个指向Buffer Pool的指针。
buf_block_alloc--
|
-- buf_pool_from_array # 如果传入的buf_pool 为空指针,则会从buffer pools 中找一个buffer pool
|
-- buf_LRU_get_free_block # 从buf_pool 中获得空闲块
| |
| -- buf_LRU_get_free_only # 如果free list 中有可用block,则获取,校验并memset之后return
| |
| -- buf_flush_wait_batch_end # 如果double write buffer 启用并且此时后台线程