第2章 InnoDB存储引擎  

本文详细介绍了InnoDB存储引擎的版本、体系结构,包括后台线程如Master Thread、IO Thread、Purge Thread和Page Cleaner Thread的作用。讨论了InnoDB的内存管理,如缓冲池、LRU List、Free List和Flush List的运作机制。此外,还阐述了Checkpoint技术和Master Thread在不同版本中的工作方式,以及如何根据数据库状态调整其行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.1 InnoDB存储引擎的版本

InnoDB存储引擎包含所有MySQL数据库的二进制发行版本中,早期版本随着MySQL数据库的更新而更新,在MySQL5.1中,可以 支持两个版本的InnoDB,一个是静态编译的InnoDB版本,可以称为老版本的InnoDB,另一个是动态加载的InnoDB版本,官方称为InnoDB Plugin,可以视为InnoDB 1.0.x版本。MySQL5.5版本中又将InnoDB的版本升级到了1.1.x。下表为InnoDB各版本功能对比:

版本功能
老版本支持ACID、行锁设计、MVCC
InnoDB 1.0.x继承上版功能,增加了compress和dynamic页格式
InnoDB 1.1.x继承上版功能,增加了Linux AIO、多回滚段
InnoDB 1.2.x继承上版功能,增加了全文索引支持、在线索引添加

2.2 InnoDB体系结构

在这里插入图片描述

2.2.1 后台线程

InnoDB存储引擎是多线程的模型,因此其后台有多个不同的后台线程,负责处理不同的任务。

  1. Master Thread
    Master Thread是一个非常核心的后台线程,主要负责将缓冲区中的数据异步刷新到磁盘,保持数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等。
  2. IO Thread
    InnoDB存储引擎使用了AIO(异步IO)来处理IO请求,这样可以提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调。InnoDB 1.0版本之前有4个IO Thread,分别是write、read、insert buffer和log IO thread。IO Thread的数量不断进行调整,从InnoDB 1.0.x版本开始,read thread和write thread分别增大到了4个,在windows系统中可以通过innodb_read_io_threadsinnodb_write_io_threads参数进行设置,如:
    在这里插入图片描述
    通过命令SHOW ENGINE INNODB STATUS;来查看InnoDB中的IO Thread;
