InnoDB引擎之内存与磁盘结构

发布时间 2023-04-10 20:42:43作者: 邴越

 

一、逻辑存储结构

 

 

 

 

 

 

1、表空间 (Tablespace)

表空间 (Tablespace)是一个逻辑容器,在一个表空间中可以有一个或多个段,一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。

 

2、段 (Segment)

段 (Segment)由一个或多个区组成,段中不要求区与区之间是相邻的。段是 数据库中的分配单位,不同类型的数据库对象以不同的段形式存在,当我们创建数据表、索引时,会创建对应的段。

 

3、区 (Extent)

区 (Extent)是比页大一级的存储结构,在InnoDB存储引擎中 ,一个区会分配64个连续的页。因为InnoDB中页的默认大小为16KB,所以一个区的大小是1MB=64*16KB。

 

4、页 (Page) 

页 (Page) 是磁盘和内存之间交互的基本单位 ,也就是说数据库管理存储空间的基本单位是页,数据库I/O操作的最小单位是页 (InnoDB页默认大小16KB)

若设置完成,则所有表中页的大小都为innodb_page_size,不可以再次对其进行修改,除非通过mysqldump导入和导出操作来产生新的库。

 

二、内存及磁盘架构

 

 

 

 

InnoDB architecture diagram showing in-memory and on-disk structures.

 

1、缓冲池(Buffer Pool)

缓冲池,简单来说就是一块内存区域。它存在的原因之一是为了避免每次都去访问磁盘,把最常访问的数据放在缓存里,提高数据的访问速度。BP以Page页为单位,默认大小16K,BP的底层采用链表数据结构管理Page。在InnoDB访问表记录和索引时会在Page页中缓存,以后使用可以减少磁 盘IO操作,提升效率。

 

在服务器上,通常物理内存的50%~80%的分配给缓冲池。

 

Page管理机制:

  • free page :空闲page,未被使用
  • clean page:被使用page,数据没有被修改过

dirty page:脏页,被使用page,数据被修改过,页中数据和磁盘的数据产生了不一致

 

针对上述三种不同的Page类型,Innodb有定义三种不同的链表用于管理Page:

  • free list :表示空闲缓冲区,管理free page
  • flush list:表示需要刷新到磁盘的缓冲区,管理dirty page,内部page按修改时间排序。脏页即存在于flush链表,也在LRU链表中,但是两种互不影响LRU链表负责管理page的可用性和释放,而flush链表负责管理脏页的刷盘操作。最早修改的在链表的尾部,所以刷盘从尾部开始刷盘
  • lru list:表示正在使用的缓冲区,管理clean page和dirty page,缓冲区以 midpoint为基点,前面链表称为new列表区,存放经常访问的数据(热点数据),占63%;后面的链表称为old列表区,存放较少数据,占37%。

 

2、写缓冲区(Change Buffer)

 
当更新一条记录时,该记录在BufferPool存在,直接在BufferPool修改,一次内存操作。如果该记录在BufferPool不存在(没有命中),会直接在ChangeBuffer进行一次内存操作,不用再去磁盘查询数据,避免一次磁盘IO。当下次查询记录时,会先进磁盘读取,然后再从 ChangeBuffer中读取信息合并,最终载入BufferPool中。

★ 写缓冲区,仅适用于非唯一普通索引页:

如果在索引设置唯一性,在进行修改时,InnoDB必须要做唯一性校验,因此必须查询磁盘,做一次IO操作。会直接将记录查询到BufferPool中,然后在缓冲池修改,不会在 ChangeBuffer操作
 

3、日志缓冲区(Log Buffer)

 
当在MySQL中对InnoDB表进行更改时,这些更改首先存储在InnoDB日志缓冲区的内存中,然后写入redo logs的文件中。日志缓冲区是内存存储区域,用于保存要写入磁盘上的日志文件的数据。日志缓冲区大小由innodb_log_buffer_size 变量定义,默认大小为16MB。

4、自适应哈希索引(Adaptive Hash Index)

 
自适应hash索引是一种键值对的存储结构,存储的是热点页所在的记录。InnoDB存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。
系统表空间(The System Tablespace)
就是我们常说的共享表空间,系统表空间(在操作系统上体现就是ibdata文件)是我们在初始化mysql实例时生成的(在初始化mysql实例时会读取my.cnf中的innodb_data_file_path参数,然后初始出相应的文件ibdata1、ibdata2 …,至于文件多大,有多少个,看你my.cnf中的参数是怎样设置的)。
独立表空间(File-Per-Table Tablespaces)
独立表空间是一个单表表空间,该表创建于自己的数据文件中,而非创建于系统表空间中。当innodb_file_per_table选项开启时,表将被创建于表空间中。否则, innodb将被创建于系统表空间中。每个表文件表空间由一个.ibd数据文件代表,该文件默认被创建于数据库目录中。表空间的表文件支持动态(dynamic)和压缩 (commpressed)行记录格式。
通用表空间(General Tablespaces)
通用表空间为通过create tablespace语法创建的共享表空间。通用表空间可以创建于mysql数据目录外的其他表空间,其可以容纳多张表,且其支持所有的行记录格式。
撤销表空间(Undo Tablespaces)
撤销表空间由一个或多个包含Undo日志文件组成。在MySQL 5.7版本之前Undo占用的是System Tablespace共享区,从5.7开始将Undo从System Tablespace分离了出来。InnoDB使用的undo表空间由innodb_undo_tablespaces配置选项控制,默认为0。参数值为0表示使用系统表空间ibdata1;大于0表示使用undo表空间undo_001、 undo_002等。
临时表空间(Temporary Tablespaces)
分为session temporary tablespaces 和global temporary tablespace两种。session temporary tablespaces 存储的是用户创建的临时表和磁盘内部的临时表。global temporary tablespace储存用户临时表的回滚段(rollback segments )。mysql服务器正常关闭或异常终止时,临时表空间将被移除,每次启动时会被重新创建。

 

