InnoDB体系架构
1、InnoDB存储引擎的体系架构
从图可见,InnoDB的存储引擎的体系架构,由多个内存块组成的内存池和作用在内存池上的一些后台线程组成。
内存池的作用:
- 维护所有进程/线程需要访问的多个内部数据结构。
- 缓存磁盘上的数据,方便快速地读取,同时在对磁盘文件的数据修改之前在这里缓存。
- 重做日志(redo log)缓冲。
后台线程的作用:
- 负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。
- 将已修改的数据文件刷新到磁盘文件
- 保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。
2、后台线程
InnoDB存储引擎是多线程的模型,因此其后台有多个不同的后台线程,负责处理不同的任务。
2.1、Master Thread
Master Thread是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插人缓冲(INSERT BUFFER)、UNDO页的回收等。
2.2、IO Thread
在InnoDB存储引擎中大量使用了AIO (Async IO)来处理写IO请求,极大地提高了数据库性能。而IO Thread的工作主要是负责这些IO请求的回调(call back)处理。主要有4个IO Thread,分别是write、read、insert buffer和 log IO thread
可通过innodb_read_io_threads 和 innodb_write_io_threads参数设置read和write的线程数。
mysql> show variables like 'innodb_version'\G
*************************** 1. row ***************************
Variable_name: innodb_version
Value: 8.0.21
1 row in set (0.00 sec)
mysql> show variables like 'innodb_%io_threads'\G
*************************** 1. row ***************************
Variable_name: innodb_read_io_threads
Value: 4
*************************** 2. row ***************************
Variable_name: innodb_write_io_threads
Value: 4
2 rows in set (0.00 sec)
通过命令show engine innodb status\G 可查看当前状态,可以看到前面介绍的IO Thread。
mysql> show engine innodb status\G
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2021-07-11 18:00:50 0x7f9f1c062700 INNODB MONITOR OUTPUT
=====================================
//...........................此处省略部分输出............................
--------
FILE I/O
--------
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
//...........................此处省略部分输出............................
END OF INNODB MONITOR OUTPUT
============================
1 row in set (0.00 sec)
2.3、Purge Thread
事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。在MySQL数据库的配置文件中添加如下配置来启用PurgeThread。
[mysqld]
innodb_purge_threads=1
InnoDB支持配置多个Purge Thread,目的是为了进步加快undo页的回收。同时由于Purge Thread需要离散地读取undo页,这样也能更进一步利用磁盘的随机读取性能。如用户可以设置4个Purge Thread。
mysql> select version()\G
*************************** 1. row ***************************
version(): 8.0.21
1 row in set (0.00 sec)
mysql> show variables like 'innodb_purge_threads'\G
*************************** 1. row ***************************
Variable_name: innodb_purge_threads
Value: 4
1 row in set (0.00 sec)
2.4、Page Cleaner Thread
Page Cleaner Thread其作用是完成脏页的刷新操作。
3、内存
3.1、缓冲池
InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。
缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。
- 在数据库中进行读取页的操作
首先将从磁盘读到的页存放在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中,若在缓冲池中,直接读取该页。否则,读取磁盘上的页。 - 对于数据库中页的修改操作
首先修改在缓冲池中的页,然后再以一定的频率(Checkpoint的机制)刷新到磁盘上。
缓冲池中缓存的数据页类型有﹔索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引、InnoDB存储的锁信息(lockinfo)、数据字典信息(data dictionary)等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。
对于InnoDB存储引擎而言,缓冲池大小可通过innodb_buffer_pool_size来参数设置。
mysql> show variables like 'innodb_buffer_pool_size'\G
*************************** 1. row ***************************
Variable_name: innodb_buffer_pool_size
Value: 134217728
1 row in set (0.01 sec)
InnoDB允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。可以在配置文件的innodb_buffer_pool_instances参数来配置实例数,该值默认为1。
在配置文件中将innodb_buffer_pool_instances设置为大于1的值就可以得到多个缓冲池实例。
mysql> show variables like 'innodb_buffer_pool_instances'\G
*************************** 1. row ***************************
Variable_name: innodb_buffer_pool_instances
Value: 1
1 row in set (0.00 sec)
通过查询information_schema数据库下的INNODB_BUFFER_POOL_STATS表可以查看缓冲池实例信息。
mysql> use information_schema
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select * from INNODB_BUFFER_POOL_STATS\G
*************************** 1. row ***************************
POOL_ID: 0
POOL_SIZE: 8192
FREE_BUFFERS: 7108
DATABASE_PAGES: 1067
OLD_DATABASE_PAGES: 387
MODIFIED_DATABASE_PAGES: 0
PENDING_DECOMPRESS: 0
PENDING_READS: 0
PENDING_FLUSH_LRU: 0
PENDING_FLUSH_LIST: 0
PAGES_MADE_YOUNG: 411
PAGES_NOT_MADE_YOUNG: 1433
PAGES_MADE_YOUNG_RATE: 0.000000007383915602977238
PAGES_MADE_NOT_YOUNG_RATE: 0.000000047995451419352045
NUMBER_PAGES_READ: 879
NUMBER_PAGES_CREATED: 192
NUMBER_PAGES_WRITTEN: 544
PAGES_READ_RATE: 0.000000013537178605458268
PAGES_CREATE_RATE: 0.0000000012306526004962063
PAGES_WRITTEN_RATE: 0.0000000018459789007443095
NUMBER_PAGES_GET: 46059
HIT_RATE: 987
YOUNG_MAKE_PER_THOUSAND_GETS: 7
NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 48
NUMBER_PAGES_READ_AHEAD: 0
NUMBER_READ_AHEAD_EVICTED: 0
READ_AHEAD_RATE: 0
READ_AHEAD_EVICTED_RATE: 0
LRU_IO_TOTAL: 25
LRU_IO_CURRENT: 0
UNCOMPRESS_TOTAL: 0
UNCOMPRESS_CURRENT: 0
1 row in set (0.00 sec)
如何管理缓冲池
InnoDB存储引擎是怎么对这么大的内存区域进行管理的呢
通常来说,数据库中的缓冲池是通过LRU(Latest Recent Used,最近最少使用〉算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。
在InnoDB存储引擎中,缓冲池中页的大小默认为16KB,同样使用LRU算法对缓冲池进行管理。稍有不同的是InnoDB存储引擎对传统的LRU算法做了一些优化。LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strategy。在默认配置下,该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb_old_blocks_pct控制。如下可以看到,其默认值为37,表示新读取的页插人到LRU列表尾端的37%的位置(差不多3/8的位置)。把midpoint之后的列表称为old列表,之前的列表称为new列表。new列表中的页都是最为活跃的热点数据。
mysql> show variables like 'innodb_old_blocks_pct'\G
*************************** 1. row ***************************
Variable_name: innodb_old_blocks_pct
Value: 37
1 row in set (0.00 sec)
那为什么不采用朴素的LRU算法,因为若直接将读取到的页放入到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。
InnoDB还可以设置innodb_old_blocks_time参数,用于表示页读取到mid位置后需要等待多久才会被加人到LRU列表的热端。
LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时页都存放在Free列表中。当需要从缓冲池中分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入到LRU列表中。否则,根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。
在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。
3.2、重做日志缓冲
从上面的图2-2可以看到,InnoDB存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区,然后按一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size控制,默认为16MB
mysql> show variables like 'innodb_log_buffer_size'\G
*************************** 1. row ***************************
Variable_name: innodb_log_buffer_size
Value: 16777216
1 row in set (0.00 sec)
在通常情况下,16MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。
- Master Thread每一秒将重做日志缓冲刷新到重做日志文件;
- 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
- 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。
3.3、额外的内存池
额外的内存池通常被DBA忽略,他们认为该值并不十分重要,事实恰恰相反,该值同样十分重要。在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。例如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。因此,在申请了很大的InnoDB缓冲池时,也应考虑相应地增加这个值。