I/O thread 0 state: wait Windows aio (insert buffer thread)
I/O thread 1 state: wait Windows aio (log thread)
I/O thread 2 state: wait Windows aio (read thread)
I/O thread 3 state: wait Windows aio (read thread)
I/O thread 4 state: wait Windows aio (read thread)
I/O thread 5 state: wait Windows aio (read thread)
I/O thread 6 state: wait Windows aio (write thread)
I/O thread 7 state: wait Windows aio (write thread)
I/O thread 8 state: wait Windows aio (write thread)
I/O thread 9 state: wait Windows aio (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,

可以看到IO Thread 0为insert buffer thread。IO Thread 1为log thread,剩余就是innodb_read_io_threads及innodb_write_io_threads来设置的读写线程。
3. Purge Thread
事务提交之后,其所使用的undolog可能不再需要,此时PurgeThread来回收已经使用并分配的undo页。
4. Page Cleaner Thread
Page Cleaner Thread是在InnoDB 1.2.x版本中引入的,其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。

2.2.2 内存

1. 缓冲池
  用于减小CPU和磁盘之间的速度鸿沟。可以通过SHOW VARIABLES LIKE 'innodb_buffer_pool_size';来查看缓冲池大小。具体来看缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的锁信息、数据字典信息等。
在这里插入图片描述
从InnoDB 1.0.x版本开始,允许有多个缓冲池实例,每个页根据哈希值平均分配到不同缓冲池实例中,这样可以减少数据库内部的资源竞争。
2. LRU List、 Free List和Flush List
  这一部分是告诉大家,InnoDB存储引擎是怎么管理内存区域的。通常来说数据库中的缓冲池是通过LRU(Latest Recent Used, 最近最少使用)算法进行管理的。在LRU算法中,最频繁使用的页放在LRU列表的前端,而最少使用的页放在LRU列表的后端,当无法在LRU列表中插入新读取到的页的时候,LRU算法会释放LRU列表尾端中的页。
  在InnoDB存储引擎中,缓存池中页的大小默认为16KB,同样使用LRU算法进行管理。但稍有不同的是,在InnoDB存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页并不是先存放到LRU列表的首部,而是放入midpoint的位置。在默认配置中,midpoint的位置在LRU列表长度的5/8处。midpoint位置可以由参数innodb_old_blocks_pct控制。

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

mysql>

  为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?这是因为在数据库中,常见的操作为索引或数据的扫描操作,这类操作需要访问表中的许多页,甚至是常见的页,而这些页通常来说又仅在这次查询中需要,而不是作为活跃的热点数据。如果放入LRU列表的首部,那么可能将热点数据页从LRU列表中移除,那么会降低数据库的性能。
  为了解决这个问题,InnoDB存储引擎引入了另一个参数来进一步管理LRU列表,这个参数是innodb_old_blocks_time,表明页读取到mid位置后需要等待多久才会被加入到列表的前端。
  LRU列表用来管理已经读取的页,但是当数据库刚启动时,LRU列表时空的,即没有任何数据。这时候页都存放在Free列表中。当需要从缓冲区中分页时,首先从Free列表中查找是否有可用的空闲页,若有则从Free列表中删除,放入到LRU列表中。否则,就根据LRU算法,淘汰LRU列表末尾的页,并将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时,成此时发生的操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old移动到new部分的操作称为page not made young。可以通过命令show engine innodb status来观察LRU列表以及Free列表的使用情况和运行状态。

mysql> show engine innodb status;
******************** 省略 ******************
Per second averages calculated from the last 25 seconds
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137363456
Dictionary memory allocated 390982
Buffer pool size   8192
Free buffers       7255
Database pages     932
Old database pages 364
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 790, created 142, written 156
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: 932, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

  通过命令show engine innodb status可以看到:当前Buffer Pool Size共有8192个页,共128MB的缓冲池。Free buffers表示当前Free列表中页的数量,Databases pages表示LRU列表中页的数量。
  pages made young显示LRU列表中页移动到前端的次数,因为我这个mysql刚安装不久,所以还没有什么记录信息。而youngs/s、non-youngs/s表示每秒这两种操作的次数。

注意命令show engine innodb status显示的不是当前的状态,而是过去某个时间范围内InnoDB存储引擎的状态。从上面的例子可以看出,Per second averages calculated from the last 25 seconds表示信息是过去25内的数据库状态。

  从InnoDB1.2版本开始,还可以通过表INNODB_BUFFER_POOL_STATS来观察缓冲池的运行状态,如:

mysql> select POOL_ID,HIT_RATE,PAGES_MADE_YOUNG,PAGES_NOT_MADE_YOUNG from information_schema.innodb_buffer_pool_stats;
+---------+----------+------------------+----------------------+
| POOL_ID | HIT_RATE | PAGES_MADE_YOUNG | PAGES_NOT_MADE_YOUNG |
+---------+----------+------------------+----------------------+
|       0 |        0 |                0 |                    0 |
+---------+----------+------------------+----------------------+
1 row in set (0.00 sec)

  在LRU列表中的页被修改后,称该页为脏页,即缓冲池中的页与磁盘中的页产生了不一致。这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页不仅存放在LRU列表中,也存放在Flush列表中,LRU列表用来管理缓冲池中也的可用性,而Flush列表用来管理将页刷新到磁盘中,二者不影响。

3. 重做日志缓冲
  InnoDB存储引擎首先将重做日志信息放入到重做日志缓冲区,然后按一定频率将其刷新到重做日志文件中。重做日志缓冲一般不需要设置很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件中,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内就可以了。这个值可以由配置参数innodb_log_buffer_size控制,默认为8MB。我安装的版本缓冲区为16MB

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

  重做日志在下列三个情况下将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中:

  • MASTER Thread每秒将缓冲刷新到日志文件中
  • 每个事务提交的时候刷新到重做日志文件中
  • 当重做日志缓冲剩余空间小于1/2时,刷新到重做日志文件中
    4. 额外的内存池
    在InnoDB存储引擎中,对内存的管理时通过一种称为内存堆的方式进行的,在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,例如,每个缓冲池中的帧缓冲还有对应的缓冲控制对象,这些对象记录了一些诸如LRU、锁、等待等消息,都需要从额外内存池中申请。

2.4 Checkpoint技术

  为了缩小处理器与磁盘之间速度的鸿沟,数据库系统引入了缓冲池机制。但是如果一条DML语句,比如Update或Delete操作改变了页中的记录,那么此时页是脏的,与磁盘中的数据不一致,此时数据库需要将较新的数据刷新磁盘中。然而如果一个个变化,将新页刷新到磁盘中,如此反复IO,同样会降低数据库的性能。同时,如果在缓冲池将页的新版本刷新到磁盘中时发生了宕机,那么数据也将变得不可以恢复。为了避免数据丢失的问题,数据库系统引入了Write Ahead Log策略,即当事务提交的时候,先写重做日志,在修改页。当发生宕机造成数据丢失时,通过重做日志可以完成数据的恢复。
  随着数据库的不断使用,日志文件越来越大,如果此时发生宕机,重新应用重做日志的时间会非常久,这个时候的代价会非常大。因此,Checkpoint技术的目的就是解决以下几个问题的:

  • 缩短数据库的恢复时间
  • 缓冲池不够用时,将脏页刷新到磁盘中
  • 重做日志不可用时,刷新脏页

  当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回到了磁盘,故数据库只需要对Checkpoint之后的重做日志进行恢复,这样就可以大为减少恢复时间。
  此外,当缓冲区不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么就强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
  对于InnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。LSN为8字节的数字,每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN,可以通过命令show engine innodb status来观察:

---
LOG
---
Log sequence number          20365943
Log buffer assigned up to    20365943
Log buffer completed up to   20365943
Log written up to            20365943
Log flushed up to            20365943
Added dirty pages up to      20365943
Pages flushed up to          20365943
Last checkpoint at           20365943
18 log i/o's done, 0.00 log i/o's/second

  在InnoDB存储引擎内部,有两种Checkpoint,分别为:

  • Sharp Checkpoint
  • Fuzzy Checkpoint

  Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新到磁盘中,这是默认的工作。
  当若在数据库运行的时候页使用Sharp Checkpoint,则数据库的性能会受到很大的影响,故在InnoDB存储引擎中使用Fuzzy Checkpoint进行页的刷新,只刷新一部分脏页,而不是刷新全部脏页。在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint:

  • Master Thread Checkpoint
  • Flush_LRU_LIST Checkpoint
  • Async/Sync Flush Checkpoint
  • Dirty Page too much Checkpoint

2.5 Master Thread工作方式

2.5.1 InnoDB 1.0.x版本之前的Master Thread

  Master Thread具有最高的线程有限级别,其内部由多个循环(loop)组成:主循环(loop)、后台循环(background loop)、刷新循环(flush loop)、暂停循环(suspend loop)。Master Thread会根据数据库运行的状态在这几种循环中切换。
  Loop被称为主循环,大多数操作在这个循环中,其中有两大部分操作——每秒的操作和每10秒的操作。

void master_thread(){
	loop:
	for(int i=0; i<10; i++){
		do thing once per second
		sleep 1 second if necessary
	}
	do things once per ten seconds
	goto loop;
}

  每一秒的操作包括:

  • 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
  • 合并插入缓冲(可能)
  • 至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能)
  • 如果当前没有用户活动,则切换到background loop(可能)

  即使某个事务没有提交,InnoDB存储引擎仍然会每秒将重做日志缓冲中的内容刷新到重做日志文件中。
  合并插入缓冲并不是每秒发生。InnoDB存储引擎会判断当前一秒内的IO次数是否小于5次,如果小于5次,InnoDB认为当前的IO压力很小,可以执行合并插入缓冲的操作。
  同样刷新100个脏页也不是每秒发生的,InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过配置文件中innodb_max_dirty_pages_pct这个参数(默认为90, 代表为90%),如果超过这个阈值,InnoDB存储引擎认为需要做磁盘同步的操作,将100个脏页写入磁盘中。
  具体代码如下:

