innodb 物理文件解析
1 综述
innodb的物理文件包括系统表空间文件ibdata,用户表空间文件ibd,日志文件ib_logfile,临时表空间文件ibtmp,undo独立表空间等。
系统表空间是innodb最重要的文件,它记录包括元数据信息,事务系统信息,ibuf信息,double write等关键信息。
用户表空间文件通常分为两类,一类是当innodb_file_per_table打开时,一个用户表空间对应一个文件,另外一种则是5.7版本引入的所谓General Tablespace,在满足一定约束条件下,可以将多个表创建到同一个文件中。
日志文件主要用于记录redo log。innodb在所有数据变更前,先写redo日志。为保证redo日志原子写入,日志通常以512字节的block单位写入。但由于现代文件系统升级,block_size通常设置到了4k,因此innodb也提供了一个选项支持redo日志以4k为单位写入。
临时表空间文件用于存储所有非压缩的临时表,第1~32个临时表专用的回滚段也存放在该文件中。由于临时表的本身属性,该文件在重启时会重新创建。
undo独立表空间是innodb的一个可选项,由innodb_undo_tablespaces配置。默认情况下,该值为0,即undo数据是存储在ibdata中。innodb_undo_tablespaces 设置为非0,可使得undo 回滚段分配到不同的文件中,目前开启undo tablespace 只能在install阶段进行。
上述文件除日志文件外,都具有较为统一的物理结构。所有物理文件由页(page 或 block)构成,在未被压缩情况下,一个页的大小为UNIV_PAGE_SIZE(16384,16K)。不同用途的页具有相同格式的页头(38)和页尾(8),其中记录了页面校验值,页面编号,表空间编号,LSN等通用信息,详见下表。所有page通过一定方式组织起来,下面我们分别从物理结构,逻辑结构,文件管理过程来具体了解innodb的文件结构。
FIL Header / Trailer |
---|
checksum 校验值 (4) |
offset 页面编号 (4) |
previous page 前页编号(4) |
next page 后页编号 (4) |
lsn for last modification 最后修改的lsn (8) |
page type 页面类型 (4) |
flush lsn 刷盘lsn,只在page 0保存 (4) |
space id 表空间编号 (4) |
... |
old-style checksum 页尾校验值 (4) |
low 32bits of lsn lsn的低4字节 (4) |
2 文件物理结构
2.1 基本物理结构
innodb 的每个数据文件都归属于一个表空间(tablespace),不同的表空间使用一个唯一标识的space id来标记。值得注意的是,系统表空间ibdata虽然包括不同文件ibdata1, ibdata2…,但这些文件逻辑上是相连的,这些文件同属于space_id为0的表空间。
表空间内部,所有页按照簇(extent)为物理单元进行划分和管理。extent内所有页面物理相邻。对于不同的page size,对应的extent大小也不同,对应为:
page size | extent size |
---|---|
4 KiB | 256 pages = 1 MiB |
8 KiB | 128 pages = 1 MiB |
16 KiB | 64 pages = 1 MiB |
32 KiB | 64 pages = 2 MiB |
64 KiB | 64 pages = 4 MiB |
通常情况下,extent由64个物理连续的页组成,表空间可以理解为由一个个extent物理相邻的extent组成。为了组织起这些extent,每个extent都有一个占40字节的XDES entry。格式如下:
Macro | bytes | Desc |
---|---|---|
XDES_ID | 8 | 如果该extent归属某个segment的话,则记录其ID |
XDES_FLST_NODE | 12 | (FLST_NODE_SIZE) 维持extent链表的双向指针节点 |
XDES_STATE | 4 | 该extent的状态信息,包括:XDES_FREE,XDES_FREE_FRAG,XDES_FULL_FRAG,XDES_FSEG |
XDES_BITMAP | 16 | 总共16*8= 128个bit,用2个bit表示extent中的一个page,一个bit表示该page是否是空闲的(XDES_FREE_BIT),另一个保留位,尚未使用(XDES_CLEAN_BIT) |
利用XDES entry,我们可以方便地了解到该extent每页空闲与否,以及其当前状态。
所有XDES entry都统一放在extent描述页中,一个extent描述页至多存放256个XDES entry,用于管理其随后物理相邻的256个extent(256*64 = 16384 page),如下图所示所示:
由图可见,每个XDES entry有严格对应的页面,其对应页面上下界可以描述为:
min_scope = extent 描述页 page_no + xdes 编号 * 64
max_scope =( extent 描述页 page_no + xdes 编号 * 64 )+63
值得注意的是,其中 page 0的extent描述页还记录了与该table space相关的信息(FSP HEADER),其类型为FIL_PAGE_TYPE_FSP_HDR。其他extent描述页的类型相同,为FIL_PAGE_TYPE_XDES。
2.2 系统数据页
系统表空间(ibdata)不仅存放了SYS_TABLE / SYS_INDEX 等系统表的数据,还存放了回滚信息(undo),插入缓冲索引页(IBUF bitmap),系统事务信息(trx_sys),二次写缓冲(double write)等信息。
innodb中核心的数据都存放在ibdata中的系统数据页中。系统数据页主要包括:FIL_PAGE_TYPE_FSP_HDR, FIL_PAGE_IBUF_BITMAP, FIL_PAGE_TYPE_SYS, IBUF_ROOT_PAGE, FIL_PAGE_TYPE_TRX_SYS, FIL_PAGE_TYPE_SYS, DICT_HDR_PAGE等。
FIL_PAGE_TYPE_FSP_HDR/FIL_PAGE_TYPE_XDES
extent描述页(page 0/16384/32768/... ),上文已述及,故不再展开。FIL_PAGE_IBUF_BITMAP
ibdata第2个page类型为FIL_PAGE_IBUF_BITMAP,主要用于跟踪随后的每个page的change buffer信息。由于bitmap page的空间有限,同样每隔256个extent Page之后,也会在XDES PAGE之后创建一个ibuf bitmap page。FIL_PAGE_INODE
ibdata的第3个page的类型为FIL_PAGE_INODE,用于管理数据文件中的segment,每个inode页可以存储FSP_SEG_INODES_PER_PAGE(默认为85)个记录。segment是表空间管理的逻辑单位,每个索引占用2个segment,分别用于管理叶子节点和非叶子节点。关于segment的详细介绍,将在第三节展开。FSP_IBUF_HEADER_PAGE_NO 和 FSP_IBUF_TREE_ROOT_PAGE_NO
上述两个页分别是Ibdata的第4个page和第5个page。change buffer本质上也是btree结构,其root页固定在第5个page FSP_IBUF_TREE_ROOT_PAGE_NO。 由于FSP_IBUF_TREE_ROOT_PAGE_NO中原先用于记录leaf inode entry的字段被用于维护空闲page链表了,因此ibdata需要使用第4页FSP_IBUF_TREE_ROOT_PAGE_NO 来对ibuf进行空间管理。FSP_TRX_SYS_PAGE_NO
ibdata第6个page的类型为FSP_TRX_SYS_PAGE_NO,记录了innodb重要的事务系统信息,包括持久化的最大事务ID,以及128个rseg(rollback segment)的地址,double write位置等。这128个rseg中,rseg0固定在ibdata中,rseg1-rseg32用于管理临时表,rseg33-rseg128 当未开启undo独立表空间 (innodb undo tablespace = 0)时,仍放在ibdata中,否则放在undo独立表空间中。每个rseg中记录了1024个slot,每个slot也都可对应一个事务,用于管理该事务的undo记录。由于每个slot也需要申请和释放page,因此每个slot也对应一个segment(空间管理逻辑单位)。FSP_DICT_HDR_PAGE_NO
ibdata第8个page的类型为FSP_DICT_HDR_PAGE_NO,用来存储数据词典表的信息 。该页存储了SYS_TABLES,SYS_TABLE_IDS,SYS_COLUMNS,SYS_INDEXES和SYS_FIELDS的root page,以及当前最大的TABLE_ID/ROW_ID/INDEX_ID/SPACE_ID。当对用户表操作时,需要先从数据字典表中获取到用户表对应的表空间,以及其索引root页的page_no,才能定位到具体数据的位置,对其进行增删改查。(只有拿到数据词典表,才能根据其中存储的表信息,进一步找到其对应的表空间,以及表的聚集索引所在的page no)double write buffer
innodb使用double write buffer来防止数据页的部分写问题,在写一个数据页之前,总是先写double write buffer,再写数据文件。当崩溃恢复时,如果数据文件中page损坏,会尝试从dblwr中恢复。double write buffer总共128个page,划分为两个block。由于dblwr在安装实例时已经初始化好了,这两个block在Ibdata中具有固定的位置,page64 ~127 划属第一个block,page 128 ~191划属第二个block。
当innodb_file_per_table为off状态时,所有用户表也将和SYS_TABLE / SYS_INDEX 等系统表一样,存储在ibdata中。当开启innodb_file_per_table时,innodb会为每一个用户表建立一个独立的ibd文件。该ibd文件存放了对应用户表的索引数据和插入缓冲bitmap。 而该表的回滚数据(undo)仍记录在ibdata中。
3 文件逻辑结构
3.1 基本逻辑结构
innodb为了组织各extent,在表空间的第一个page还维护了三个extent的链表:FSP_FREE、FSP_FREE_FRAG、FSP_FULL_FRAG。分别将extent完全未被使用,部分被使用,完全被使用的Xdes entry串联起来。
innodb的逻辑管理管理单位是段(segment 或称 inode)。为节省空间,每个segment都先从表空间FREE_FRAG中分配32个页(FSEG_FRAG_ARR),当这些32个页面不够使用时。按照以下原则进行扩展:如果当前小于1个extent,则扩展到1个extent满;当表空间小于32MB时,每次扩展一个extent;大于32MB时,每次扩展4个extent。
在为segment分配空闲的extent时,如果表空间FSP_FREE上没有空闲的segment,则会为FSP_FREE重新初始化一些空闲extent。extent的分配类似于实现了一套借还机制。segment向表空间租借extent,只有segment退还该空间时,该extent才能重新出现在FSP_FREE/FSP_FULL_FRAG/FSP_FULL中。
segment内部为了管理起这些分配来的extent。也有三个extent链表:FSEG_FREE、FSEG_NOT_FULL、FSEG_FULL。也分别对应extent完全未被使用,部分被使用,完全被使用的Xdes entry。这三个链表的地址被记录在inode entry中。INode entry的具体结构如下表所示:
Macro | bits | Desc |
---|---|---|
FSEG_ID | 8 | 该inode归属的Segment ID,若值为0表示该slot未被使用 |
FSEG_NOT_FULL_N_USED | 8 | FSEG_NOT_FULL链表上被使用的Page数量 |
FSEG_FREE | 16 | 完全没有被使用并分配给该Segment的extent链表 |
FSEG_NOT_FULL | 16 | 至少有一个page分配给当前Segment的extent链表,全部用完时,转移到FSEG_FULL上,全部释放时,则归还给当前表空间FSP_FREE链表 |
FSEG_FULL | 16 | 分配给当前segment且Page完全使用完的extent链表 |
FSEG_MAGIC_N | 4 | Magic Number |
FSEG_FRAG_ARR 0 | 4 | 属于该Segment的独立Page。总是先从全局分配独立的Page,当填满32个数组项时,就在每次分配时都分配一个完整的extent,并在XDES PAGE中将其Segment ID设置为当前值 |
…… | …… | …… |
FSEG_FRAG_ARR 31 | 4 | 总共存储32个记录项 |
从上文我们可以看到,innodb通过inode entry来管理每个Segment占用的数据页,每个segment可以看做一个文件页维护单元。inode entry所在的inode page有可能存放满,因此又通过头Page(FIL_PAGE_TYPE_FSP_HDR)中维护了两个inode Page链表FSP_SEG_INODES_FULL和FSP_SEG_INODES_FREE。前者对应没有空闲inode entry的inode page链表,后者对应的至少有一个空闲inode entry的inode page链表。
3.2 索引
ibd文件中真正构建起用户数据的结构是BTREE。表中的每一个索引对应一个btree。主键(cluster index)对应btree的叶子节点上记录了行的全部列数据(加上transaction id列及rollback ptr)。当表中无主键时,innodb会为该表每一行分配一个唯一的rowID,并基于它构造btree。如果表中存在二级索引(secondary index),那么其BTREE叶子节点存储了键值加上cluster index索引键值。
每个btree使用两个Segment来管理数据页,一个管理叶子节点(leaf segment),一个管理非叶子节点(non-leaf segment)。这两个segment的inode entry地址记录在btree的root page中。root page分配在non-leaf segment第一个碎片页上(FSEG_FRAG_ARR)。
当对一个表进行增删改查的操作时,我们首先需要从ibdata的第8页FSP_DICT_HDR_PAGE_NO中load改表的元数据信息,从SYS_INDEXES表中获取该表各索引对应的root page no,进而通过root page对这个表的用户数据btree进行操作。表空间的逻辑结构如下图所示:
3.3 索引页数据
索引最基本的页类型为FIL_PAGE_INDEX,其结构如下表所示。Index Header中记录了page所在Btree层次,所属index ID,page directory槽数等与页面相关的信息。Fseg Header中记录了该index的leaf-segment和non-leaf segment的inode entry,system records包括infimum和supremum,分别代表该页最小、最大记录虚拟记录。page directory是页内记录的索引。Btree只能检索到记录所在的page,page内的检索需要使用到通过page directory构建起的二分查找。
Index Page |
---|
FILHeader (38) |
Index Header (36) |
Fseg Header (20) |
System Records (26) |
User Records |
Free Space |
Page Directory |
FIL trailer (8) |
innodb按行存放数据。当前MySQL支持等行格式包括antelope(compact和redundant),和barracuda(dynamic和compressed)。barracuda与antelope主要区别在于其处理行外数据等方式,barracuda只存储行外数据等地址指针,不像antelope一样存放768字节的行前缀内容。以compact行格式为例介绍行格式的具体内容,如下图所示,行由变长字段长度列表、NULL标志位、记录头信息、系统列、用户列组成。记录头信息中存放删除标志、列总数、下行相对偏移等信息、系统列包括rowID、transactionID、rollback pointer等组成。
变长字段长度 | NULL 标志位 | 记录头信息 | Field 1 | ... | Field N |
---|
4 文件管理过程
下面用精简后的源码来简单介绍innodb文件的管理过程:
4.1 btree的创建过程
btree的创建过程可以概括为:先创建non_leaf segment,利用non_leaf segment的首页(即32个碎片页中第一页)作为root page;然后创建leaf_segment;最后对root page进行必要的初始化。详细过程请参考以下代码:
btr_create(
ulint type,
ulint space,
const page_size_t& page_size,
index_id_t index_id,
dict_index_t* index,
const btr_create_t* btr_redo_create_info,
mtr_t* mtr)
{
/* index tree 的segment headers 存储于新分配的root page中,ibuf tree的
segment headers放在独立的ibuf header page中。以下代码屏蔽了ibuf tree的
创建逻辑,重点介绍index tree的创建过程 */
/* 局部变量 */
...
/* 创建一个non_leaf segment段,并将段的地址存储到段首页偏移为
PAGE_HEADER + PAGE_BTR_SEG_TOP的位置,用block记录下non_leaf segment
段首页page对应的block,该block将作为该btree的root page */
block = fseg_create(space, 0,
PAGE_HEADER + PAGE_BTR_SEG_TOP, mtr);
if (block == NULL) {
return(FIL_NULL);
}
/* 记录下root page的信息 */
page_no = block->page.id.page_no();
frame = buf_block_get_frame(block);
/* 创建leaf_segment,并将段首存储到root page上偏移为
PAGE_HEADER + PAGE_BTR_SEG_LEAF的位置 */
if (!fseg_create(space, page_no,
PAGE_HEADER + PAGE_BTR_SEG_LEAF, mtr)) {
/* 没有足够的空间分配新的segment,需要释放掉已分配的root page */
btr_free_root(block, mtr);
return(FIL_NULL);
}
/* 在root page上做index page的初始化,根据页面压缩与否做不同处理 */
page_zip = buf_block_get_page_zip(block);
if (page_zip) {
/* 其他逻辑 */
page = page_create_zip(block, index, 0, 0, NULL, mtr);
} else {
/* 其他逻辑 */
page = page_create(block, mtr,
dict_table_is_comp(index->table),
dict_index_is_spatial(index));
}
/* 在root page上设置其所在的index id */
btr_page_set_index_id(page, page_zip, index_id, mtr);
/* 将root page的前后页面设置为NULL */
btr_page_set_next(page, page_zip, FIL_NULL, mtr);
btr_page_set_prev(page, page_zip, FIL_NULL, mtr);
/* 其他逻辑 */
/* 返回root page的页面号 */
return(page_no);
}
4.2 segment的创建过程
segment的创建过程比较简单:先在inode page中为segment分配一个inode entry,然后再inode entry上进行初始化,更新space header里的最大segment id,即可。需要注意的是:当传入的page 为0 时,意味着要创建一个独立的segment,需要将当前的inode entry地址记录在段首page中,并返回;当传入的page非0时,segment需要在指定的page的指定位置记录下当前的inode entry地址。详细过程请参考代码:
buf_block_t*
fseg_create_general(
/*================*/
ulint space_id,/*!< in: space id */
ulint page, /*!< in: page where the segment header is placed: if
this is != 0, the page must belong to another segment,
if this is 0, a new page will be allocated and it
will belong to the created segment */
ulint byte_offset, /*!< in: byte offset of the created segment header
on the page */
ibool has_done_reservation, /*!< in: TRUE if the caller has already
done the reservation for the pages with
fsp_reserve_free_extents (at least 2 extents: one for
the inode and the other for the segment) then there is
no need to do the check for this individual
operation */
mtr_t* mtr) /*!< in/out: mini-transaction */
{
/* 局部变量 */
...
/* 如果传入的page是0,则创建一个独立的段,并把segment header的信息
存储在段首page中。如果传入page是非0,则这是一个非独立段,需要将
segment header的信息存储在指定page的指定位置上 */
if (page != 0) {
/* 获取指定page */
block = buf_page_get(page_id_t(space_id, page), page_size,
RW_SX_LATCH, mtr);
header = byte_offset + buf_block_get_frame(block);
}
/* 其他逻辑 */
/* 获取space header和inode_entry */
space_header = fsp_get_space_header(space_id, page_size, mtr);
inode = fsp_alloc_seg_inode(space_header, mtr);
if (inode == NULL) {
goto funct_exit;
}
/* 获取当前表空间最大segment id,并更新表空间最大
segment id */
seg_id = mach_read_from_8(space_header + FSP_SEG_ID);
mlog_write_ull(space_header + FSP_SEG_ID, seg_id + 1, mtr);
/* 初始化inode entry的segment 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);
/* 初始化inode entry的三个extent链表 */
flst_init(inode + FSEG_FREE, mtr);
flst_init(inode + FSEG_NOT_FULL, mtr);
flst_init(inode + FSEG_FULL, mtr);
/* 初始化innode entry的32个碎片页 */
mlog_write_ulint(inode + FSEG_MAGIC_N, FSEG_MAGIC_N_VALUE,
MLOG_4BYTES, mtr);
for (i = 0; i < FSEG_FRAG_ARR_N_SLOTS; i++) {
fseg_set_nth_frag_page_no(inode, i, FIL_NULL, mtr);
}
/* 如果传入的page是0,则分配一个段首page */
if (page == 0) {
block = fseg_alloc_free_page_low(space, page_size,
inode, 0, FSP_UP, RW_SX_LATCH,
mtr, mtr
#ifdef UNIV_DEBUG
, has_done_reservation
#endif /* UNIV_DEBUG */
);
header = byte_offset + buf_block_get_frame(block);
mlog_write_ulint(buf_block_get_frame(block) + FIL_PAGE_TYPE,
FIL_PAGE_TYPE_SYS, MLOG_2BYTES, mtr);
}
/* 在page指定位置记录segment header,segment header由
inode page所在的space id,page no, 以及inode entry的在
inode page 中的页内偏移组成 */
mlog_write_ulint(header + FSEG_HDR_OFFSET,
page_offset(inode), MLOG_2BYTES, mtr);
mlog_write_ulint(header + FSEG_HDR_PAGE_NO,
page_get_page_no(page_align(inode)),
MLOG_4BYTES, mtr);
mlog_write_ulint(header + FSEG_HDR_SPACE, space_id, MLOG_4BYTES, mtr);
funct_exit:
DBUG_RETURN(block);
}
4.3 extent的分配过程
表空间分配extent的逻辑比较简单,直接查询FSP_FREE上有没有剩余的extent即可,没有的话就为FSP_FREE重新初始化一些extent。详细逻辑如下:
static
xdes_t*
fsp_alloc_free_extent(
ulint space_id,
const page_size_t& page_size,
ulint hint,
mtr_t* mtr)
{
/* 局部变量 */
...
/* 获取space header */
header = fsp_get_space_header(space_id, page_size, mtr);
/* 获取hint页所在的xdes entry */
descr = xdes_get_descriptor_with_space_hdr(
header, space_id, hint, mtr, false, &desc_block);
fil_space_t* space = fil_space_get(space_id);
/* 当hint页所在的xdes entry的状态是XDES_FREE时,直接将其摘下返回,
否则尝试从FSP_FREE中为segment分配extent。如果FSP_FREE为空,
则需要进一步从未初始化的空间中为FSP_FREE新分配一些extent,
并从新的FSP_FREE中取出第一个extent返回 */
if (descr && (xdes_get_state(descr, mtr) == XDES_FREE)) {
/* Ok, we can take this extent */
} else {
/* Take the first extent in the free list */
first = flst_get_first(header + FSP_FREE, mtr);
if (fil_addr_is_null(first)) {
fsp_fill_free_list(false, space, header, mtr);
first = flst_get_first(header + FSP_FREE, mtr);
}
/* 分配失败 */
if (fil_addr_is_null(first)) {
return(NULL); /* No free extents left */
}
descr = xdes_lst_get_descriptor(
space_id, page_size, first, mtr);
}
/* 将分配到的extent从FSP_FREE中删除 */
flst_remove(header + FSP_FREE, descr + XDES_FLST_NODE, mtr);
space->free_len--;
return(descr);
}
当为segment分配extent时稍微复杂一些:先检查FSEG_ FREE中是否有剩余的extent,如果没有再用fsp_alloc_free_extent从表空间中申请extent。在第二种情况下,FSEG_ FREE中的extent不足,因此还会进一步尝试为FSEG_FREE分配更多extent。详细过程如下:
static
xdes_t*
fseg_alloc_free_extent(
fseg_inode_t* inode,
ulint space,
const page_size_t& page_size,
mtr_t* mtr)
{
/* 局部变量 */
...
/* 如果FSEG_FREE非空,则从其中为segment分配extent,如果FSEG_FREE为空,
则从调用fsp_alloc_free_extent 为当前segment分配extent */
if (flst_get_len(inode + FSEG_FREE) > 0) {
first = flst_get_first(inode + FSEG_FREE, mtr);
descr = xdes_lst_get_descriptor(space, page_size, first, mtr);
} else {
descr = fsp_alloc_free_extent(space, page_size, 0, mtr);
if (descr == NULL) {
return(NULL);
}
/* 将从space申请到的extent设置为segment私有状态(XDES_FSEG),
将改extent加入到FSEG_FREE中 */
seg_id = mach_read_from_8(inode + FSEG_ID);
xdes_set_state(descr, XDES_FSEG, mtr);
mlog_write_ull(descr + XDES_ID, seg_id, mtr);
flst_add_last(inode + FSEG_FREE, descr + XDES_FLST_NODE, mtr);
/* 当前FSEP_FREE中剩余的extent不多,尝试为当前segment分配更多
物理相邻的extent */
fseg_fill_free_list(inode, space, page_size,
xdes_get_offset(descr) + FSP_EXTENT_SIZE,
mtr);
}
return(descr);
}
4.4 page的分配过程
表空间page的分配过程如下:先查看hint_page所在的extent是否适合分配空闲页面,不适合的话,则尝试从FSP_FREE_FRAG链表中寻找空闲页面。如果FSP_FREE_FRAG为空,则新分配一个extent,将其添加到FSP_FREE_FRAG中,并在其中分配空闲页面。
static MY_ATTRIBUTE((warn_unused_result))
buf_block_t*
fsp_alloc_free_page(
ulint space,
const page_size_t& page_size,
ulint hint,
rw_lock_type_t rw_latch,
mtr_t* mtr,
mtr_t* init_mtr)
{
/* 局部变量 */
...
/* 获取表空间header 和 hint page所在extent的xdes entry */
header = fsp_get_space_header(space, page_size, mtr);
descr = xdes_get_descriptor_with_space_hdr(header, space, hint, mtr);
*/ 如果xdes entry的状态是XDES_FREE_FRAG,那就直接从该extent中分配page,
否则从FSP_FREE_FRAG中去寻找空闲page */
if (descr && (xdes_get_state(descr, mtr) == XDES_FREE_FRAG)) {
/* Ok, we can take this extent */
} else {
/* Else take the first extent in free_frag list */
first = flst_get_first(header + FSP_FREE_FRAG, mtr);
/* 尝试从FSP_FREE_FRAG中寻找空闲页面,当FSP_FREE_FRAG链表为空时,
需要使用fsp_alloc_free_extent分配一个新的extent,将该extent加入
FSP_FREE_FRAG,并在其中分配空闲page */
if (fil_addr_is_null(first)) {
descr = fsp_alloc_free_extent(space, page_size,
hint, mtr);
if (descr == NULL) {
/* No free space left */
return(NULL);
}
xdes_set_state(descr, XDES_FREE_FRAG, mtr);
flst_add_last(header + FSP_FREE_FRAG,
descr + XDES_FLST_NODE, mtr);
} else {
descr = xdes_lst_get_descriptor(space, page_size,
first, mtr);
}
/* Reset the hint */
hint = 0;
}
/* 从找到的extent中分配一个空闲页面 */
free = xdes_find_bit(descr, XDES_FREE_BIT, TRUE,
hint % FSP_EXTENT_SIZE, mtr);
if (free == ULINT_UNDEFINED) {
ut_print_buf(stderr, ((byte*) descr) - 500, 1000);
putc('\n', stderr);
ut_error;
}
page_no = xdes_get_offset(descr) + free;
/* 其他逻辑 */
/* 在fsp_alloc_from_free_frag中设置分配page的XDES_FREE_BIT为false,
表示被占用;递增头page的FSP_FRAG_N_USED字段;如果该extent被用满了,
就将其从FSP_FREE_FRAG移除,并加入到FSP_FULL_FRAG链表中,更新FSP_FRAG_N_USED的值 */
fsp_alloc_from_free_frag(header, descr, free, mtr);
/* 对Page内容进行初始化后返回 */
return(fsp_page_create(page_id_t(space, page_no), page_size,
rw_latch, mtr, init_mtr));
}
为了能够使得segment内逻辑上相邻的节点在物理上也尽量相邻,尽量提高表空间的利用率,在segment中分配page的逻辑较为复杂。详细过程如下所述:
static
buf_block_t*
fseg_alloc_free_page_low(
fil_space_t* space,
const page_size_t& page_size,
fseg_inode_t* seg_inode,
ulint hint,
byte direction,
rw_lock_type_t rw_latch,
mtr_t* mtr,
mtr_t* init_mtr
#ifdef UNIV_DEBUG
, ibool has_done_reservation
#endif /* UNIV_DEBUG */
)
{
/* 局部变量 */
...
/* 计算当前segment使用的和占用的page数。前者统计的统计方法为
累加32个碎片页中已使用的数量,FSEG_FULL/FSEG_NOT_FULL中已使
用page的数量,后者的统计方法为累加32个碎片页已使用数量,
FSEG_FULL/FSEG_NOT_FULL/FSEG_FREE三个链表中总page数*/
reserved = fseg_n_reserved_pages_low(seg_inode, &used, mtr);
/* 获取表空间header 和 hint page所在extent的xdes entry */
space_header = fsp_get_space_header(space_id, page_size, mtr);
descr = xdes_get_descriptor_with_space_hdr(space_header, space_id,
hint, mtr);
if (descr == NULL) {
/* 说明hint page在free limit之外,将hint page置0,取消hint page的作用*/
hint = 0;
descr = xdes_get_descriptor(space_id, hint, page_size, mtr);
}
/* In the big if-else below we look for ret_page and ret_descr */
/*-------------------------------------------------------------*/
if ((xdes_get_state(descr, mtr) == XDES_FSEG)
&& mach_read_from_8(descr + XDES_ID) == seg_id
&& (xdes_mtr_get_bit(descr, XDES_FREE_BIT,
hint % FSP_EXTENT_SIZE, mtr) == TRUE)) {
take_hinted_page:
/* 1. hint page所在的extent属于当前segment,并且
hint page也是空闲状态,这是最理想的情况 */
ret_descr = descr;
ret_page = hint;
goto got_hinted_page;
/*-----------------------------------------------------------*/
} else if (xdes_get_state(descr, mtr) == XDES_FREE
&& reserved - used < reserved / FSEG_FILLFACTOR
&& used >= FSEG_FRAG_LIMIT) {
/* 2. segment空间利用率高于临界值(7/8 ,FSEG_FILLFACTOR),
并且hint page所在的extent处于XDES_FREE状态,直接将该extent从
FSP_FREE摘下,分配至segment的FSEG_FREE中,返回hint page */
ret_descr = fsp_alloc_free_extent(
space_id, page_size, hint, mtr);
xdes_set_state(ret_descr, XDES_FSEG, mtr);
mlog_write_ull(ret_descr + XDES_ID, seg_id, mtr);
flst_add_last(seg_inode + FSEG_FREE,
ret_descr + XDES_FLST_NODE, mtr);
/* 在利用率条件允许的情况下,为segment的FSEG_FREE多分配几个
物理相邻的extent */
fseg_fill_free_list(seg_inode, space_id, page_size,
hint + FSP_EXTENT_SIZE, mtr);
goto take_hinted_page;
/*-----------------------------------------------------------*/
} else if ((direction != FSP_NO_DIR)
&& ((reserved - used) < reserved / FSEG_FILLFACTOR)
&& (used >= FSEG_FRAG_LIMIT)
&& (!!(ret_descr
= fseg_alloc_free_extent(
seg_inode, space_id, page_size, mtr)))) {
/* 3. 当利用率小于临界值,不建议分配新的extent,避免空间浪费,
此时从FSEG_FREE中获取空闲extent,用于分配新的page */
ret_page = xdes_get_offset(ret_descr);
if (direction == FSP_DOWN) {
ret_page += FSP_EXTENT_SIZE - 1;
}
} else if ((xdes_get_state(descr, mtr) == XDES_FSEG)
&& mach_read_from_8(descr + XDES_ID) == seg_id
&& (!xdes_is_full(descr, mtr))) {
/* 4. 当hint page所在的extent属于当前segment时,该extent内如有空闲page,
将其返回 */
ret_descr = descr;
ret_page = xdes_get_offset(ret_descr)
+ xdes_find_bit(ret_descr, XDES_FREE_BIT, TRUE,
hint % FSP_EXTENT_SIZE, mtr);
} else if (reserved - used > 0) {
/* 5. 如果该segment占用的page数大于实用的page数,说明该segment还有空
闲的page,则依次先看FSEG_NOT_FULL链表上是否有未满的extent,如果没有,
再看FSEG_FREE链表上是否有完全空闲的extent */
fil_addr_t first;
if (flst_get_len(seg_inode + FSEG_NOT_FULL) > 0) {
first = flst_get_first(seg_inode + FSEG_NOT_FULL,
mtr);
} else if (flst_get_len(seg_inode + FSEG_FREE) > 0) {
first = flst_get_first(seg_inode + FSEG_FREE, mtr);
} else {
return(NULL);
}
ret_descr = xdes_lst_get_descriptor(space_id, page_size,
first, mtr);
ret_page = xdes_get_offset(ret_descr)
+ xdes_find_bit(ret_descr, XDES_FREE_BIT, TRUE,
0, mtr);
} else if (used < FSEG_FRAG_LIMIT) {
/* 6. 当前segment的32个碎片页尚未使用完毕,使用fsp_alloc_free_page从
表空间FSP_FREE_FRAG中分配独立的page,并加入到该inode的frag array page
数组中 */
buf_block_t* block = fsp_alloc_free_page(
space_id, page_size, hint, rw_latch, mtr, init_mtr);
if (block != NULL) {
/* Put the page in the fragment page array of the
segment */
n = fseg_find_free_frag_page_slot(seg_inode, mtr);
fseg_set_nth_frag_page_no(
seg_inode, n, block->page.id.page_no(),
mtr);
}
return(block);
} else {
/* 7. 当上述情况都不满足时,直接使用fseg_alloc_free_extent分配一个空闲
extent,并从其中取一个page返回 */
ret_descr = fseg_alloc_free_extent(seg_inode,
space_id, page_size, mtr);
if (ret_descr == NULL) {
ret_page = FIL_NULL;
ut_ad(!has_done_reservation);
} else {
ret_page = xdes_get_offset(ret_descr);
}
}
/* page分配失败 */
if (ret_page == FIL_NULL) {
return(NULL);
}
got_hinted_page:
/* 将可用的hint page标记为used状态 */
if (ret_descr != NULL) {
fseg_mark_page_used(seg_inode, ret_page, ret_descr, mtr);
}
/* 对Page内容进行初始化后返回 */
return(fsp_page_create(page_id_t(space_id, ret_page), page_size,
rw_latch, mtr, init_mtr));
}
5 总结
innodb的文件结构由自下而上包括page(页),extent(簇),segment(段),tablespace(表空间)等几个层次。page是最基本的物理单位,所有page具有相同的页首和页尾;extent由通常由连续的64个page组成,tablespace由一个个连续的extent组成;段是用来管理物理文件的逻辑单位,可以向表空间申请分配和释放page 或 extent,是构成索引,回滚段的基本元素;表空间是一个宏观概念,当innodb_file_per_table为ON时一个用户表对应一个表空间。