三、页的内部结构

页如果按类型划分的话,常见的有:

  • 数据页(B-tree Node)
  • Undo页(Undo Log Page)
  • 系统页(System Page)
  • 事务数据页(Transaction system Page)
  • 插入缓冲位图页(Insert Buffer Bitmap)
  • 插入缓冲空闲列表页(Insert Buffer Free List)
  • 未压缩的二进制大对象页(Uncompressed BLOB Page)
  • 压缩的二进制大对象页(Compressed BLOB Page)

 

数据页是我们最常使用的页。

 

1、文件结构

InnoDB 数据页由以下 7 个部分组成:

File Header (文件头)

  • Page Header (页头)
  • Infimun 和 Supremum Records
  • User Records (用户记录,即行记录)
  • Free Space (空闲空间)
  • Page Directory (页目录)
  • File Trailer (文件结尾信息)

 

 

 

其中 File Header、Page Header、File Trailer的大小是固定的, 分别为 38、56、8 字节,这些空间用来标记该页的一些信息,如 Checksum, 数据页所在 B+ 树索引的层数等。User Records、Free Space、Page Directory 这些部分为实际的行记录存储空间,因此大小是动态的。

 

2、如何管理空闲页

Buffer Pool 是一片连续的内存空间,当 MySQL 运行一段时间后,这片连续的内存空间中的缓存页既有空闲的,也有被使用的。

那当我们从磁盘读取数据的时候,总不能通过遍历这一片连续的内存空间来找到空闲的缓存页吧,这样效率太低了。

所以,为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的「控制块」作为链表的节点,这个链表称为 Free List(空闲链表)。

 

 

 

Free List上除了有控制块,还有一个头节点,该头节点包含链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。

Free List节点是一个一个的控制块,而每个控制块包含着对应缓存页的地址,所以相当于 Free 链表节点都对应一个空闲的缓存页。

有了 Free List后,每当需要从磁盘中加载一个页到 Buffer Pool 中时,就从 Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的控制块从 Free 链表中移除。

 

3、如何管理脏页

设计 Buffer Pool 除了能提高读性能,还能提高写性能,也就是更新数据的时候,不需要每次都要写入磁盘,而是将 Buffer Pool 对应的缓存页标记为脏页,然后再由后台线程将脏页写入到磁盘。

 

那为了能快速知道哪些缓存页是脏的,于是就设计出 Flush List,它跟 Free List类似的,链表的节点也是控制块,区别在于 Flush List 的元素都是脏页。

 

 

有了 Flush 链表后,后台线程就可以遍历 Flush 链表,将脏页写入到磁盘。

 

4、如何提高缓存命中率

Buffer Pool 的大小是有限的,对于一些频繁访问的数据我们希望可以一直留在 Buffer Pool 中,而一些很少访问的数据希望可以在某些时机可以淘汰掉,从而保证 Buffer Pool 不会因为满了而导致无法再缓存新的数据,同时还能保证常用数据留在 Buffer Pool 中。

 

要实现这个,最容易想到的就是 LRU(Least recently used)算法。

 

简单的 LRU 算法并没有被 MySQL 使用,简单的 LRU 算法无法避免下面这两个问题:

  • 预读失效
  • Buffer Pool 污染

 

5、改进型LRU算法维护

普通LRU:末尾淘汰法,新数据从链表头部加入,释放空间时从末尾淘汰。

 

改性LRU:链表分为new和old两个部分,加入元素时并不是从表头插入,而是从中间midpoint位置插入,如果数据很快被访问,那么page就会向new列表头部移动,如果数据没有被访问,会逐步向old尾部移动,等待淘汰。

 

每当有新的page数据读取到buffer pool时,Innodb引擎会判断是否有空闲页,是否足够,如果有就将free page从free list列表删除,放入到LRU列表中。没有空闲页,就会根据LRU算法淘汰LRU链表默认的页,将内存空间释放分配给新的页。

 

 

5、脏页什么时候会被刷入磁盘

引入了 Buffer Pool 后,当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,但是磁盘中还是原数据。

因此,脏页需要被刷入磁盘,保证缓存和磁盘数据一致,但是若每次修改数据都刷入磁盘,则性能会很差,因此一般都会在一定时机进行批量刷盘。

可能大家担心,如果在脏页还没有来得及刷入到磁盘时,MySQL 宕机了,不就丢失数据了吗?

 

这个不用担心,InnoDB 的更新操作采用的是 Write Ahead Log 策略,即先写日志,再写入磁盘,通过 redo log 日志让 MySQL 拥有了崩溃恢复能力。

下面几种情况会触发脏页的刷新:

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

 

在我们开启了慢 SQL 监控后,如果你发现**「偶尔」会出现一些用时稍长的 SQL**,这可能是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,导致数据库操作抖动。

如果间断出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。