void master_thread(){
	loop:
	for(int i=0; i<10; i++){
		thread_sleep(1)
		do log buffer flush to disk
		if(last_one_second_ios < 5)
			do merge at most 5 insert buffer
		if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
			do buffer pool flush 100 dirty pages
		if(no user activity)
			goto background loop
	}
	do things once per ten seconds
	background loop:
		do something
		goto loop:
}

  接着来看每10秒的操作,包括如下内容:

  • 刷新100个脏页到磁盘(可能情况下)
  • 合并至多5个插入缓冲(总是)
  • 将日志缓冲刷新到磁盘(总是)
  • 删除无用的Undo页(总是)
  • 刷新100个或者10个脏页到磁盘中(总是)

  在以上过程中,InnoDB存储引擎会判断过去10秒之内磁盘的IO操作是否会小于200次,如果时则将100个脏页刷新到磁盘。接着InnoDB存储引擎会合并插入缓冲,不同于每秒的操作,这次的合并插入缓冲操作总会在这个阶段执行。之后InnoDB存储引擎会再进行一次将日志缓冲刷新到磁盘的操作,这和每秒发生的操作时一样的。
  接着InnoDB存储引擎会进一步执行full purge操作,即删除无用的Undo页。对表进行update、delete时,原先的行会被删除,但是为了一致性读的原因,需要保留这些行版本的信息。但是在full purge过程中,InnoDB存储引擎会判断事务系统中已被删除的行是否可以删除,如果可以删除,则立即删除,不过InnoDB存储引擎在执行full purge操作的时候,每次最多尝试回收20个undo页。
  最后,InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘中。

  接着来看background loop,若当前没有用户活动(数据库空闲时)或者数据库关闭(shutdown)就会切换到这个循环,做如下操作:

  • 删除无用的Undo页(总是)
  • 合并20个插入缓冲(总是)
  • 跳回到主循环(总是)
  • 不断刷新100个页直到符合条件(可能,跳转到flush loop中完成)

  若flush loop中也没有什么事情可以做,InnoDB存储引擎会切换到suspend_loop,将Master Thread挂起,等待事件的发生。

