InnoDB 的文件组织结构
准备
MySQL版本: 8.0
从用户的角度看 InnoDB 根据不同的功能将文件分为 Redo Log 文件(ib_logfile
),Undo Log 文件(undo_xxx
),系统表文件(ibdata
),临时表文件(ibtmp
),数据文件(.ibd
)等等。当 MySQL 运行时,InnoDB 引擎内部同样需要在内存中维护这些文件,方便管理和读取,所以我们从内存中的文件管理和文件的物理结构两个方向来分析.
内存文件管理
在 InnoDB 中,表空间是基本的数据单元,比如系统表属于系统表空间(System tablespace
),Undo Log 属于(Undo Tablespaces
). 由表空间来管理其文件和元信息.
Fil_system
在 InnoDB 的内存中维护了一个Fil_system
的数据结构来缓存整个文件管理. Fil_system
分为64个shard
. 而每个shard
管理多个Tablespace
, 层级关系为: Fil_system->shard->Tablespace
层级关系为: Fil_system -> shard -> tablespace
64个shard
中,第1个即索引为0的shard
属于系统表空间,最后一个shard
即索引为63的shard
为Redo Log
的Tablespace
, 而58-61是属于Undo Log
的shard
:
1 | /** Maximum number of shards supported. */ |
在shard
中的每一个Tablespace
都有唯一的ID,通过shard_by_id()
函数寻找所属的shard
:
1 | Fil_shard *shard_by_id(space_id_t space_id) const |
每一个Tablespce
都对应一个或多个物理文件fil_node_t
。
文件管理的数据结构
InnoDB将文件分为Log文件(Redo Log、Undo Log)、系统表空间文件ibdata
、临时表空间文件、用户表空间。其中数据文件(.ibd
)都由Pages
->Extents
-> Segments
-> Tablespaces
多级组成。Tablespace
是由多个Segment
组成,而每个Segment
又由多个Extent
组成,每个Extent
由多个Page
组成,Extent
中Page
的数量由其Page
的大小决定, 对应关系如下:
1 | /** File space extent size in pages |
Page
每个Tablespace
内部都是由Page
组成,每个Page
都具有相同的大小,默认的UNIV_PAGE_SIZE
是16KB,即每个Extent
的大小为1M, Page
的大小可以由参数--innodb-page-size
配置。下图是Page
的布局图:
需要注意的是Extent
是一个物理概念,对应的是物理文件上1MB空间大小,而Segment是一个逻辑概念,便于整个B tree索引的管理.
FIL_PAGE_OFFSET
就是Page
的 Number 号, 第 0 页即为 0FIL_PAGE_TYPE
是Page
的类型,比如Tablespace
的第 0 页即为FIL_PAGE_TYPE_FSP_HDR
类型- 在第0页中
FIL_PAGE_PREV
的值被替换为FIL_PAGE_SRV_VERSION
即MySQL的版本, 比如80013
- 在第0页中
FIL_PAGE_NEXT
的值被替换为FIL_PAGE_SPACE_VERSION
即Tablespace
的ID, 比如临时表就是0xFFFFFFFD
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
为Page
所在的Tablespace的space_id
Tablespace
Tablespace
由多个Page
组成,为了管理这些Page
也需要一些Page
存放元信息,所以Page 0
、Page 1
、Page 2
和Page 3
包含了这些元信息数据:
We initially let it be 4 pages:
- page 0 is the fsp header and an extent descriptor page,
- page 1 is an ibuf bitmap page,
- page 2 is the first inode page,
- page 3 will contain the root of the clustered index of the
first table we create here. */
我们这里介绍关于文件管理的Page 0
和Page 2
. Page 0
作为Tablespace
的Header
其中保存关于Extent
的信息,而Page 2
作为管理Segment
的元信息页面.
Page 0
Page 0
包括Tablespace
的头部、Extents
和Segments
的链表管理,下图的Page 0
的布局,其中蓝色的字段表示文件链表:
图中蓝色字段表示数据类型为文件链表
FSP_SPACE_ID
表示该Tablespace的IDFSP_SIZE
表示目前Tablespace
有多少个Page
FSP_FREE_LIMIT
表示目前在空闲的Extent
上最小的尚未被初始化的Page
的Page Number
FSP_SPACE_FLAGS
表示Tablespace
的标志位FSP_FRAG_N_USED
表示FSP_FREE_FRAG
中已经使用的Page数量FSP_FREE
表示所有的Page
均为空闲的Extent
FSP_FREE_FRAG
表示Extent
中尚有Page
未被使用FSP_FULL_FRAG
表示Extent
的所有的Page
均已被使用FSP_SEG_ID
表示下一个未被使用的Segment ID
FSP_SEG_INODES_FULL
表示Segment Page
的所有的Inode
均已被使用FSP_SEG_INODES_FREE
表示Segment Page
存在空闲的Inode
Extent Descriptor
Page 0
中的字段XDES Entry
即为Extent的描述符, FSP_HEADER
后面紧跟256个EXTENT DESCRIPTOR
用于记录Extent
的描述页,大小为 40 Bytes. 其中的状态标志有6种:
1 | enum xdes_state_t { |
其中字段XDES_BITMAP
为16Bytes
即 16 * 8
bits,其中一个Page
占用两个bit
用来表示Page
是否被占用。
记录EXTENT DESCRIPTOR
信息的Page
所在的Extent
会被直接插入FSP_FREE_FRAG
,因为已经存在一个被使用了的Page
。而一个EXTENT DESCRIPTOR
的Page
只记录了256个Extent
,所以假如Page
的大小是16KB
即每隔256 * 64 = 16384
页 就需要申请一个带有EXTENT DESCRIPTOR
的Page
,后面新申请分配的Extent
依然与其他的Extent
文件链表连接。
1 | bool init_xdes = (ut_2pow_remainder(i, page_size.physical()) == 0); |
文件链表
例如FSP_FREE
是一个文件链表,有一个BaseNode
包含整个链表元素长度,并且指向第一个元素First
和最后一个元素Last
。 除了BaseNode
多了一个4 bytes
的长度标记,其他的元素都具有相同的数据结构,6 bytes
分为两部分: 4 bytes
的Page Number
, 2 bytes
的Page
在整个Tablespace的偏移,其组织形式如下图:
Page 2
Tablespace
的第三个Page
是关于Segment
信息的,类型为FIL_PAGE_INODE
,其中一个Inode
可以理解为管理一个Segment
元信息单元:
在一个Inode
即一个Segment
中:
FSEG_ID
表示Segment
的ID
FSEG_NOT_FULL_N_USED
表示Segment
中被使用的Page
数量FSEG_FREE
表示所有Page
均为空闲的Extent
FSEG_NOT_FULL
表示部分Page
空闲的Extent
FSEG_FULL
表示所有Page
均被使用的Extent
FSEG_MAGIC_N
表示一个magic numbe
用于Debug
Slot
表示一个Page
数据页(Index)
在InnoDB中,数据即索引,索引即数据,所以这里的数据页一般也指索引页,非压缩页的结构如下:
PAGE_N_DIR_SLOTS
表示数据页中页目录的数量PAGE_HEAP_TOP
指向数据页中的空闲空间的起始地址PAGE_N_HEAP
表示目前存放的 Record 数量PAGE_FREE
表示删除的 Record 的链表PAGE_GARBAGE
表示被删除的 Record 所占的 Bytes 大小PAGE_LAST_INSERT
指向最近一次插入的 RecordPAGE_DIRECTION
表示最后一个记录插入的方向PAGE_N_DIRECTION
表示连续同一个方向插入的 Record 数量PAGE_N_RECS
表示当前数据页中用户的 Record 数量PAGE_MAX_TRX_ID
表示当前数据页中最大的 Transaction IDPAGE_LEVEL
表示当前数据页在整个索引的 B+ 树中的层级PAGE_INDEX_ID
表示当前数据页所属的索引 IDPAGE_BTR_SEG_LEAF
表示 Leaf 节点对应的 Segment Header 信息PAGE_BTR_SEG_TOP
表示非 Leaf 节点对应的 Segment Header 信息PAGE_OLD_INFIMUM
表示当前数据页中最大的 RecordPAGE_OLD_SUPREMUM
表示当前数据页中最小的 Record
Infimum Record
和Supremum Record
分别代表该数据页中逻辑最大和最小的值.
数据页的页目录
Page Directory(页目录)占用两个字节,存放了 Record 在 Page 的相对偏移 weizhi, 页目录是一个稀疏目录(Sparse Directory), 即一个页目录中有多个 Record, 上下限分别为 4 和 8.在整个数据页中,会存在多个 Record 作为 Slot,即管理一个页目录. 其中 Record 中的REC_NEW_N_OWNED
字段记录该页目录的 Record 数量.
数据页 Record 的插入(page_cur_rec_insert)
- 获取Record的长度
- 从
PAGE_FREE
链表取下最近一个被删除的Record, 判断大小是否合适,假如待插入的Record超过了被删除的Record的大小, 则从Page的PAGE_HEAP_TOP
指向的位置分配空间 - 将待插入的Record拷贝至对应的存储位置
- 更新Record的前序后置Record
- 设置Record的
REC_OLD_N_OWNED
和REC_OLD_HEAP_NO
标志位 - 更新Page Header的
PAGE_DIRECTION
,PAGE_N_DIRECTION
,PAGE_LAST_INSERT
标志位 - 更新数据页的页目录(Page Directory), 即对应的
REC_NEW_N_OWNED
自增1,假如超过了PAGE_DIR_SLOT_MAX_N_OWNED
即8个Record, 数据目录需要分裂成两个 - 写Redo Log持久化插入操作
数据页 Record 的删除
- 写 Redo Log 持久化
- 将待删除的 Record 从对应的前序后置 Record 链表中删除
- 更新页目录 directory slot 对应的信息
- 将待删除的 Record 插入
PAGE_FREE
链表 - 假如 directory slot 中 Record 的数量小于
PAGE_DIR_SLOT_MIN_N_OWNED
, 则平衡 Page Directory
数据页 Record 的查找
- 调用
page_cur_search_with_match()
对指定 Page 的 Page Directory (页目录)进行二分查找定位 Record 周边的两个 slot - 从
low_rec
开始线性迭代直到up_rec
,查找符合条件的 Record.
物理文件管理
为了理解上半部分介绍的多种数据结构和不同Page
的格式的真正用途,我们需要分析 InnoDB 是如何利用这些数据结构来完成文件的管理。
物理文件创建
创建Tablespace的同时通过os_file_create()
创建物理文件, 一个Fil_shard
管理多个 Tablespace, 一个 Tablesapce 根据类型不同会创建一个或者多个文件. 例如数据Tablespace仅有一个物理文件,而系统表有多个文件.
Tablespace 创建
对于非临时表的数据Tablesapce,初始化大小为
FIL_IBD_FILE_INITIAL_SIZE
(7) 个Page, 而对于临时Tablspace, 初始化大小为FIL_IBT_FILE_INITIAL_SIZE
(5) 个Pagefil_create_tablespace()
初始化tablespace
的相关元信息包括m_flags
等等创建
ibd
文件并更新至内存文件管理Fil_system
,初始化Page 0
的FIL_Header
初始化
Page 0
的FSP_Header
字段和EXTENT DESCRIPTOR
文件链表,更新EXTENT DESCRIPTOR
列表中的第一个Extent
描述符
Tablespace
的创建流程初始化Page 0
,完成了Tablespace
中前256
个Extent Descriptor
的初始化创建。在用户创建的Tablespace
过程中,对于系统表的新建,会调用btr_create()
从而引入Segment
的创建. 对于用户普通表, 会根据是否在建表时指定Primary Key选择不同的策略来创建索引(btr_create()
)从而引入Segment
的创建(create_clustered_index_when_no_primary()/create_index()
).
Segment 创建
首先通过
fsp_reserve_free_extents()
为不同类型的 Tablespace 预留空间.寻找空闲的
Page
作为Inode
的Page
,初始化Inode
的文件链表选中一个空闲的
Inode
并对其进行初始化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19seg_id = mach_read_from_8(space_header + FSP_SEG_ID);
// 更新 Tablespace 的 Header 中 FSP_SEG_ID
mlog_write_ull(space_header + FSP_SEG_ID, seg_id + 1, mtr);
// 更新 Inode 的所属的 FSEG_ID 和 FSEG_NOT_FULL_N_USED
mlog_write_ull(inode + FSEG_ID, seg_id, mtr);
mlog_write_ulint(inode + FSEG_NOT_FULL_N_USED, 0, MLOG_4BYTES, mtr);
// 初始化文件链表 FSEG_FREE、FSEG_NOT_FULL 和 FSEG_FULL
flst_init(inode + FSEG_FREE, mtr);
flst_init(inode + FSEG_NOT_FULL, mtr);
flst_init(inode + FSEG_FULL, mtr);
mlog_write_ulint(inode + FSEG_MAGIC_N, FSEG_MAGIC_N_VALUE, MLOG_4BYTES, mtr);
for (i = 0; i < FSEG_FRAG_ARR_N_SLOTS; i++) {
// 初始化 Inode 的32个 Slot
fseg_set_nth_frag_page_no(inode, i, FIL_NULL, mtr);
}从选中的
Inode
中选取一个Extent
并从中获取空闲的Page
在B+树索引中,叶子节点的
Page
分裂过程中,需要申请一个新的Page
,而新申请的Page
为了与被分裂的Page
物理相邻,所以新申请的Page
会被预设一个hint_page_no
,根据分裂方向决定:假如分裂的方向为
FSP_DOWN
:1
2direction = FSP_DOWN;
hint_page_no = page_no - 1; // page_no为被分裂的Page的ID假如分裂的方向为
FSP_UP
:1
2direction = FSP_UP;
hint_page_no = page_no + 1; // page_no为被分裂的Page的ID
假如
hint
页所在的Extent
属于该Segment
并且hint
页属于空闲XDES_FREE_BIT
状态,直接获取该hint
页。假如
hint
所在的Extent
属于XDES_FREE
状态、而Segment
中使用的Page
数量已经超过分配的Page
数量的87.5%
并且Segment
中使用的Page
数量也超过了32
:- 尝试为
Segment
分配一个Extent
,将其加入该Segment
的FSEG_FREE
链表,获取hint
页
- 尝试为
假如申请的用户为
B+
树分裂,而Segment
中使用的Page
数量已经超过分配的Page
数量的87.5%
并且Segment
中使用的Page
数量也超过了32
:- 尝试为
Segment
分配一个Extent
,将其加入该Segment
的FSEG_FREE
链表 - 假如
B+
树分裂方向为FSP_DOWN
,返回Extent
的最后一个Page
- 尝试为
否则返回
Extent
中第一个空闲的Page
假如
hint
所在的Extent
存在空闲的Page
,直接获取该Extent
中的一个空闲Page
假如
Segment
中已经分配的Page
数量大于使用的Page
数量:- 尝试迭代
FSEG_NOT_FULL
和FSEG_FREE
链表,取得一个存在空闲Page
的Extent
然后获取一个空闲Page
- 尝试迭代
假如
Segment
使用的Page
数量小于32(FSEG_FRAG_LIMIT
):- 尝试直接为
Segment
分配一个Page
,插入Segment
自带的Slot
- 尝试直接为
上述情况都不满足的情况下,我们需要为该
Segment
申请一个新的Extent
,之后再获取空闲的Page
Extent 创建
为Segment
分配Extent
时:
- 通过
Tablespace
的Header
检查是否存在空闲的Extent
,假如存在,即从FSP_FREE
链表移除,之后返回空闲的Extent
- 否则需要新建
Extent
,最多创建4个Extent
,并且每隔FSP_EXTENT_SIZE
个Page
就需要一个创建Extent descriptor
的Page
。存在Extent descriptor
的Page
需要加入Tablespace
的FSP_FREE_FRAG
链表,完全空闲的Extent
加入FSP_FREE
链表 Extent
申请分配完成后,从FSP_FREE
链表中移除一个Extent
分配给Segment
并插入FSEG_FREE
链表。
参数解释
innodb_file_per_table
在MySQL5.6以上的版本新增了一个参数innodb_file_per_table
, 默认创建的Table
存储在系统表System Tablespace
, 打开这个参数允许用户为每一个table
创建单独的Tablespace
:
1 | mysql> SET GLOBAL innodb_file_per_table=1; |
示例中的表t2
会在所属的Database
目录下拥有独立的表空间文件t2.ibd
.
innodb_data_file_path
innodb_data_file_path
配置系统表空间文件的名字,大小,属性等。例如:
1 | [mysqld] |
示例配置两个系统表文件ibdata1
和ibdata2
, 大小分别是50M
和12M
, 其中ibdata2
可以自动扩容,但大小限制为500M
.