原创

MySQL InnoDB架构

InnoDB架构图

InnoDB架构图1
InnoDB架构图2

InnoDB主要包含3个部分:

  • 内存池
    • buffer pool
      • 数据页
      • 索引页
      • 插入缓冲页
      • 锁信息
      • 自适应hash索引
      • 数据字典
    • double write
    • redo log buffer
    • 额外内存池
  • 后台线程
    • 主线程
    • 页清理线程
    • 清除线程
    • 异步IO线程
      • 插入缓冲线程
      • 读、写线程
      • redo log线程
  • 磁盘文件
    • 系统表空间
    • 用户表空间
    • 重做日志文件

其中后台线程用来将内存中的数据异步更新到磁盘文件中。

InnoDB磁盘文件

InnoDB磁盘文件
系统表空间和用户表空间

二进制文件(binlog)等文件是MySQL Server层维护的文件,所以未列入InnoDB的磁盘文件中。

系统表空间(共享表空间)

  1. data dictionary数据字典:记录数据库相关信息
  2. double write buffer双写缓冲:解决部分写失败(页断裂)
  3. insert buffer插入缓冲:内存insert buffer数据,周期写入共享表空间,防止意外宕机
  4. rollback segments回滚段
  5. undo空间:undo页
  6. 系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据

系统表空间是由一个或者多个数据文件组成。默认情况下,一个初始大小为10MB,名为ibdata1的系统数据文件在MySQL的data目录下被创建。用户可以使用innodb_data_file_path对数据文件的大小和数量进行配置。
示例:

innodb_data_file_path=/db/ibdata1:1000M;/dr2/db/ibdata2:1000M:autoextend
  • 这里将/db/ibdata1/dr2/db/ibdata2两个文件组成系统表空间。
  • 如果这两个文件位于不同的磁盘上,磁盘的负载可能被平均,因此可以提高数据库的整体性能。
  • 两个文件的文件名之后都跟了属性,表示文件ibdata1的大小为1000MB,文件ibdata2的大小为1000MB,而且用完空间之后可以自动增长(autoextend)。

用户表空间(独立表空间)

如果设置了参数innodb_file_per_table(MySQL5.7后默认配置),则用户可以将每个基于InnoDB存储引擎的表产生一个独立的用户表空间。用户表空间的命名规则为:表名.ibd

用户表空间只存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息还是存放在默认的系统表空间中。

  1. 每个表的数据和索引都会存在自已的表空间中(.ibd)
  2. 每个表的结构(.frm)
  3. insert buffer bitmap

redo log和归档文件

redo log

重做日志文件的作用是什么?

  • 当InnoDB的数据存储文件发生错误时,重做日志文件就能派上用场。InnoDB存储引擎可以使用重做日志文件将数据恢复为正确状态,以此来保证数据的正确性和完整性。
  • 为了得到更高的可靠性,用户可以设置多个镜像日志组,将不同的文件组放在不同的磁盘上,以此来提高重做日志的高可用性。

重做日志文件组是如何写入数据的?

  • 每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。
  • 在日志组中每个重做日志文件的大小一致,并以循环写入的方式运行。InnoDB存储引擎先写入重做日志文件1,当文件被写满时,会切换到重做日志文件2,再当重做日志文件2也被写满时,再切换到重做日志文件1。

redo log机制

如何设置重做日志文件大小?
用户可以使用innodb_log_file_size来设置重做日志文件的大小,这对InnoDB存储引擎的性能有着非常大的影响。
(1) 如果重做日志文件设置的太大,数据丢失时,恢复时可能需要很长的时间;
(2) 另一方面,如果设置的太小,重做日志文件太小会导致依据checkpoint的检查需要频繁刷新脏页到磁盘中,导致性能的抖动。

InnoDB内存结构

InnoDB架构图1
InnoDB架构图2

Buffer Pool缓冲池

InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。但是由于CPU速度和磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池记录来提高数据库的的整体性能。

所以,缓冲池的大小直接影响着数据库的整体性能,可以通过配置参数innodb_buffer_pool_size来设置。

组成:

  • 数据页
  • 索引页
  • 插入缓冲
  • 自适应hash索引
  • 锁信息
  • 数据字典

数据页和索引页

Page是InnoDB存储的基本结构,也是内存和磁盘交互的最小单位(并不是磁盘管理的最小单位,磁盘管理的最小单位是扇区,是512Byte),默认Page大小为16K

