InnoDB文件物理结构解析1 - 概述

之前立下的Flag, 经过这段时间断断续续的折腾,总算告一段落。写的分析程序(库)已经可以对InnoDB的表空间的数据文件(.ibd)进行的基本的解析,通过程序编写也对ibd文件的文件结构有了一定的了解。程序已经放在github上:
https://github.com/Li-Xiang/mysql-file-parser

项目用Java编写maven管理, 你可以clone下来后导入自己的IDE, 本文后续我基于该程序进行介绍。

在查阅资料过程发现网上已经有大量的关于ibd文件结构的文章,不想大量摘抄, 选了一些文章质量比较高连接,推荐给大家:

  • Jeremy Cole的blog以及他的项目innodb_diagrams: 主要是"InnoDB_Structures.pdf"这个文件。

  • 淘宝的"PolarDB 数据库内核月报"上面的文章质量也很高,相关的文章有:
    http://mysql.taobao.org/monthly/2016/02/01/
    http://mysql.taobao.org/monthly/2017/11/01/
    http://mysql.taobao.org/monthly/2018/04/03/
    http://mysql.taobao.org/monthly/2019/10/01/

1. InnoDB表空间和数据文件

与Oracle数据库类似,在InnoDB存储引擎中,所有的用户数据(InnoDB tables and associated indexes)都存放在表空间(Tablespace)中,有两种类型的表空间:General(or Shared) Tablespaces和File-Per-Table Tablespace。类似的, 表空间只是逻辑概念,表空间的数据文件存储在datadir目录中, 以.ibd做为扩展名,本文要讨论的就是这些.ibd文件,也就是InnoDB表空间的(数据文件的)物理存储结构。

这里讨论的是InnoDB中普通(用户创建)表及相关索引的文件存储结构, 如果你查阅MySQL官方文档, InnoDB中还包含有"System Tablespace", “Undo Tablespaces”, “Temporary Tablespace”, 这些表空间是MySQL内部使用的, 存储数据库系统相关信息, 不在文讨论范围, 也不适用本文的内容。

InnoDB默认将表创建在file-per-table表空间中,也就是一个Table独立存储为一个单独的".ibd"文件。该行为受innodb_file_per_table=on (default)参数控制, 如果将该参数值设置为off, 将导致InnoDB将表创建在系统表空间内。一个表空间至少包含一个数据文件,一个数据文件(.idb)只属于一个表空间,不同的表空间使用一个space_id来唯一标识。可以通过系统试图I_S.innodb_tables和I_S.innodb_tablespaces查看:

root@localhost [information_schema]> select table_id,name,space, row_format,space_type from information_schema.innodb_tables where name in ('mysql/db','mysql/user','jforum/jforum_config');
+----------+----------------------+------------+------------+------------+
| table_id | name                 | space      | row_format | space_type |
+----------+----------------------+------------+------------+------------+
|     1025 | mysql/db             | 4294967294 | Dynamic    | General    |
|     1026 | mysql/user           | 4294967294 | Dynamic    | General    |
|     1400 | jforum/jforum_config |        343 | Dynamic    | Single     |
+----------+----------------------+------------+------------+------------+
3 rows in set (0.00 sec)

root@localhost [information_schema]> select space,name, row_format, page_size,space_type,server_version,state from information_schema.innodb_tablespaces where space in (4294967294,343 );
+------------+----------------------+------------+-----------+------------+----------------+--------+
| space      | name                 | row_format | page_size | space_type | server_version | state  |
+------------+----------------------+------------+-----------+------------+----------------+--------+
| 4294967294 | mysql                | Any        |     16384 | General    | 8.0.18         | normal |
|        343 | jforum/jforum_config | Dynamic    |     16384 | Single     | 8.0.18         | normal |
+------------+----------------------+------------+-----------+------------+----------------+--------+
2 rows in set (0.14 sec)

这里可以看到, mysql库下的db, user系统表的space_id都为4294967294, 他们的表空间类型General(共享空间)。用户表jforum库下的jforum_config表的space_id为343, 表空间类型为Single, 也就是File-Per-Table。这些表的行格式都是Dynamic, 后面会进行介绍。

file-per-table=on是MySQL默认设置, 也是大部分最佳实践推荐设置, 不同的表空间类型的物理存储结构会不同,目前写的程序也能解析file-per-table表空间的数据文件。

