mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint

时间:2021-7-4 作者:qvyue

在上一章节我们了解到mysq的存储引擎是插件式存储引擎,这是区别于其他数据库的一个重要特性。每种存储引擎有其各自的特点,开发人员可以根据不同的场景选择不同的存储引擎。

存储引擎可以分为官方引擎和第三方引擎。大名鼎鼎的InnoDB早期就是第三方的,后被Oracle收购。

InnoDB是OLTP(Online transaction processing,联机事务处理)中核心表的首选引擎。

以下文章根据笔者使用的InnoDB版本进行讲解演示:8.0.21

一、InnoDB体系架构

mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
体系架构

存储引擎内存池
1)维护进程/线程需要访问的多个内部数据结构。
2)缓存磁盘上的数据,同时在堆存盘文件修改之前,在这里缓存。
3)重做日志(redo log)缓冲。

后台线程:
负责刷新内存池中的数据,保证内存缓冲池中的数据时最近的数据。同时将已修改的数据刷新到磁盘,同时保证发生异常时,InnoDB能恢复到正常运行状态。

1.1 后台线程

InnoDB是多线程模型,有多个不同的后台线程,负责处理不同的任务。

1.1.1 Master Thread

核心线程,负责将缓冲池的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收。

1.1.2 IO Thread

在InnoDB中大量使用了AIO来处理写请求,极大提高数据库性能。IO Thread主要负责这些请求的回调。

使用以下命令查看当前mysql的io线程数:

show Engine innodb statusg

--------
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)

如上所示,有一个insert buffre thread线程Thread0;一个log thread线程thread1;4个读线程,4个写线程。

使用下面的参数可以查看并且修改读写线程数,在linux是不能修改的,在windows可以修改:

show variables like 'innodb_read_io_threads';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_read_io_threads | 4     |
+------------------------+-------+
1 row in set (0.00 sec)

mysql> show variables like 'innodb_write_io_threads';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_write_io_threads | 4     |
+-------------------------+-------+
1 row in set (0.00 sec)

1.1.3 Purge Thread

当事务被提交以后,undolog将不再被需要了,这时候通过purty thread清除undolog,回收已经使用并且分配的undo页。

通过以下命令查看该线程:

mysql> show variables like 'innodb_purge_threads';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| innodb_purge_threads | 4     |
+----------------------+-------+
1 row in set (0.01 sec)

1.1.4 Page Cleaner Thread

mysql> show variables like 'innodb_page_cleaners';
将之前版本的脏页刷新操作都放到单独的线程去操作,减轻Master Thread的工作以及用户查询线程的阻塞,增加InnoDB的性能。
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| innodb_page_cleaners | 1     |
+----------------------+-------+
1 row in set (0.00 sec)

1.2 内存

1.2.1 缓冲池

InnoDB是基于磁盘存储的,并将其中的记录按照页的方式进行存储。由于CPU的速度与磁盘的速度差异巨大,通常会使用缓冲池技术来提高数据库的整体性能。

当读取数据时,流程大体如下:

mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
缓冲池.png

当修改数据时,大体流程如下:

mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
缓冲池写流程.png

从上面的过程我们能够得知,缓冲吃的大小实际对InnoDB的性能有很大的影响,我们可以通过下面的参数来修改mysq的缓冲池大小:

show variables like 'innodb_buffer_pool_size';

+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.00 sec)

缓存池中缓存着很多页数据,其结构如下所示:

mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
内存数据对象

并且,InnoDB支持多个缓冲池实例,每个页根据不同的哈希值分配到不同的缓冲池当中。好处是减少数据库内部的资源竞争,增加数据库的并发能力。缓冲池默认是1个,可以通过以下参数设置缓冲池的数量:

show variables like 'innodb_buffer_pool_instances';
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 1     |
+------------------------------+-------+
1 row in set (0.00 sec)

