文章

InnoDB 表空间

独立表空间结构

区(extent)

通常,表空间中的页会非常非常多,为了更好地管理这些页,InnoDB 引入了(extent)的概念。规定一个区由连续的 64 个页组成,如果默认使用 16 KB 大小的页,那么一个区大小为 1 MB。此外,每 256 个区还会划分成一个组,大概如图所示:

从图中我们可以看出,第一组的前 3 个页类型是固定的,分别是:

  1. FSP_HDR:存储当前表空间的一些信息,以及该组所有区的信息

  2. IBUF_BITMAP:存储该组所有区的所有页关于 INSERT BUFFER 的信息

  3. INODE:存储段的相关信息

除了第一组,后面组的前 2 个页类型是固定的,分别是:

  1. XDES:全称是 extent descriptor,类似 FSP_HDR,区别在于 XDES 只存储该组所有区的信息,而 FSP_HDR 还额外存储了当前表空间的一些信息

  2. IBUF_BITMAP:存储该组所有区的所有页关于 INSERT BUFFER 的信息

InnoDB 为什么要引入区呢?

其实主要是为了规避机械硬盘随机IO性能低下的问题。我们知道使用索引进行范围查询的时候,只需要找到符合条件的最左边的记录和最右边的记录,然后沿着双向链表上的页一一扫描即可。由于是链表结构,相邻的页在物理磁盘上可能相隔很远,这时候沿着链表扫描页就是随机IO操作,性能比较差劲。为了将随机IO尽可能转化成顺序IO,InnoDB 就引入了区,一个区是物理磁盘上连续的 64 个页,当表中数据量大的时候,InnoDB 不再按页为索引分配空间,会改成按区分配,数据里足够多的时候甚至会连续分配多个连续的区,这样使用索引范围查询的时候就可以尽量接近顺序IO的性能。

段(segment)

除了区,InnoDB 还引入了(segment)来进一步优化大数据量下使用索引范围查找的性能。段是一个逻辑概念,并不对应表空间中某一块连续的物理区域。我们知道 B+ 树分叶子节点和非叶子节点,在没有段的情况下,叶子页和非叶子页在物理磁盘上的排列是混在一起的,这势必会影响顺序查找叶子节点的性能。因此,InnoDB 引入了段,叶子节点和非叶子节点都有自己独有的区,存放叶子节点的区的集合就划分成一个段(数据段),存放叶子节点的区的集合划分成另一个段(索引段),这样顺序查找叶子节点就更接近顺序IO了。

除了索引段和数据段,还有其他的段,比如:回滚段。

碎片区

每张表都至少有一个索引,一个索引有2个段,一个段至少占用 1MB 的空间,这样算下来一张表至少占用 2MB 的存储空间。但是实际情况中会存在一些数据量比较少的表,InnoDB 不想浪费这 2MB 空间,引入了碎片区(fragment)的概念。碎片区中的页可以用于不同的段,还可以不属于任何段。对于数据量很少的表,InnoDB 使用碎片页来存储数据,节省了磁盘容量开销。

因此,给段分配存储空间的策略如下:

  1. 当表中数据量很少的时候,从碎片区以页为单位给段分配存储空间

  2. 当某个段已经使用了碎片区中的 32 个页后,就会改成以完整的区为单位给段分配存储空间

知道了碎片区的概念,此时我们可以给出段的准确定义:段其实是一些完整区和一些碎片页的集合

区的分类

区大致分成以下 4 类:

区状态

说明

FREE

空闲的区(独立)

FSG

附属于某个段的区

FREE_FRAG

有剩余空间的碎片区(独立)

FULL_FRAG

没有剩余空间的碎片区(独立)

InnoDB 为了管理区以及区中页的状态,设计了一个  XDES Entry 结构,每个区都有一个对应的 XDES Entry 存储其状态信息。在 FSP_HDR 页 和 XDES 页中都有存储 XDES Entry,并且这两种页分别都存储了 256 个XDES Entry,对应所在组的 256 个区。

我们来仔细看下 XDES Entry 的结构:

从图中可以看出,XDES Entry 是一个 40 字节的结构,分为 4 个部分:

  1. Segment ID:段id,表示该区所属的段

  2. List Node:用于将若干个 XDES Entry 组成一个链表

  3. State:区状态,有 4 种取值:FREE、FSG、FREE_FRAG、FULL_FRAG

  4. Page State Bitmap:这个部分占用 16 字节,即 128 比特位,我们知道一个区有 64 个页,这 128 个比特位被划分为 64 个部分,每两个比特位对应一个页。在这两个比特位中,第一个比特位用于表示对应页是否空闲,第二个比特位暂未使用。

为什么要将区分组呢?