2. InnoDB行格式

我们知道InnoDB支持多种行格式(row format),行格式决定了InnoDB如何物理存储行数据(Page的格式),目前InnoDB支持4种行格式: REDUNDANT, COMPACT, DYNAMIC, and COMPRESSED.

创建表时默认的行格式受innodb_default_row_format参数控制,MySQL5.6默认值为COMPACT,MySQL5.7开始默认值为DYNAMIC。在创建表时是可以通过ROW_FORMAT选项指定表的行格式的, 也是可以通过Alter table修改的:

root@localhost [testcase]> select @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic                     |
+-----------------------------+
1 row in set (0.00 sec)

### 指定行格式为REDUNDANT
root@localhost [testcase]> CREATE TABLE tab (
    ->    id int,
    ->    str varchar(50)
    -> ) ENGINE=InnoDB ROW_FORMAT=REDUNDANT;
Query OK, 0 rows affected (0.03 sec)

root@localhost [testcase]> select name,space,row_format,space_type from information_schema.innodb_tables where name='testcase/tab';
+--------------+-------+------------+------------+
| name         | space | row_format | space_type |
+--------------+-------+------------+------------+
| testcase/tab |  1503 | Redundant  | Single     |
+--------------+-------+------------+------------+
1 row in set (0.00 sec)

### 将行格式改成DYNAMIC
root@localhost [testcase]> alter table testcase.tab ROW_FORMAT=DYNAMIC;
Query OK, 0 rows affected (0.20 sec)
Records: 0  Duplicates: 0  Warnings: 0

root@localhost [testcase]> select name,space,row_format,space_type from information_schema.innodb_tables where name='testcase/tab';
+--------------+-------+------------+------------+
| name         | space | row_format | space_type |
+--------------+-------+------------+------------+
| testcase/tab |  1504 | Dynamic    | Single     |
+--------------+-------+------------+------------+
1 row in set (0.00 sec)

不同的行格式有不同的特性,如CPU/空间特性,详细可参考官方文档:

The COMPACT row format reduces row storage space by about 20% compared to the REDUNDANT row format, at the cost of increasing CPU use for some operations. If your workload is a typical one that is limited by cache hit rates and disk speed, COMPACT format is likely to be faster. If the workload is limited by CPU speed, compact format might be slower.

行格式还有对应的文件格式概念(InnoDB File Format):

InnoDB有两种文件格式: Antelope and Barracuda。Antelope是最早期的文件格式(Antelope is the original InnoDB file format), 仅支持COMPACT和REDUNDANT行格式, 不支持DYNAMIC和COMPRESSED行格式。Barracuda是InnoDB后续引入的新文件格式, 支持COMPACT, REDUNDANT, DYNAMIC和COMPRESSED行格式。InnoDB的文件格式通过innodb-file-format参数指定,MySQL 5.7版本该参数已经废弃,在MySQL 8.0版本开始已经被移除。 官方文档的解释是该参数的目的是版本降级,但降级的目标版本已经到达了产品的生命周期(end of their product lifecycles),所以移除该参数。

行格式对ibd文件的Page解析是决定性的,例如,REDUNDANT格式记录的Extra Bytes部分占6个字节,DYNAMIC/COMPACT占5个字节。通常我们使用的都是COMPACT/DYNAMIC格式(MySQL默认),在网上大部分的文章也是基于这两种格式的,后续的介绍也只讨论这两种行格式。

DYNAMIC与COMPACT有相同的存储特性,DYNAMIC对大字段(BLOB, TEXT, VARCHAR字段,且字段的数据量非常大)的存储进行了增强。

The DYNAMIC row format offers the same storage characteristics as the COMPACT row format but adds enhanced storage capabilities for long variable-length columns and supports large index key prefixes.

当一个可变长字段太大(单个Page无法放下整行),将发生行溢出(off-page/overflow page), COMPACT行格式行溢出时,主键(Cluster Key)会存储行中的前768字节加20字节指向存放溢出数据的页的指针,而DYNAMIC行格式仅存储20字节的指针, 使用的是完全的行溢出。

本文及和解析程序也不会考虑行溢出的情况,mysql-file-parser后续也许会进行完善。

3. ibd文件基本结构