如上所示是1个实例,实际是因为的服务器是单核。当使用本地8核电脑时,发现缓冲池实例默认变成了8个。

当前我的mysql版本8.0.21和5.6.48都不允许修改该参数,无论什么方法都没修改成功。

mysql> set global innodb_buffer_pool_instances=4;
ERROR 1238 (HY000): Variable 'innodb_buffer_pool_instances' is a read only variable

使用以下命令查看当前缓冲池实例:

mysql> show engine innodb status;

BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137363456
Dictionary memory allocated 397868
Buffer pool size   8192
Free buffers       7168
Database pages     1020
Old database pages 396
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 878, created 142, written 163
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1020, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------

也可以在information_schema表中查看。

1.2.2 LRU List、Free List 、Flush List

数据库的缓冲池通过LRU(Last Recent Used,最近最少使用)算法来管理的。最频繁使用的放在LRU列表的前端,不经常使用的放在LRU的后端。当缓冲池不能够读取新页时,会先清除列表的后端。

在InnoDB中,缓冲池中页的大小默认为16k,并且对LRU算法进行了优化,加入了midpoint位置。对于新读取到的页数据不是直接加入到首部,而是加入到midpoint位置,该算法称为midpoint insertion strategy。

使用如下命令查看:

mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37    |
+-----------------------+-------+
1 row in set (0.00 sec)

value默认为37,大概在37%的位置。

在Innodb中,把midpoint之后称为old列表,在其之前称为new列表,new列表中即可理解为活跃数据。

加入midpoint的原因?
如果将新数据加入到首部,可能导致热点数据被移除newlist,影响效率。新数据可能仅仅是此次操作需要,并不是热点活跃数据。如果热点活跃数据因为新数据的加入被清除,将导致会重新请求磁盘。

midpoint和old列表的数据如何加入到热点端呢?
innoDB引入以下参数,控制midpoint和old列表被读取到并加入热点端需要等待的时间。

mysql> show variables like 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
1 row in set (0.00 sec)

LRU list用来管理已经读取的也数据,但是当InnoDB刚启动的时候,LRU List是空的,这时的页都存放在Free list当中。当进入数据读取时,也就是需要从缓冲池中分页,首先在Free List中寻找是否有可用的空闲页,有的话,将该空闲页从Free List中删除,并添加到LRU List当中;如果没有空闲页的话,则根据LRU的机制,淘汰LRU List末端的页,将该内存分配至新的页。

将old列表数据移入new列表的过程称为page made young,因为innodb_old_blocks_time导致old没有加入到new的行为叫做innodb not made young。

使用以下命令可以查看缓冲池的具体状态:

mysql> show engine innodb statusG;

BUFFER POOL AND MEMORY
----------------------
Total memory allocated 137363456; in additional pool allocated 0
Dictionary memory allocated 2486254
Buffer pool size   8191
Free buffers       5920
Database pages     2245
Old database pages 808
Modified db pages  0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 39240, not young 26758
0.00 youngs/s, 0.00 non-youngs/s
Pages read 3563, created 10858, written 3945955
0.00 reads/s, 0.00 creates/s, 0.44 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 2245, unzip_LRU len: 0
I/O sum[20]:cur[3], unzip sum[0]:cur[0]

如上所示:
Buffer pool size:表示一共有8191个页,即8191*16k=131,168k的缓冲池。
Free bufers:表示Free列表中页的数量。
Database pages:表示LRU列表中页的数量。其中LUR列表和Free列表的和并不一定等于Buffer pool size,因为缓冲池中的页还会分给自适应哈西索引,lock信息,insert buffer等页,而这部分不需要进行LRU维护,因此不存在与LRU列表当中。
Pages made young:表示LRU列表中页移动到前端的次数。
not young:表示LRU列表中页因为innodb_old_blocks_time设置而没移动到前端的次数。
youngs/s:页移动到前端每秒操作次数。
non-youngs/s:页未移动到前端每秒操作次数。
Buffer pool hit rate:缓冲池的命中率,如果小于95%,要考虑是否因为全表扫描引起的LRU列表被污染问题。
LRU lenunzip_LRU:innodb支持压缩页的功能,将原本16k的页压缩为1k、2k、4k、8k等。由于大小的变化,对于页的管理也发生了变化。经过压缩的页由unzip_LRU管理。其中LRU当中是包含了unzip_LRU的数量的。
Modified db pages:脏页数量。