InnoDB操作增删改时,缓冲池中的数据页数据、索引页数据与磁盘文件中的数据不一致,那该数据页就是脏页(其实是待更新页),脏页会定期同步到磁盘文件中。

insert buffer 插入缓冲

主要针对次要索引的数据插入存在的问题而设计。

在InnoDB引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在次要索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于次要索引叶节点的插入不再是顺序的了,这时就需要离散的访问次要索引页,由于随机读取的存在导致插入操作性能下降。

InnoDB为此设计了Insert Buffer来进行插入优化。对于次要索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非主键索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer中。看似数据库这个非主键的索引已经插到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行Insert Buffer和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。

adaptive hash index 自适应hash索引

key-value键值对保存,InnoDB会根据访问的频率和模式,为热点页建立哈希索引,来提高查询效率

自适应哈希索引是通过缓冲池的B+树页构建而来,因此建立速度很快,而且不需要对整张数据表建立哈希索引。其有一个要求,即对这个页的连续访问模式必须是一样的,也就是说其查询的条件(WHERE)必须完全一样,而且必须是连续的。

lock info锁信息

就是表级锁(表锁、元数据锁)和行级锁(行记录锁、间隙锁、next-key)

data dictionary 数据字典

元数据信息,包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。MySQL INFORMATION_SCHEMA库提供了对数据局元数据、统计信息、以及有关MySQL server的访问信息(例如:数据库名或表名,字段的数据类型和访问权限等)。该库中保存的信息也可以称为MySQL的数据字典。

Redo log Buffer 重做日志缓冲

InnoDB在缓冲池中变更数据时,会首先将相关变更写入重做日志缓冲中,然后再按时或者当事务提交时写入磁盘,这符合Force-log-at-commit原则;当重做日志写入磁盘后,缓冲池中的变更数据才会依据checkpoint机制择时写入到磁盘中,这符合了Write Ahead Log(WAL)原则。

重做日志缓冲一般不需要设置得很大,因为一般情况每一秒钟都会将重做日志缓冲刷新到日志文件中。可通过配置参数innodb_log_buffer_size控制,默认为8MB。

额外内存池

Double Write Buffer 双写缓冲

作用:Double Write带给InnoDB存储引擎的是数据页的可靠性。

组成:Double Write由两部分组成,一部分是内存中的double write buffer,大小为2MB,另一部分是物理磁盘上共享表空间(ibdata1中)连续的128个页,大小也为2MB。

内存数据落盘

内存数据落盘

从图中我们可以看到内存数据落盘主要有两种:

  1. 脏页落盘
  2. redo log日志落盘(异步线程)

当缓冲池中的页的版本比磁盘要新时,数据库需要将新版本的页从缓冲池刷新到磁盘。但是如果每次一个页发送变化,就进行刷新,那么性能开发是非常大的,于是InnoDB采用了Write Ahead Log(WAL)策略和Force Log at Commit机制实现事务级别下数据的持久性。

脏页落盘

  1. 在数据库中进行读取操作,将从磁盘中读到的页放在缓冲池中,下次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
  2. 对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为CheckPoint的机制刷新回磁盘

Check Point

定义:脏页写入到磁盘的时机,所以检查点也就意味着脏页数据的写入。

思考一下这个场景:
如果重做日志可以无限地增大,同时缓冲池也足够大,那么是不需要将缓冲池中页的新版本刷新回磁盘。因为当发生宕机时,完全可以通过重做日志来恢复整个数据库系统中的数据到宕机发生的时刻。

但是这需要两个前提条件:

  1. 缓冲池可以缓存数据库中所有的数据
  2. 重做日志可以无限增大

所以,InnoDB产生了Check Point机制,其目的是解决以下几个问题:

  • 缩短数据库的恢复时间
  • buffer pool空间不够用时,将脏页数据刷新到磁盘
  • redo log空间快满,刷新脏页数据到磁盘
check point 解决哪些问题
  • 当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。数据库只需对Checkpoint后的重做日志进行恢复,这样就大大缩短了恢复的时间。
  • 当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页刷回磁盘。
  • 当重做日志出现不可用时,因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的,重做日志可以被重用的部分是指这些重做日志已经不再需要,当数据库发生宕机 时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。如果重做日志还需要使用,那么必须强制Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

Checkpoint发生的时间、条件以及脏页的选择都很复杂,而Checkpoint所做的事情无非就是将缓冲池中的脏页刷回到磁盘,不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发Checkpoint。

LSN