一个 XDES Entry 结构占40字节,算下来一个页大概可以存放几百个 XDES Entry,但是表空间中区的数量通常远不止几百个,一个页无法存储所有的 XDES Entry,因此 InnoDB 把表空间中的区分成了若干个组,一个组包含 256 个区,在每个组的第一个页面存储本组的所有区对应的 256 个 XDES Entry 结构。 

为什么要将 XDES Entry 组成链表呢?

我们先梳理下插入数据的过程:

  1. 当段中数据较少的时候,首先查看表空间中是否还有状态为 FREE_FRAG 的区(有剩余空间的碎片区),如果还有,则从中取一些碎片页插入数据;如果没有,则从表空间申请一个状态位 FREE 的区,将其状态改为 FREE_FRAG,然后从中取一些碎片页插入数据。当 FREE_FRAG 区中的页都用完时,其状态会变为 FULL_FRAG。

  2. 当段已经使用了 32 个碎片页之后,就直接申请完整的区来插入数据。  

现在有几个问题:

如何找到不同状态的区?

遍历是最直接的方式,但是当表中数据量很大的时候遍历的效率会很低(1GB 的表的区数量大于 1000 个)。因此,考虑到性能,InnoDB 选择使用 XDES Entry 中的 List Node 构建几个链表:

  1. FREE 链表:把状态为 FREE 的区对应的 XDES Entry 连接起来

  2. FREE_FRAG 链表:把状态为 FREE_FRAG 的区对应的 XDES Entry 连接起来

  3. FULL_FRAG 链表:把状态为 FULL_FRAG 的区对应的 XDES Entry 连接起来

有了这 3 个链表,就能轻松拿到我们想要的区,如我们想找的 FREE_FRAG 状态的区,只需要把 FREE_FRAG 链表的头节点拿出来就行,其他状态同理。

怎么知道哪些区属于哪个段呢? 

思路同上,InnoDB 给每个段都构建了 3 个链表,这些链表上的 XDES Entry 对应的区都属于这个段:

  1. FREE 链表:所有页面都是空闲的区对应的 XDES Entry 会放入这个链表

  2. NOT_FULL 链表:部分页面都是空闲的区对应的 XDES Entry 会放入这个链表

  3. FULL 链表:没有空闲页面的区对应的 XDES Entry 会放入这个链表

注意:属于段的这些区状态为:FSG

怎么找到这些 XDES Entry 链表的头节点或者尾节点呢?

针对这个问题,InnoDB 设计了一个叫 List Base Node 的结构,其中存储了链表头尾节点的位置:

  1. List Length:表示链表一共有多少个节点(每个节点代表一个区)

  2. First Node Page NumberFirst Node Offset 用来定位链表头节点在表空间中的位置

  3. Last Node Page NumberLast Node Offset 用来定位链表尾节点在表空间中的位置

InnoDB 把这些链表对应的 List Node Base 存储在了表空间中固定的位置,因此可以很方便地通过这些 List Base Node 定位想要找的链表。

段的结构

就像每个区都有对应的 XDES Entry 结构存储这个区中的信息一样,InnoDB 也给每个段设计了一个 INODE Entry 结构来存储这个段中的信息:

  1. Segment ID:表示这个 INODE Entry 对应的段id 

  2. NOT_FULL_N_USED:表示 NOT_FULL 链表中使用了多少个页,用于下次从 NOT_FULL 链表中快速定位可分配的空闲页

  3. List Base Node:段的 FREE、NOT_FULL、FULL 这 3 个链表的 List Base Node,记录了这些链表的头尾节点和节点数

  4. Magic Number : 标记这个 INODE Entry 是否已经被初始化,如果值为 97937874 ,则表明该 INODE Entry 已经初始化,否则没有被初始化。 (初始化指的是填充各个字段的值)

  5. Fragment Array Entry:每个 Fragment Array Entry 都对应一个碎片页,每个段可以有 32 个碎片页(Fragment Array Entry 中存储的是页号)

各种类型页的结构

FSP_HDR 页

FSP_HDR 是表空间的第一个页,存储了当前表空间以及该组所有区的信息:

  1. File Space Header:表空间头部,存储表空间的一些整体信息

  2. XDES Entry:存储对应区的信息(本组有 256 个) 

  3. Empty Space:用于填充页结构