也可以通过information_schame中的INNODB_BUFFER_POOL_STATS表查看。

另外也可通过表INNODB_BUFFER_PAGE_LRU查看每个页的信息。

CREATE TEMPORARY TABLE `INNODB_BUFFER_PAGE_LRU` (
  `POOL_ID` bigint(21) unsigned NOT NULL DEFAULT '0',
  `LRU_POSITION` bigint(21) unsigned NOT NULL DEFAULT '0',
  `SPACE` bigint(21) unsigned NOT NULL DEFAULT '0',
  `PAGE_NUMBER` bigint(21) unsigned NOT NULL DEFAULT '0',
  `PAGE_TYPE` varchar(64) DEFAULT NULL,
  `FLUSH_TYPE` bigint(21) unsigned NOT NULL DEFAULT '0',
  `FIX_COUNT` bigint(21) unsigned NOT NULL DEFAULT '0',
  `IS_HASHED` varchar(3) DEFAULT NULL,
  `NEWEST_MODIFICATION` bigint(21) unsigned NOT NULL DEFAULT '0',
  `OLDEST_MODIFICATION` bigint(21) unsigned NOT NULL DEFAULT '0',
  `ACCESS_TIME` bigint(21) unsigned NOT NULL DEFAULT '0',
  `TABLE_NAME` varchar(1024) DEFAULT NULL,
  `INDEX_NAME` varchar(1024) DEFAULT NULL,
  `NUMBER_RECORDS` bigint(21) unsigned NOT NULL DEFAULT '0',
  `DATA_SIZE` bigint(21) unsigned NOT NULL DEFAULT '0',
  `COMPRESSED_SIZE` bigint(21) unsigned NOT NULL DEFAULT '0',
  `COMPRESSED` varchar(3) DEFAULT NULL,
  `IO_FIX` varchar(64) DEFAULT NULL,
  `IS_OLD` varchar(3) DEFAULT NULL,
  `FREE_PAGE_CLOCK` bigint(21) unsigned NOT NULL DEFAULT '0'
) ENGINE=MEMORY DEFAULT CHARSET=utf8;

在LRU列表的页被修改后,称为脏页。即磁盘上页的数据和缓冲池中页的数据产生了不一致。这是将会通过checkpoint机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。

没有直接关于脏页的表,需要在INNODB_BUFFER_PAGE_LRU表增加OLDEST_MODIFICATION>0条件获取脏页数量:

mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
如何查看脏页
mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
table name

其中table name 为null的是系统表。

1.2.3 重做日志缓冲

从前文的内存对象图中,看到除了缓冲池以外,还有重做日志缓冲(redo log buffer)。

InnoDB首先将重做日志添加到重做日志缓存区,然后按照一定的频率将其刷新到重做日志文件。重做日志缓冲不需要设置很大,每一秒会将重做日志刷新到重做日志文件。因此用户只需要将每次的事务提交量保持在这个缓冲大小之内即可。

通过以下参数查看并设置大小:

mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set (0.00 sec)

如上所示默认为16M,足够日常使用,重做日志刷新到磁盘有以下三种情况:
1)Master Thread每一秒将日志刷新到重做日志文件。
2)每个事务提交时将日志缓冲刷新到重做日志文件。
3)当缓冲日志空间剩余小于1/2时,重做日志缓冲刷新到重做日志文件。

1.2.4 额外的内存池

