![9d79abbe14b6b95647a4aecede4cf349.gif](https://i-blog.csdnimg.cn/blog_migrate/8b179781f3b2c257916a1816211e3431.gif)
点击上方蓝字关注我们!
作者介绍
王竹峰,去哪儿网数据库专家,擅长数据库开发、数据库管理及维护,一直致力于 MySQL 数据库源码的研究与探索,对数据库原理及实现具有深刻的理解。曾就职于达梦数据库,多年从事数据库内核开发的工作,后转战人人网,任职高级数据库工程师,目前在去哪儿网负责 MySQL 源码研究与运维、数据库管理和自动化运维平台设计开发及实践工作,是 Inception 开源项目及《MySQL 运维内参》的作者,也是 Oracle MySQL ACE。
------
本文作者将出版于《MySQL 运维内参》中部分内容进行分享,通过多篇文章连载形式,全方位介绍 MySQL 日志实现内幕,可持续关注我们的推文哦!
InnoDB 日志管理机制
InnoDB 存储引擎是支持事务 ACID 特性的,它是以二十多年前 IBM 的一篇著名文章 ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging 为理论基础,大多数关系型数据库的实现都是基于这个理论的,包括 Oracle、DM 等。
这个理论基本就是一个关系型数据库相关的数据库恢复原型设计,包括日志、回滚、REDO、并发控制、Buffer Pool 管理等方面,内容非常全面。同时,这些内容是一个不可分割的整体,它们的共同目标之一就是保证数据库数据的一致性,保证数据库事务的 ACID 特性,所以这一章要讲的东西都是互相迁连的,它们之间相互作用,相互配合,相互驱动,才能保证数据库的数据完整性。下面就先从 Buffer Pool 的实现开始讲起。
InnoDB Buffer Pool
Buffer Pool 的背景
InnoDB 的 Buffer Pool 主要用来存储访问过的数据页面,它就是一块连续的内存,通过一定的算法可以使这块内存得到有效的管理。它是数据库系统中拥有最大块内存的系统模块。
InnoDB 存储引擎中数据的访问是按照页(有的也叫块,默认为 16KB)的方式从数据库文件读取到 Buffer Pool 中的,然后在内存中用同样大小的内存空间来做一个映射。为了提高数据访问效率,数据库系统预先就分配了很多这样的空间,用来与文件中的数据进行交换。访问时按照最近最少使用(LRU)算法来实现 Buffer Pool 页面的管理,经常访问的页面在最前面,最不经常的页面在最后面。如果 Buffer Pool 中没有空闲的页面来做文件数据的映射,就找到 Buffer Pool 中最后面且不使用的位置,将其淘汰,然后用来映射新数据文件页面,同时将它移到 LRU 链表中的最前面。这样就能保证经常访问的页面在没有刷盘的情况下始终在 Buffer Pool 中,从而保证了数据库的访问效率。
Buffer Pool 的大小可以在配置文件中配置,由参数 innodb_buffer_pool_size 的大小来决定,默认大小为 128MB。在 MySQL 5.7.4 之前,一旦 MySQL 已经启动,这个值便不能再做修改,如果需要修改,只能退出 MySQL 进程,然后修改对应的配置文件来设置新的 Buffer Pool 大小,重新启动后才能生效。这在运维上非常不方便,因为很多时候,需要去调整 Buffer Pool 的大小,特别是在单机多实例,或者提供云数据库服务的情况下,我们需要根据用户及实际业务的需要,不断地去动态增加或减少 Buffer Pool size,从而合理地利用内存及优化数据库。
让人庆幸的是,MySQL 官方也发现了这种不便。在 MySQL 5.7.5 之后,MySQL 在源码上改变了对 Buffer Pool 的管理,可以在 MySQL 进程运行的情况下,动态地配置 innodb_buffer_pool_size。另外,需要强调的是,如果 Buffer Pool 的大小超过了 1GB,应该通过调整 innodb_buffer_pool_instances=N,把它分成若干个 instance 的做法,来提升 MySQL 处理请求的并发能力,因为 Buffer Pool 是通过链表的方式来管理页面的,同时为了保护页面,需要在存取的时候对链表加锁,在多线程的情况下,并发去读写 Buffer Pool 里面缓存的页面需要锁的竞争和等待。所以,修改为多个 instance,每个 instance 各自管理自己的内存和链表,可以提升效率。
Buffer Pool 实现原理
在启动 MySQL 服务时,会将所有的内嵌存储引擎启动,包括 InnoDB。InnoDB 会通过函数 buf_pool_init 初始化所有的子系统,其中就包括了 InnoDB Buffer Pool 子系统。Buffer Pool 可以有多个实例,可以通过配置文件中的参数 innodb_buffer_pool_instances 来设置,默认值为 1,实现多实例的 Buffer Pool 主要是为了提高数据页访问时的并发度。每个实例的空间大小都是相同的,也就是说系统会将整个配置的 Buffer Pool 大小按实例个数平分,然后每个实例各自进行初始化操作。
在代码中,一个 Buffer Pool 实例用 buf_pool_t 结构体来描述,这个结构体是用来管理 Buffer Pool 实例的一个核心工具,它包括了很多信息,主要有如下四部分。
FREE 链表:用来存储这个实例中所有空闲的页面。
flush_list 链表:用来存储所有被修改过且需要刷到文件中的页面。
mutex:主要用来保护这个 Buffer Pool 实例,因为一个实例只能由一个线程访问。
chunks:指向这个 Buffer Pool 实例中第一个真正内存页面的首地址,页面都是连续存储,所以通过这个指针就可以直接访问所有的其他页面。
上面的两个链表,管理的对象是结构体 buf_page_t,这是一个物理页面在内存中的管理结构,是一个页面状态信息的结合体,其中包括所属表空间、Page ID、最新及最早被修改的 LSN 值(最早 LSN 信息会在做检查点时使用,后面将会讲到),以及形成 Page 链表的指针等逻辑信息。实际上,这个结构是被另一个结构管理的,它是 buf_block_t,buf_block_t 与 buf_page_t 是一一对应的,都对应 Buffer Pool 中的一个 Page,只是 buf_page_t 是逻辑的,而 buf_block_t 包含一部分物理的概念,比如这个页面的首地址指针 frame 等。关于 buf_block_t,后面还会继续介绍。
初始化一个 Buffer Pool 实例内存空间的函数是 buf_chunk_init。一个 Buffer Pool 实例的内存分布是一块连续的内存空间,这块内存空间中存储了两部分内容,前面是这些数据缓存页面的控制头结构信息(buf_block_t 结构),每一个控制头信息管理一个物理页面,这些控制头信息的存储,占用了部分 Buffer Pool 空间,所以在运维过程中,看到状态参数 innodb_buffer_pool_bytes_data 总是比 innoDB_buffer_pool_size 小,就是因为控制头信息占用了部分空间。实际的分配方式是,Buffer Pool 页面从整个实例池中从后向前分配,每次分配一个页面,而控制结构是从前向后分配,每次分配一个 buf_block_t 结构的大小,直到相遇为止,这样就将一个实例初始化好了。但一般情况下,中间都会剩余一部分没有被使用,因为剩余的空间不能再放得下一个控制结构与一个页面了。相应的分配代码如下。
/* 一个Buffer Pool的实例大小 */
chunk->mem_size = mem_size;
/* 申请对应大小的内存空间。这里虽然申请了,但并不能真正直接得
到这部分空间,而是通过mmap映射到相应大小的空间,在后面真正使用
到内存页面的时候,才慢慢地逐渐分配真实的空间 */
chunk->mem = os_mem_alloc_large(&chunk->mem_size);
/* Allocate the block descriptors from the start of the memory block. */
chunk->blocks = (buf_block_t*) chunk->mem;
/* frame指向的是物理Buffer Pool页面,所以需要以UNIV_PAGE_SIZE大小对齐 */
frame = (byte*) ut_align(chunk->mem, UNIV_PAGE_SIZE);
/* chunk的单位是Buffer Pool中的页面个数 */
chunk->size = chunk->mem_size / UNIV_PAGE_SIZE - (frame != chunk->mem);
/* Subtract the space needed for block descriptors.
此处很重要,这是为了给frame找到合适的位置。在整个chunk的空间中,前面
需要存储Buffer Pool页面的控制信息,后面都是给Buffer Pool页面使用的。
这里计算出这个Buffer Pool实例中可以存储多少个页面,使得浪费的空间最少 */
{
ulint size = chunk->size;
while(frame < (byte*) (chunk->blocks + size)) {
/* 在这个循环中,frame从前向后,而chunk->blocks + size是从后向前,当第
一次frame比blocks的最后一个大的时候,停止循环。那么,此时的frame就是
当前Buffer Pool实例中第一个有效的Buffer Pool物理页面,
而chunk->blocks + size是当前Buffer Pool中最后一个有效的页面控制信息。
当然,这个是用来控制Buffer Pool中最后一个页面的 */
frame += UNIV_PAGE_SIZE;
/* 因为chunk->blocks的类型是buf_block_t,所以每次size变化时,
chunk->blocks + size指针的值都增大sizeof(buf_block_t),这也就是
用来存储buf_block_t所需要的实际空间大小 */
size--;
}
/* 循环停止,size为这个实例中最终的Page个数 */
chunk->size = size;
}
/* 从chunk最开始的位置存储blocks信息(页面控制信息) */
block = chunk->blocks;
for(i = chunk->size; i--; ) {
buf_block_init(buf_pool, block, frame);
UNIV_MEM_INVALID(block->frame, UNIV_PAGE_SIZE);
/* Add the block to the free list */
UT_LIST_ADD_LAST(list, buf_pool->free, (&block->page));
ut_d(block->page.in_free_list = TRUE);
ut_ad(buf_pool_from_block(block) == buf_pool);
/* 找到下一个block及下一个物理页面 */
block++;
frame += UNIV_PAGE_SIZE;
}
其中,chunk->size 是在前面提前根据 Buffer Pool 实例内存大小计算出来的,可以存储的最多的 Page 及 Page 对应控制结构的个数。
一个 Buffer Pool 实例中的所有控制头信息连续存储在一起,所以控制信息存储完成之后才是真正的缓冲页面,图 11.1 所示的是一个 Buffer Pool 实例的内存分布情况。
对于 Buffer Pool 中的所有页面,都有一个控制头信息与它对应,从图 11.1 可以看出,每一个 ctl 都表示了一个属于自己的 page 使用情况。初始化实例时当然还需要对每一个控制头信息进行初始化,也就是每一个 buf_block_t 结构。初始化一个页面控制信息是通过 buf_block_init 函数实现的,buf_block_t 结构中包含了很多信息,主要包括如下四部分。
其对应的页面地址 frame。
页信息结构 buf_page_t,这个结构用来描述一个页面的信息,包括所属表空间的 ID 号、页面号、被修改时产生的 LSN(newest_modification及oldest_modification)、使用状态(现在共有 9 种状态)等(上面已经介绍过与 buf_block_t 的关系)。
用来保护这个页面的互斥量 mutex。
访问页面时对这个页面上的锁 lock(read/write)等。
在初始化完每一个页面之后,需要将每一个页面加入到上面提到的空闲页链表中,因为这些页面现在的状态都是未使用(BUF_BLOCK_NOT_USED)。
到现在为止,缓冲池的一个实例就算初始化完成了,在访问数据库的时候会通过这些内存页面来缓存文件数据。
相对于整个 Buffer Pool 而言,多个 Buffer Pool 实例之间的关系,需要在这里再讲述一下。上面所说的单个实例的初始化,是完全独立的,多个实例之间没有任何关系,单独申请、单独管理、单独刷盘,可以从其实现的代码中看到这一点,代码如下。
dberr_t
buf_pool_init(
ulint total_size, /*!< in: size of the total pool in bytes */
ibool populate, /*!< in: virtual page preallocation */
ulint n_instances) /*!< in: number of instances */
{
ulint i;
/* total_size表示的是参数innodb_buffer_pool_size的值,即总的Buffer Pool空间大小;
n_instances表示的是innodb_buffer_pool_instances的大小。两者相除之后,就得到了每一个实例
的大小 */
const ulint size = total_size / n_instances;
/* buf_pool_ptr用来管理整个Buffer Pool空间,不过只是一个数组而已。
数组元素就是每一个Buffer Pool实例的管理首地址。buf_pool_ptr是系统
全局变量,可以通过这个变量找到每一个Buffer Pool实例 */
buf_pool_ptr = (buf_pool_t*) mem_zalloc(
n_instances * sizeof*buf_pool_ptr);
for(i = 0; i < n_instances; i++) {
/* 根据下标得到每一个实例的Buffer Pool对象 */
buf_pool_t* ptr = &buf_pool_ptr[i];
/* 初始化每一个Buffer Pool实例,就是上面介绍实例初始化代码中所讲述的内容 */
if(buf_pool_init_instance(ptr, size, populate, i) != DB_SUCCESS) {
/* Free all the instances created so far. */
buf_pool_free(i);
return(DB_ERROR);
}
}
/* 统计最终的Buffer Pool空间大小 */
buf_pool_set_sizes();
buf_LRU_old_ratio_update(100* 3/ 8, FALSE);
/* 初始化Buffer Pool自适应搜索系统 */
btr_search_sys_create(buf_pool_get_curr_size() / sizeof(void*) / 64);
return(DB_SUCCESS);
}
从代码中可以看出,每一个 Buffer Pool 实例在整个 Buffer Pool 中确实是完全独立的,而在具体使用时,针对不同页面,通过一个 HASH 算法,来映射到一个具体的实例中,对应的代码如下。
buf_pool_t*
buf_pool_get(
ulint space, /*!< in: space id */
ulint offset) /*!< in: offset of the page within space */
{
ulint fold;
ulint index;
ulint ignored_offset;
/* 根据页面号offset进行计算,再通过ignored_offset的值与表空间ID值得到fold,
然后再模srv_buf_pool_instances,即Buffer Pool的实例数,这样就得到了相对整个Buffer
Pool数组的下标,取到的就是这个实例了 */
ignored_offset = offset >> 6; /* 2log of BUF_READ_AHEAD_AREA (64) */
fold = buf_page_address_fold(space, ignored_offset);
index = fold % srv_buf_pool_instances;
return(&buf_pool_ptr[index]);
}
通过 Buffer Pool 多实例的管理机制,可以减少系统运行过程中不同页面之间一些操作的相互影响,从而很好地解决了由于页面之间的资源争抢导致的性能低下的问题,所以在实际的运维过程中,建议要分多实例的管理方式,把 MySQL 及 InnoDB 用好,让业务少一些烦恼。
【END】