2.5.2 InnoDB 1.2.x版本之前的Master Thread

  我们查看1.0.x版本的具体实现之后,我们可以发现在1.0.x版本中InnoDB引擎对IO其实是有限制的,在缓冲区向磁盘刷新数据的时候其实都做了一定的硬编码,也就是我们常说的程序写死了。而随着计算机硬件的发展,比如当前SSD的普及,这中硬编码的会限制InnoDB存储引擎对磁盘IO的性能,尤其是写入性能。从之前的Master Thread伪代码来看,InnoDB存储引擎至多只会刷新100个脏页到磁盘,合并20个插入缓冲。但是如果在写入密集的应用程序中,每秒可能会产生大于100个脏页,如果产生大于20个插入缓冲的情况,Master Thread似乎“忙不过来”;尤其是在IO容量很大的请款下,明明这一秒可以将大于100的脏页写入磁盘,却由于硬代码的限制,只能写入100个脏页。
  所以在1.2.x版本中,提供了参数innodb_io_capacity,用来表示磁盘IO的吞吐量,默认值是200.对于刷新到磁盘页的数量,会按照innodb_io_capacity的百分比来进行控制。规则如下:

  • 在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值的5%
  • 在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity

  另一个问题是,参数innnodb_max_dirty_pages_pct默认值的问题,在InnoDB 1.0x版本之前,这个值的默认值是90,意味着脏页占缓冲池的90%,但是这个值太大了,当脏页占缓冲池的90%时,如果有很大的内存或数据库服务器的压力很大,这时刷新脏页的速度反而会降低。从InnoDB 1.0.x版本开始,innodb_max_dirty_pages_pct默认值为75,这样既可以加快刷新脏页的频率,也可以保证磁盘的IO负载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值