当我们设置了较大的缓冲池时,也要相应的增加额外缓冲区的大小。对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当其内存不足是,需要从缓冲池当中获取。

1.3 Checkpoint 技术

了解checkpoint前,先看以下的几个问题:
1)是否能每次业数据变化,都刷新到磁盘?这个开销量很大,显然不现实。
2)如果在将缓冲池数据刷新到磁盘的过程中宕机了,那么如何保证数据的丢失呢?目前的事务数据库都采用了Write ahead log策略。即当事务提交时,先写重做日志,在修改页。通过重做日志来恢复宕机前的数据。
3)当项目运行了很多年,重做日志将会很大,重做的过程将会很久,恢复的代价非常大。

因此引入了CheckPoint技术,主要解决以下几个问题:
1)缩短日志重做时间。不需要重做所有日志,只需要对checkpoint之后的日志进行重做。
2)缓冲池不够用时,将脏页刷新到磁盘。
3)当重做日志不可用时。重做日志循环使用,不被使用的部分可以被重用。若此时重做日志还需要使用,则强制checkpoint,将缓冲池中的页至少刷新到重做日志的位置。

在InnoDB当中,其实是通过LSN(Log Sequence Number)来标记版本的。每个页有LSN,重做日志有LSN,CheckPoint中也有LSN。可以通过以下命令查看:

mysql> show engine innodb statusG;

---
LOG
---
Log sequence number          18611341
Log buffer assigned up to    18611341
Log buffer completed up to   18611341
Log written up to            18611341
Log flushed up to            18611341
Added dirty pages up to      18611341
Pages flushed up to          18611341
Last checkpoint at           18611341

在InnoDB存储引擎内部,有两种CheckPoint,分别是:
1)Sharp Checkpoint :数据库关闭时,将所有的脏页刷回磁盘。在数据库运行中,会对可用性造成影响。
2)Fuzzy Checkpoint:只刷新部分脏页回磁盘,InnoDB使用的方式。

可能发生checkpoint的几种情况:

1)Master Thread Checkpoint
Master Thread每一秒或10秒按照一定的比例异步刷新脏页到磁盘,不阻塞用户查询。

2)FLUSH_LRU_LIST Checkpoint
InnoDB要保证LRu列表至少有100个空白页可用,当不足时,会清除LRU列表尾部的页,如果是脏页,则触发CheckPoint。因为是LRU列表的操作,所以称为FLUSH_LRU_LIST。

在mysql5.6版本之后,这个检查的过程被放在了一个单独的线程中,page cleaner。通过以下参数可以控制可用页的数量:

mysql> show variables like 'innodb_lru_scan_depth';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_lru_scan_depth | 1024  |
+-----------------------+-------+
1 row in set (0.00 sec)

3)Async/Sync Checkpoint
指重做日志不可用的情况,此时需要强制刷新一些页回磁盘。已经写入重做日志的记录为redo_lsn,已经刷新回磁盘的是checkpoint_lsn。则有以下定义,下面粘贴自《Mysql技术内幕 InnoDB存储引擎 第二版》

mysql原理(二)mysql存储-InnoDB体系架构及Checkpoint
发生条件

Async/Sync Checkpoint保证重做日志循环使用的可用性。老版本的InnoDB会发现阻塞用户线程的问题。后面该过程也使用Page Cleaner Thread,不会阻塞用户线程。

4)Dirty Page too much Checkpoint
脏页数量太多,本质还是保证有足够的空闲页可用。通过以下参数控制:

mysql> show variables like 'innodb_max_dirty_pages_pct';
+----------------------------+-----------+
| Variable_name              | Value     |
+----------------------------+-----------+
| innodb_max_dirty_pages_pct | 90.000000 |
+----------------------------+-----------+
1 row in set (0.00 sec)

如上图所示表示当脏页数量达到页数量的90%,则强制checkpoint。以前的InnoDB是90,在mysql5.6是75,在mysql8以后又变回了90。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。