对于InnoDB存储引擎而言,是通过LSN(Log Sequence Number)来标记版本的。
LSN是8字节的数字,每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。可以通过命令SHOW ENGINE INNODB STATUS来观察:

show engine innodb status;

---
LOG
---
Log sequence number 34822137537
Log flushed up to   34822137537
Last checkpoint at  34822133028
0 pending log writes, 0 pending chkp writes
54189288 log i/o's done, 3.00 log i/o's/second
...
InnoDB数据恢复

在Innodb事务日志中,采用了Fuzzy Checkpoint,Innodb每次取最老的modified page(last checkpoint)对应的LSN,再将此脏页的LSN作为Checkpoint点记录到日志文件,意思就是此LSN之前的LSN对应的日志和数据都已经flush到redo log

当mysql crash的时候,Innodb扫描redo log,从last checkpoint开始apply redo log到buffer pool,直到last checkpoint对应的LSN等于Log flushed up to对应的LSN,则恢复完成。

InnoDB一条事物日志共经历4个阶段:

  • 创建阶段:事务创建一条日志;
  • 日志刷盘:日志写入到磁盘上的日志文件;
  • 数据刷盘:日志对应的脏页数据写入到磁盘上的数据文件;
  • 记录Check Point:日志被当作Check Point写入日志文件;

对应这4个阶段,系统记录了4个日志的相关信息,用于其他处理.

  • Log sequence number(LSN1):当前系统LSN最大值,新的事务日志LSN将在此基础上生成(LSN1+新日志的大小);
  • Log flushed up to(LSN2):当前已经写入日志文件的LSN;
  • Oldest modified data log(LSN3):当前最旧的脏页数据对应的LSN,写Checkpoint的时候直接将此LSN写入到日志文件;
  • Last checkpoint at(LSN4):当前已经写入Checkpoint的LSN;

对于系统来说,以上4个LSN是递减的,即:LSN1>=LSN2>=LSN3>=LSN4

Check Point分类

sharp checkpoint

数据库正常关闭时,会触发把所有的脏页都写到磁盘上。

Sharp Checkpoint 发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。但是若数据库在运行时也使用Sharp Checkpoint,那么数据库的可用性就会受到很大的影响。故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

fuzzy checkpoint

正常使用时模糊检查点,部分页写入磁盘。

Master Thread Checkpoint

以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘,这个过程是异步的。此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。

FLUSH_LRU_LIST Checkpoint

因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用。在InnoDB 1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然这会阻塞用户的查询操作。倘若没有100个可用空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表的,因此称为FLUSH_LRU_LIST Checkpoint。

而从MySQL 5.6版本,也就是InnoDB 1.2.x版本开始,这个检查被放在了一个单独的页清理线程(Page Cleaner)线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024,如:

SHOW GLOBAL VARIABLES LIKE 'innodb_lru_scan_depth';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_lru_scan_depth | 1024  |
+-----------------------+-------+
Async/Sync Flush Checkpoint

指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。若将已经写入到重做日志的LSN记为redo_lsn,将已经刷新回磁盘最新页的LSN记为checkpoint_lsn,则可定义:

checkpoint_age = redo_lsn - checkpoint_lsn

再定义以下的变量:
`async_water_mark = 75% * total_redo_log_file_size`
`sync_water_mark = 90% * total_redo_log_file_size`

若每个重做日志文件的大小为1GB,并且定义了两个重做日志文件,则重做日志文件的总大小为2GB。 那么async_water_mark=1.5GB,sync_water_mark=1.8GB。则:

  1. checkpoint_age<async_water_mark时,不需要刷新任何脏页到磁盘;
  2. async_water_mark<checkpoint_age<sync_water_mark时触发Async Flush,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age<async_water_mark;
  3. checkpoint_age>sync_water_mark这种情况一般很少发生,除非设置的重做日志文件太小,并且在进行类似LOAD DATA的BULK INSERT操作。此时触发Sync Flush操作,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age<async_water_mark

可见,Async/Sync Flush Checkpoint是为了保证重做日志的循环使用的可用性。

在InnoDB 1.2.x版本之前,Async Flush Checkpoint会阻塞发现问题的用户查询线程,而Sync Flush Checkpoint会阻塞所有的用户查询线程,并且等待脏页刷新完成。

从InnoDB 1.2.x版本开始——也就是MySQL 5.6版本,这部分的刷新操作同样放入到了单独的Page Cleaner Thread中,故不会阻塞用户查询线程。