File Space Header

  1. Space ID:当前表空间id

  2. Not Used:未使用的空间

  3. Size:当前表空间拥有的页数

  4. Free Limit:尚未被初始化的最小页号,该值之后的区对应的 XDES Entry 都没有加入 FREE 链表,即之后的区尚未被初始化,用来辅助懒初始化,目的是节省空间开销

  5. Space Flags:表空间的一些属性,后面在详细介绍

  6. FRAG_N_USED:FREE_FRAG 链表中已经使用的页数,用来快速定位空闲的页

  7. LIst Base Node for FREE List:FREE 链表的头尾节点和节点数信息

  8. LIst Base Node for FREE_FRAG List:FREE_FRAG 链表的头尾节点和节点数信息

  9. LIst Base Node for FULL_FRAG List:FULL_FRAG 链表的头尾节点和节点数信息

  10. Next Unused Segment ID:当前表空间下一个未使用的段id,创建新段的时候直接使用该值作为段id

  11. LIst Base Node for SEG_INODE_FULL:SEG_INODES_FULL链表的头尾节点和节点数信息

  12. LIst Base Node for SEG_INODE_FREE:SEG_INODES_FREE链表的头尾节点和节点数信息

如果表空间中的段特别多,一个 INODE 的页可能放不下,因此 InnoDB 设计了 LIst Base Node for SEG_INODE_FULLLIst Base Node for SEG_INODE_FREE 两个链表来区分放满 INODE Entry 的页和未放满 INODE Entry 的页。

Space Flags 在不同版本可能有出入,下面是 5.7 版本的:

名称

空间占用(bit)

说明

POST_ANTELOPE

1

文件格式是否大于 ANTELOPE

ZIP_SSIZE

4

压缩页的大小

ATOMIC_BLOBS

1

是否自动把很长的字段值放到 BLOB 页中

PAGE_SSIZE

4

页的大小

DATA_DIR

1

是否从数据目录获取表空间

SHARED

1

是否为共享表空间

TEMPORARY

1

是否为临时表空间

ENCRYPTION

1

表空间是否加密

UNUSED

18

未被使用

XDES 页

XDES 是表空间中每组(第一个组除外)第一个页,跟 FSP_HDR 页非常类似,不再赘述。

IBUF_BITMAP 页

IBUF_BITMAP 页存储了一些有关 Change Buffer 的信息。

INODE 页

我们已经介绍过 INODE Entry,而 INODE 页就是用来存储 INODE Entry 的:

  1. List Node for INODE Page List:用于构成 SEG_INODES_FULL 和 SEG_INODES_FREE 这两个 INODE 页链表 

  2. INODE Entry:用于存储段相关信息

  3. Empty Space:用于填充页结构

SEG_INODES_FULL 和 SEG_INODES_FREE 这两个链表的基节点存储在 FSP_HDR 页的 File Space Header 中。

Segment Header 结构

我们知道一个索引会产生两个段,分别是叶子节点段(数据段)和非叶子节点段(索引段),而每个段都会对应一个 INODE Entry 结构,那我们怎么知道某个段对应哪个 INODE Entry 结构呢? 

InnoDB 把段和 INODE Entry 的对应关系保存在 INDEX 根页的 Page Header 中:

Page Header of Root INDEX:

  1. PAGE_BTR_SEG_LEAF:叶子段的 Segment Header(仅在 B+ 树根页中定义)

  2. PAGE_BTR_SEG_TOP:非叶子段的 Segment Header(仅在 B+ 树根页中定义)

Segment Header:

  1. Space ID of the INODE Entry:INODE Entry 所在的表空间id

  2. Page Number of the INODE Entry:INODE Entry 所在的页号

  3. Byte Offset of the INODE Entry:INODE Entry 在页中的偏移量

综合上面对 InnoDB 独立表空间每个部分的介绍,我们可以画出它的完整结构:

系统表空间结构

系统表空间和独立表空间结构很类似,主要区别是系统表空间额外存储了整个 InnoDB 的系统属性,体现在结构上是系统表空间第一个区的第 4 页到第 8 页跟独立表空间的不同(对应页号为:3 ~ 7):

系统表空间第一个区.png

  1. insert buffer header:存储 insert buffer 的头部信息

  2. insert buffer root:存储 insert buffer 的根页

  3. TRX_SYS:存储事务系统相关信息

  4. first rollback segment:第一个回滚段的页

  5. data dictionary header:存储数据字典头部信息

此外,系统表空间的 extent 1 和 extent 2 这两个区(页号 64 ~ 191)也叫双写缓冲区(Doublewrite Buffer),跟事务和 MVCC 有关。

InnoDB 数据字典

InnoDB 除了保存着我们插入的用户数据,还需要保存许多额外的信息,比如: 

  1. 表所属的表空间

  2. 表中有哪些字段、字段对应的类型

  3. 表中有哪些索引、索引对应的字段、索引对应的根页面在哪个表空间的哪个页面

  4. 表中有哪些外键,外键对应哪个表的哪些字段

  5. 表空间对应的文件路径

InnoDB 定义了一些内部系统表(internal system table)来存储上面这些元数据,这些系统表也叫数据字典,如:

表名

说明

SYS_TABLES

整个InnoDB存储引擎中所有的表的信息

SYS_COLUMNS

整个InnoDB存储引擎中所有的列的信息

