引 子
InnoDB 存储引擎是支持事务ACID特性的,它是以二十多年前IBM的一篇著名文章《ARIES:A Transaction Recovery Method Supporting Fine-Granularity Locking and PartialRollbacks Using Write-Ahead Logging》为理论基础,大多数关系型数据库的实现都是基于这个理论的,包括Oracle、DM等。
这个理论基本就是一个关系型数据库相关的数据库恢复原型设计,包括日志、回滚、REDO、并发控制、BufferPool管理等方面,内容非常全面。同时,这些内容是一个不可分割的整体,它们的共同目标之一就是保证数据库数据的一致性,保证数据库事务的ACID特性,所以这一节要讲的东西都是互相迁连的,它们之间相互作用,相互配合,相互驱动,才能保证数据库的数据完整性。下面就先从Buffer Pool的背景和实现开始讲起。
仔细说说InnoDB Buffer Pool
Buffer Pool的背景
InnoDB的Buffer Pool主要用来存储访问过的数据页面,它就是一块连续的内存,通过一定的算法可以使这块内存得到有效的管理。它是数据库系统中拥有最大块内存的系统模块。
InnoDB存储引擎中数据的访问是按照页(有的也叫块,默认为16K)的方式从数据库文件读取到Buffer Pool中的,然后在内存中用同样大小的内存空间来做一个映射。为了提高数据访问效率,数据库系统预先就分配了很多这样的空间,用来与文件中的数据进行交换。访问时按照最近最少使用(LRU)算法来实现Buffer Pool页面的管理,经常访问的页面在最前面,最不经常的页面在最后面。如果Buffer Pool中没有空闲的页面来做文件数据的映射,就找到Buffer Pool中最后面且不使用的位置,将其淘汰,然后用来映射新数据文件页面,同时将它移到LRU链表中的最前面。这样就能保证经常访问的页面在没有刷盘的情况下始终在Buffer Pool中,从而保证了数据库的访问效率。
Buffer Pool的大小可以在配置文件中配置,由参数innodb_buffer_pool_size的大小来决定,默认大小为128M。在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实例的一个核心工具,它包括了很多信息,主要有如下几个部分。
1. FREE链表,用来存储这个实例中所有空闲的页面。
2. flush_list链表,用来存储所有被修改过且需要刷到文件中的页面。
3. mutex,主要用来保护这个Buffer Pool实例,因为一个实例只能由一个线程访问。
4. chunks,指向这个Buffer Pool实例中第一个真正内存页面的首地址,页面都是连续存储,所以通过这个指针就可以直接访问所有的其他页面。
上面的两个链表,管理的对象是结构体buf_page_t,这是一个物理页面在内存中的管理结构,是一个页面状态信息的结合体,其中包括所属表空间、Page
ID、最新及最早被修改的LSN值(最早LSN信息会在做检查点时使用,后面将会讲到),以及形成Page链表的指针等逻辑信息。实际上,这个结构是被另一个结构管理的,它是buf_block_t,buf_block_t与buf_page_t是一一对应的,都对应BufferPool中的一个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结构的大小,直到相遇为止,这样就将一个实例初始化好了。但一般情况下,中间都会剩余一部分没有被使用,因为剩余的空间不能再放得下一个控制结构与一个页面了。相应的分配代码如下。
其中,`chunk->size`是在前面提前根据Buffer Pool实例内存大小计算出来的,可以存储的最多的Page及Page对应控制结构的个数。
限于篇幅,本文第一部分结束。
文章来自微信公众号:DBAce
本文链接:http://www.yunweipai.com/15532.html