由于磁盘是一种相对较慢的存储设备,内存与磁盘的交互是一个相对较慢的过程由于innodb_log_file_size定义的是一个相对较大的值,正常情况下,由前面两种checkpoint刷新脏页到磁盘,在前面两种checkpoint刷新脏页到磁盘之后,脏页对应的redo log空间随即释放,一般不会发生Async/Sync Flush checkpoint。同时也要意识到,为了避免频繁低发Async/Sync Flush checkpoint,也应该将innodb_log_file_size配置的相对较大一些。

MySQL官方版本并不能查看刷新页是从Flush列表中还是从LRU列表中进行Checkpoint的,也不知道因为重做日志而产生的Async/Sync Flush的次数。但是InnoSQL版本提供了方法,可以通过命令SHOW ENGINE INNODB STATUS来观察,如:

show engine innodb status;
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 2058485760; in additional pool allocated 0
Dictionary memory allocated 913470
Buffer pool size
Free buffers
Database pages
Old database pages 15468
Modified db pages  0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 15032929, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 15075936, created 366872, written 36656423
0.00 reads/s, 0.00 creates/s, 0.90 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: 41957, unzip_LRU len: 0
I/O sum[39]:cur[0], unzip sum[0]:cur[0]
Dirty Page too much

Dirty Page too much Checkpoint是在Master Thread 线程中每秒一次的频率实现的。

即脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数innodb_max_dirty_pages_pct控制:

SHOW GLOBAL VARIABLES LIKE 'innodb_max_dirty_pages_pct';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| innodb_max_dirty_pages_pct | 75    |
+----------------------------+-------+

当缓冲池中脏页的数量占据75%时,强制进行Checkpoint,刷新一部分的脏页到磁盘。在InnoDB 1.0.x版本之前,该参数默认值为90,之后的版本都为75。

Double Write

如果说Insert Buffer给InnoDB存储引擎带来了性能上的提升,那么Double Write带给InnoDB存储引擎的是数据页的可靠性。

双写示例

如上图所示,Double Write由两部分组成,一部分是内存中的double write buffer,大小为2MB,另一部分是物理磁盘上共享表空间连续的128个页,大小也为2MB。

在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到内存中的double write buffer区域,之后通过double write buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免操作系统缓冲写带来的问题。在完成double write页的写入后,再将double wirite buffer中的页写入各个表空间文件中。

如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的double write中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志。

redo log落盘

InnoDB存储引擎会首先将重做日志信息先放入重做日志缓冲中,然后再按时或者当事务提交时写入磁盘,这符合Force-log-at-commit原则。当重做日志写入磁盘后,缓冲池中的变更数据才会依据checkpoint机制择时写入到磁盘中,这符合WAL原则

WAL要求数据的变更写入到磁盘前,首先必须将内存中的日志写入到磁盘。

Force-log-at-commit要求当一个事务提交时,所有产生的日志都必须刷新到磁盘上,如果日志刷新成功后,缓冲池中的数据刷新到磁盘前数据库发生了宕机,那么重启时,数据库可以从日志中恢复数据。

在checkpoint择时机制中,就有重做日志文件写满的判断,所以,如前文所述,如果重做日志文件太小,经常被写满,就会频繁导致checkpoint将更改的数据写入磁盘,导致性能抖动。

操作系统的文件系统是带有缓存的,当InnoDB向磁盘写入数据时,有可能只是写入到了文件系统的缓存中,没有真正的“落袋为安”。

InnoDB的innodb_flush_log_at_trx_commit属性可以控制每次事务提交时InnoDB的行为。该属性默认为1。

  • 当属性值为0时,事务提交时,不会对重做日志进行写入操作,而是等待主线程按时写入;
  • 当属性值为1时,事务提交时,会将重做日志写入文件系统缓存,并且调用文件系统的fsync,将文件系统缓冲中的数据真正写入磁盘存储,确保不会出现数据丢失;
  • 当属性值为2时,事务提交时,也会将日志文件写入文件系统缓存,但是不会调用fsync,而是让文件系统自己去判断何时将缓存写入磁盘。

redo log 落盘配置

innodb_flush_log_at_commit是InnoDB性能调优的一个基础参数,涉及InnoDB的写入效率和数据安全。

  • 当参数值为0时,写入效率最高,但是数据安全最低;
  • 参数值为1时,写入效率最低,但是数据安全最高;
  • 参数值为2时,二者都是中等水平。一般建议将该属性值设置为1,以获得较高的数据安全性,而且也只有设置为1,才能保证事务的持久性。
正文到此结束