SYS_INDEXES

整个InnoDB存储引擎中所有的索引的信息

SYS_FIELDS

整个InnoDB存储引擎中所有的索引对应的列的信息

SYS_FOREIGN

整个InnoDB存储引擎中所有的外键的信息

SYS_FOREIGN_COLS

整个InnoDB存储引擎中所有的外键对应列的信息

SYS_TABLESPACES

整个InnoDB存储引擎中所有的表空间信息

SYS_DATAFILES

整个InnoDB存储引擎中所有的表空间对应的文件路径信息

SYS_VIRTUAL

整个InnoDB存储引擎中所有的虚拟生成列的信息

其中,最重要的是 SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS 这 4 张基本系统表(basic system table),只要有了这 4 张表,就意味着可以获取其他系统表以及用户定义的表的所有元数据(如:有哪些列、有哪些索引、某个索引的根页页号是多少) 。

下面,我们来看下这 4 张基本系统表的结构:

SYS_TABLES 表

列名

说明

NAME

表名

ID

该表的唯一 id

N_COLS

该表拥有的列数

TYPE

表的类型,记录了文件格式、行格式、压缩等信息

MIX_ID

已过时

MIX_LEN

表的一些额外属性

CLUSTER_ID

未使用

SPACE

该表所属的表空间 id

SYS_TABLES 有两个索引:

  1. 以 NAME 列为主键的聚簇索引

  2. ID 列的二级索引

SYS_COLUMNS 表

列名

说明

TABLE_ID

该列所属的表的 id

POS

该列在表中是第几列

NAME

该列的名称

MTYPE

主数据类型(main data type),如:INT、CHAR、VARCHAR、FLOAT、DOUBLE 等

PRTYPE

精确数据类型(precise type),用于修饰主数据类型,如:是否允许为 NULL、是否允许负数等

LEN

该列最多占用的字节数

PREC

该列的精度,默认为 0

SYS_COLUMNS 只有一个索引:以 (TABLE_ID, POS) 为主键的聚簇索引

SYS_INDEXES 表

列名

说明

TABLE_ID

该索引所属表的 id

ID

该索引的唯一 id

NAME

该索引的名称

N_FIELDS

该索引包含的列个数

TYPE

该索引的类型,如:聚簇索引、唯一索引、普通的二级索引、更改缓冲区的索引、全文索引等

SPACE

该索引根页所属的表空间

PAGE_NO

该索引根页的页号

MERGE_THRESHOLD 

页合并阈值(比例),如果某个页中的记录被删除到低于这个阈值,就把该页和相邻页合并

SYS_INDEXES 表只有一个索引:以 (TABLE_ID, ID) 为主键的聚簇索引

SYS_FIELDS 表

列名

说明

INDEX_ID

该索引列所属的索引的id

POS

该索引列在索引中是第几列

NAME

该索引列的名称

SYS_FIELDS 只有一个索引:以 (INDEX_ID, POS) 为主键的聚簇索引

这 4 张表存储了其他表的元数据,那它们自己的元数据谁来存储呢?InnoDB 选择把它们的部分元数据(有哪些列、有哪些索引)硬编码到了代码中,同时把它们的索引的根页页号等信息存储在了系统表的第 8 个页(页号为 7)中,即上图中的 SYS: data dictionary header 页:

  1. Data Dictionary Header:数据字典头部信息,记录基本系统表的根页面位置以及InnoDB存储引擎的一些全局信息

    1. Max Row ID:row_id 的列插入数据时使用该值,全局共享

    2. Max Table ID:每次新建一个表时,就会把该值作为该表的id

    3. Max Index ID:每次新建一个索引时,就会把该值作为该索引的id

    4. Max Space ID:每次新建一个表空间时,就会把该值作为该表空间的id 

    5. Mix ID Low:没啥用

    6. Root of SYS_TABLES clust index:SYS_TABLES 表聚簇索引的根页页号

    7. Root of SYS_TABLE_IDS sec index:SYS_TABLES 表为 id 列建立的二级索引的根页页号

    8. Root of SYS_COLUMNS clust index:SYS_COLUMNS 表聚簇索引的根页页号

    9. Root of SYS_INDEXES clust index:SYS_INDEXES 表聚簇索引的根页页号

    10. Root of SYS_FIELDS clust index:SYS_FIELDS 表聚簇索引的根页页号

  2. Segment Header:段头部信息,记录本页面所在段对应的 INODE Entry 位置信息(有 Segment Header 部分,说明专门使用了一个段来给数据字典相关信息分配存储空间)

注意:我们无法直接访问这些内部系统表的数据,但是 information_schema 数据库提供了一些 INNODB_SYS_ 开头的表,这些表的数据来源于那些内部系统表:

mysql use information_schema;.png

License:  CC BY 4.0