ibd文件由一个个固定长度的数据页(Page)组成, 页的默认大小为16384字节(16KB), 由Innodb_page_size参数控制, 该参数只能在数据库实例初始化时指定,之后不能修改。因为页大小是固定的,所以要读取一个页是非常简单的,只需要从文件的page-number*page-size位置开始,读取page-size个字节。

Page是InnoDB的最小存储单位,Page太小了,类似的,为提高空间管理效率,InnoDB还有区(extent)和段(segment)的逻辑概念。区是由若干连续的页组成,而段又由若干的连续的区组成,区是InnoDB分配空间的基本单位。更多关于extent和segment的介绍,请移步: File Space Management。

ibd中有多种不同类型的页,每种类型的页有各种不同的用途,但所有的页都有相同的基本结构(FIL Header和FIL Trailer):

    0 +-----------------------------+
      |    FIL Header (38 bytes)    |
   38 +-----------------------------+
      | Other headers and Page data |
      |  depending on Page Type.    |
16376 +-----------------------------+
      |    FIL Trailer (8 bytes)    |
16384 +-----------------------------+

其中FIL Header包含一个页的基本信息,解析FIL Header是解析整个数据页的第一步。FIL Header的结构如下:

 0 +-----------------------------------+		
   | CHECKSUM                          | //FIL_PAGE_SPACE_OR_CHKSUM
 4 +-----------------------------------+
   | FIL_PAGE_OFFSET                   | // PAGE NUMBER
 8 +-----------------------------------+
   | FIL_PAGE_PREV                     |
12 +-----------------------------------+
   | FIL_PAGE_NEXT                     |
16 +-----------------------------------+
   | FIL_PAGE_LSN                      |
24 +-----------------------------------+
   | FIL_PAGE_TYPE                     |
26 +-----------------------------------+
   | FIL_PAGE_FILE_FLUSH_LSN           |
34 +-----------------------------------+
   | SPACE_ID                          | //FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
38 +-----------------------------------+

下面以一个具体的ibd文件来说明, 完整的测试程序放在org.littlestar.mysql.ibd.examples下

public class IbdFile1 {
	public static void main(String[] args) throws IOException, Exception {
		String fileName = "D:\\Data\\mysql\\8.0.18\\data\\sakila\\film.ibd";
		try (IbdFileParser parser = new IbdFileParser(fileName)) {
			StringBuilder buff = new StringBuilder();
			buff.append("PAGE   CHECKSUM PAGE_OFFSET  PAGE_PREV  PAGE_NEXT           PAGE_LSN              PAGE_TYPE           FLUSH_LSN SPACE_ID\n")
			    .append("---- ---------- ----------- ---------- ---------- ------------------ ----------------------- ------------------ --------\n");
			for (long i = 0; i < parser.getPageCount(); i++) {
				Page page = parser.getPage(i);
				FilHeader filHeader = page.getFilHeader();
				buff.append(String.format("%4d ", i))
					.append(String.format("0x%4s ", toHexString(filHeader.getCheckSumRaw())))
					.append(String.format("%11d ", filHeader.getPageOffset()))
					.append(String.format("%10d ", filHeader.getPreviousPage()))
					.append(String.format("%10d ", filHeader.getNextPage()))
					.append(String.format("0x%16s ", toHexString(filHeader.getPageLSNRaw())))
					.append(String.format("%23s ", filHeader.getPageTypeName()))
					.append(String.format("0x%16s ", toHexString(filHeader.getFlushLSNRaw())))
					.append(String.format("%8d", filHeader.getSpaceId()))
					.append("\n");
			}
			System.out.println(buff);
		}
	}
}

