循环buffer的实现_全方位解读 MySQL 日志实现内幕(一)

9d79abbe14b6b95647a4aecede4cf349.gif

点击上方蓝字关注我们!

b7b85d8b3546d0b404a0e562664a1c99.png

作者介绍

王竹峰,去哪儿网数据库专家,擅长数据库开发、数据库管理及维护,一直致力于 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 实例的内存分布情况。

2e845d731976f511e6552fd9d9942a12.png

对于 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】

1ca5f2fab6a0aff7931da6c5c10b1028.png

ab89aaab537737c32d9bd2afcf8fb1c6.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值