/ 程序执行的输出如下:
PAGE   CHECKSUM PAGE_OFFSET  PAGE_PREV  PAGE_NEXT           PAGE_LSN              PAGE_TYPE           FLUSH_LSN SPACE_ID
---- ---------- ----------- ---------- ---------- ------------------ ----------------------- ------------------ --------
   0 0xfaf2f6ae           0      80018          1 0x00000002fc051196   FIL_PAGE_TYPE_FSP_HDR 0x0000000000000000      387
   1 0x90b09fbd           1          0          0 0x00000002fc052bde    FIL_PAGE_IBUF_BITMAP 0x0000000000000000      387
   2 0xe86bb12b           2          0          0 0x00000002fc051196          FIL_PAGE_INODE 0x0000000000000000      387
   3 0x60b74d0c           3 4294967295 4294967295 0x00000002fbf0571c            FIL_PAGE_SDI 0x0000000000000000      387
   4 0x0f666da9           4 4294967295 4294967295 0x00000002fc051196          FIL_PAGE_INDEX 0x0000000000000000      387
   5 0x56c9b259           5 4294967295 4294967295 0x00000002fc0309a4          FIL_PAGE_INDEX 0x0000000000000000      387
   6 0xa0af9ead           6 4294967295 4294967295 0x00000002fc0541a9          FIL_PAGE_INDEX 0x0000000000000000      387
   7 0xacea1280           7 4294967295 4294967295 0x00000002fc0541b9          FIL_PAGE_INDEX 0x0000000000000000      387
   8 0xff743aa0           8 4294967295          9 0x00000002fbfe3efc          FIL_PAGE_INDEX 0x0000000000000000      387
   9 0x6a968cda           9          8         10 0x00000002fbff0c14          FIL_PAGE_INDEX 0x0000000000000000      387
  10 0x4bdf777b          10          9         11 0x00000002fbffd9f7          FIL_PAGE_INDEX 0x0000000000000000      387
  11 0x71aec053          11         10         12 0x00000002fc00a6b8          FIL_PAGE_INDEX 0x0000000000000000      387
  12 0xd01c16cf          12         11         13 0x00000002fc01726a          FIL_PAGE_INDEX 0x0000000000000000      387
  13 0x7db2fcf1          13         12         14 0x00000002fc02400c          FIL_PAGE_INDEX 0x0000000000000000      387
  14 0xf1b7f350          14         13         15 0x00000002fc037b0d          FIL_PAGE_INDEX 0x0000000000000000      387
  15 0xd4adea4e          15         14         18 0x00000002fc044660          FIL_PAGE_INDEX 0x0000000000000000      387
  16 0x0353247e          16 4294967295         17 0x00000002fc0309a4          FIL_PAGE_INDEX 0x0000000000000000      387
  17 0xa120c89b          17         16 4294967295 0x00000002fc054199          FIL_PAGE_INDEX 0x0000000000000000      387
  18 0xf3b509e5          18         15         19 0x00000002fc051196          FIL_PAGE_INDEX 0x0000000000000000      387
  19 0x764b4910          19         18         20 0x00000002fc051196          FIL_PAGE_INDEX 0x0000000000000000      387
  20 0xefc75205          20         19 4294967295 0x00000002fc054176          FIL_PAGE_INDEX 0x0000000000000000      387
  21 0x00000000           0          0          0 0x0000000000000000 FIL_PAGE_TYPE_ALLOCATED 0x0000000000000000        0

可以看到FIL_PAGE_OFFSET就是页数据文件中的编号,从0开始, 文件的第一个页为FIL_PAGE_TYPE_FSP_HDR, PAGE_PREV和PAGE_NEXT用于构建双向链表,后续会拿FIL_PAGE_INDEX做进一步介绍,4294967295对应的是0xffffffff,代表链表的头/尾,如果一个节点PAGE_PREV和PAGE_NEXT的是都是0xffffffff说明这是个根节点或者这个页类型没有链表结构。

预分配页FIL_PAGE_TYPE_ALLOCATED所有值都是0,在被时候后会被修改为对应的值。

因为是File-Per-Table, 所以SPACE_ID都是387, 可以通过innodb_tablespaces确认:

root@localhost [testcase]> select space,name, row_format, page_size,space_type,state from information_schema.innodb_tablespaces where space =387;
+-------+-------------+------------+-----------+------------+--------+
| space | name        | row_format | page_size | space_type | state  |
+-------+-------------+------------+-----------+------------+--------+
|   387 | sakila/film | Dynamic    |     16384 | Single     | normal |
+-------+-------------+------------+-----------+------------+--------+
1 row in set (0.14 sec)

每种页都有不同的用途,用于存储用户表和相关索引数据的是FIL_PAGE_INDEX页,后续会重点介绍该页,MySQL8.0后新增了FIL_PAGE_SDI页,存储了表和空间相关的元数据,因为存储结构与FIL_PAGE_INDEX页一样,也会一并介绍。

你可能感兴趣的:(MySQL,mysql)