Innodb的内部存储结构小结

来源:互联网 发布:好听的淘宝名字大全 编辑:程序博客网 时间:2024/05/23 16:56

http://www.itpub.net/forum.php?mod=viewthread&tid=1813772


本帖最后由 myownstars 于 2013-9-3 20:51 编辑

本篇是Jeremy Cole一系列文章的总结,加以个人的理解,如有错误之处还望不吝赐教。
原文详见http://blog.jcole.us/2013/01/ 

innodb的内部结构,依次从space file -> segment -> extent -> page -> row逐一介绍

Space File
innodb的file分为系统表空间文件和用户表空间文件(开启file-per-table)
结构分别如下
ibdata1_File_Overview.png 


IBD_File_Overview.png 

由图可知两者有一定的共同点,即前3个页依次均为FSP_HDR, IBUF_BITMAP和INODE;
不同之处
1:system file包含了一系列与系统相关的页,如insert buffer / transaction / rollback / data dictionary / double write buffer
2:innodb表如果创建了二级索引,则紧邻前3页分别为表索引的root page,第1个应为聚簇索引的root page

1个file最多由2^32个页组成,相邻页以大小为1M的extent为单位聚集;
每个file均以FSP_HDR页打头,后者包含一个FSP header,用于跟踪其下所有extent的使用情况,如free/fragmented extent
FSP_HDR只能追踪256个extent即256M,故剩下的空间每256M均包含一个XDES页(与FSP_HDR相同,但FSP header置0);
如下所示

Space_File_Overview.png 

什么是segment
5.6的官方解释
For example, within a file-per-table tablespace, the table data is in one segment and each associated index is in its own segment. The system tablespace contains many different segments, because it can hold many tables and their associated indexes. The system tablespace also includes up to 128 rollback segments making up the undo log
跟oracle类似,每个table/index均有相应的segment
http://dev.mysql.com/doc/refman/5.6/en/innodb-file-space.html  
When a segment grows inside the tablespace, InnoDB allocates the first 32 pages to it one at a time. After that, InnoDB starts to allocate whole extents to the segment. InnoDB can add up to 4 extents at a time to a large segment to ensure good sequentiality of data.
segment增长时,开始以页为单位,一次分配一个页,共32页;然后以extent为单位,一次分配1个extent;最后一次分配4个extent;

Two segments are allocated for each index in InnoDB. One is for nonleaf nodes of the B-tree, the other is for the leaf nodes. Keeping the leaf nodes contiguous on disk enables better sequential I/O operations, because these leaf nodes contain the actual table data. 
每个索引拥有2个segment,一个用于非叶结点,另一个用于叶子结点

file的第3个页为inode page,里面存放inode entry,其中每个entry对应一个segment
INODE_Page_Overview.png 
每个inode entry大小为192字节,inode页最多可容纳85个,而每个索引占用2个,故一个inode页最多可容纳42个索引;
inode页有2种状态:free--该页至少有一个free inode entry;full--没有一个free inode entry;
list node为双向链表,分别指向previous/next inode page;另有list base node,存储于FSP Header,分别指向该链表的first/end list node,下面会详细介绍;
INODE_Entry.png 
FSEG ID:segment ID
Magic number:存储一个固定值,表示该inode entry已被初始化
Fragment Array Entry:共计32页,从FREE_FRAG extent中分配得来,与前面的官方文档相对应,即一个segment的前32个页以页为单位分配,用完后再以extent为单位分配
extent按照使用情况可分为free--已被分配给该segment但尚没有页被使用 not_full--至少有一个页已被使用 full--所有页均被使用;
当某个extent的所有页均被置为unused时,其会被放入FSP header的free list,可供其它segment使用,这与oracle的机制不一样;



如何创建index
忽略fast index creation,每次创建索引都须重建表文件,把space file的第3+N(已有索引数)个页当作该索引的root page;
查找FSP Header的base list node for free Inode,遍历该链表找出两个已初始化(magic number=97937874)且未分配(segment id=0?)的inode entry,一个负责non-lead node,另一个负责管理lead node;
这两个segment的前32个页逐页分配,超出此大小后则以extent为单位,从FSP header的free list获取free extent;


FSEG_Header.png 
Index_File_Segment_Structure.png 





extent
space file的第一个页为FSP_HDR,以后每256M对应一个XDES页;
下面看一下FSP_HDR页的构成,开头为38字节的FIL header,其次为FSP Header,剩余为XDES Entry
FSP_HDR_Page_Overview.png 
FIL Header与普通页结构一样,下面会详细介绍
FSP Header是FSP_HDR页特有的结构,XDES页中的FSP Header全部置0。


FSP_Header.png 
Highest page number in file:当前文件的最高page no,随文件扩展而增长;因扩展空间分很多步,可能部分页还未初始化即填充0;
Highest page number initialized:已经被初始化的页的最高no,所谓初始化即分配了FIL Header结构,可以被使用,类似oracle的高水位线
FREE_FRAG/FULL_FRAG:此类extent以单个页为分配单位,不同页可用于不同目的,比如带有FSP_HDR/XDES页的extent,剩余页可用作他用,此类extent被称作FRAG(fragment),full_frag表示该extent已无free page
--官方文档的解释
Some pages in the tablespace contain bitmaps of other pages, and therefore a few extents in an InnoDB tablespace cannot be allocated to segments as a whole, but only as individual pages
--
FREE list:extent的所有页均为free

XDES即extent描述符

XDES_Entry.png 
File Segment ID:该extent所属的segment id
State:当前extent的状态,有4个值:free/free_frag/full_frag/fseg,前3个标注该extent由FSP Header管理,即全局共享;最后1个则表示该extent为某segment ID所有,即私有;
Page State Bitmap:共16字节(64个页),每2bit代表一个page:第1个标注该页是否free,第2个目前没有用到;
--Jeremy原话
There are two bits per page, but the “clean” bit is always set to 1, so e.g.:
aa=10101010 = [used, used, used, used]
ff=11111111 = [free, free, free, free]
--
List node:描述符为双向链表,其分别指向previous/next
List_Nodes.png 

注:前面提到的FSP Header又包含有一系列Base Node,规定了list长度以及第一个和最后一个list node的偏移量
List_Base_Nodes.png 
依据现在掌握的知识,当segment需要扩展时,可以查看FSP Header中的list base node for "free" list,通过其可遍历当前所有free list node进而获取所需空间;
可通过list base node for "free" list中的list length * 1M快速获取当前表的free space
--
When you ask for available free space in the tablespace by issuing a SHOW TABLE STATUS statement, InnoDB reports the extents that are definitely free in the tablespace.
--



典型的页结构如下
FIL_Header_and_Trailer.png 

里面的字段都很好理解,其中涉及到page number的off set/previous-next page均为4字节32bit,印证了开头说的一个文件只能包含2^32个页的说法
上面已介绍了FSP_HDR页结构,下面描述一下索引页
INDEX_Page_Overview.png 

FIL Header和FSEG Header前面均已描述,直接看INDEX Header
INDEX_Header.png 
Format flag:行记录格式,compact/redundant
Heap top directory:已用空间的offset;位于其和page directory之间的都属free space
First garbage record offset:指向deleted 记录列表的第一个entry,innodb的delete记录只被标志为deleted,待到purge操作时才被物理移除
Garbage space:deleted记录占用的字节数
Last insert position:最近一次插入记录的offset,可用于下次insert
Page level:当前页在索引中的level,leaf为0,依次递增;在一个3级索引中,root page为2;
Max transaction ID:当前页的最大事务ID
Page direction:每次插入新记录时都要同最后一次插入的记录值相比较,以便决定是sequential inserts还是random inserts,有3个候选值:LEFT/RIGHT/NO_DIRECTION
Number of inserts in page direction:如新插入记录的page direction同当前相同,则计数增1 
Number of directory slots:page directory中的slot数量
Number of heap records:本页中所有记录数目,包括infimum/supremum以及deleted记录
Number of records:non-deleted的记录数


INDEX_System_Records.png 
每个页包含两条system record,infimum和supremum,其存储位置固定,分别为offset 99和offset 112,分别存储字面值infimum和supremum
Infimum的next record指针指向当前页lowest key的记录;
Supremum的next record指针为0,而当前页拥有highest key用户记录的next record指针指向supremum

User record
按照插入顺序添加到页,依据key值递增形成单向链表(每个记录头都有next record指针),以infimum为起点supremum为终点,可以快速的遍历页中所有记录;
依据每个index header的next page指针可以很快的以递增顺序访问整个索引;
又因为记录间为单向链表,降序访问索引就没那么简单了;--这点不如oracle

innodb页的记录从上往下分配,而page directory从下往上分配,两者中间的部分为实际的free space;


Page Directory
INDEX_Page_Directory.png 
页里的记录为单向链表且增序排列,如果新插一条记录须遍历整个链表----注:物理位置可通过index header的Last insert position获取,但要维护逻辑增序需插入链表适当位置
页目录用于优化此过程,每个slot对应4-8个记录(super record除外),可执行二分查找;
Slot数目由index header的number of slots指定,至少包含两个指向system record的slot;

其存储对应记录的offset及其key value,还有对应的记录数,即当前slot和前一个slot之间的记录数,如下所示:
该slot对应4条记录,key值为7,该记录在页内偏移量为349字节
B_Tree_Page_Directory_Cropped.png 

下图描述的更详细,该页共24条用户记录,从infimum起到supremum结束形成一条单向链表,共7个page directory;
倒着分配,第一个slot位于页的16374字节处,每条占用2字节
B_Tree_Page_Directory_Structure.png 
依据Jeremy的工具,当该页满的时候,其page directory应该如下--除去两个super record,其余至少对应4个user record
$ innodb_space -f test/t.ibd -r ./describer_test.rb -d Test -p 5 page-directory-summary
slot    offset  type          owned   key
0       99      infimum       1       
1       221     conventional  4       (238)
2       349     conventional  4       (242)
3       477     conventional  4       (246)
4       605     conventional  4       (250)
5       733     conventional  4       (254)

<many lines omitted>
112     14429   conventional  4       (682)
113     14557   conventional  4       (686)
114     14685   conventional  4       (690)
115     14813   conventional  4       (694)
116     14941   conventional  4       (698)
117     112     supremum      5


最后对比一下有无page directory的查找例子:
不使用page directory
irb> index.linear_search([10000])
linear_search: root=3, level=2, key=(10000)
linear_search_from_cursor: page=3, level=2, start=(1)
linear_search_from_cursor: page=3, level=2, current=(1)
linear_search_from_cursor: page=36, level=1, start=(1)
linear_search_from_cursor: page=36, level=1, current=(1)
linear_search_from_cursor: page=36, level=1, current=(235)
linear_search_from_cursor: page=36, level=1, current=(703)

<16 lines omitted>
linear_search_from_cursor: page=36, level=1, current=(8659)
linear_search_from_cursor: page=36, level=1, current=(9127)
linear_search_from_cursor: page=36, level=1, current=(9595)
linear_search_from_cursor: page=25, level=0, start=(9595)
linear_search_from_cursor: page=25, level=0, current=(9595)
linear_search_from_cursor: page=25, level=0, current=(9596)
linear_search_from_cursor: page=25, level=0, current=(9597)

<400 lines omitted>
linear_search_from_cursor: page=25, level=0, current=(9998)
linear_search_from_cursor: page=25, level=0, current=(9999)
linear_search_from_cursor: page=25, level=0, current=(10000)


使用page directory
irb> index.binary_search([10000])
binary_search: root=3, level=2, key=(10000)
binary_search_by_directory: page=3, level=2, dir.size=1, dir[0]=()
linear_search_from_cursor: page=3, level=2, start=(1)
linear_search_from_cursor: page=3, level=2, current=(1)
binary_search_by_directory: page=36, level=1, dir.size=151, dir[75]=(139699)--总共151个页目录(index header),先从midpoint开始,不断二分
binary_search_by_directory: page=36, level=1, dir.size=75, dir[37]=(68563)
binary_search_by_directory: page=36, level=1, dir.size=37, dir[18]=(32995)
binary_search_by_directory: page=36, level=1, dir.size=18, dir[8]=(14275)
binary_search_by_directory: page=36, level=1, dir.size=8, dir[3]=(4915)
binary_search_by_directory: page=36, level=1, dir.size=5, dir[2]=(8659)
binary_search_by_directory: page=36, level=1, dir.size=3, dir[1]=(10531)
binary_search_by_directory: page=36, level=1, dir.size=1, dir[0]=(8659)
linear_search_from_cursor: page=36, level=1, start=(8659) --找到最后一个页目录,遍历该页目录对应的4-8条记录
linear_search_from_cursor: page=36, level=1, current=(8659)
linear_search_from_cursor: page=36, level=1, current=(9127)
linear_search_from_cursor: page=36, level=1, current=(9595)
binary_search_by_directory: page=25, level=0, dir.size=117, dir[58]=(9826)
binary_search_by_directory: page=25, level=0, dir.size=59, dir[29]=(9942)
binary_search_by_directory: page=25, level=0, dir.size=30, dir[14]=(9998)
binary_search_by_directory: page=25, level=0, dir.size=16, dir[7]=(10026)
binary_search_by_directory: page=25, level=0, dir.size=7, dir[3]=(10010)
binary_search_by_directory: page=25, level=0, dir.size=3, dir[1]=(10002)
binary_search_by_directory: page=25, level=0, dir.size=1, dir[0]=(9998)--注:用户记录为单向递增链表,而页目录只对应<=key的记录,故需要找到小于且最接近10000的页目录,定位到用户记录,然后递增访问
linear_search_from_cursor: page=25, level=0, start=(9998)
linear_search_from_cursor: page=25, level=0, current=(9998)
linear_search_from_cursor: page=25, level=0, current=(9999)
linear_search_from_cursor: page=25, level=0, current=(10000)



行记录
仅讨论compact结构,相比redundant,compact消除了可从数据字典中获取的数据
行由data和header构成,通用的header格式如下
Record_Header.png 
Next Record Offset: 下条记录的相对偏移量,2字节,
Record Type:记录类型,目前只支持4个,conventional(0), node pointer(1),infimum(2),supremum(3)
Order:该行插入序号,infimum为0,supremum为1,依此递增
Nullable filed bitmap:字段为nullable时占有1bit,
Variable field lengths array:存储变长字段的长度
一个行头至少占用5个字节

innodb数据行可分为聚簇索引行和二级索引行,另外每个索引的non-leaf节点也存有行;
先看一下聚簇索引叶子节点的行格式
Record_Clustered_Leaf.png 
聚簇索引键值不能为空,移出了nullable field bitmap,同时添加了transaction id(6字节),roll pointer(7字节),此外还有cluster key fields(聚簇索引列)和non-key fileds(实际行数据);
对于聚簇索引,每行数据至少 5字节header + 6字节transaction id + 7字节roll pointer =18字节额外开销!

Record_Clustered_Non_Leaf.png 
Non-leaf页不支持MVCC,故移除了transaction id和roll pointer,non-key field也被child page no替代;

再来看二级索引
Record_Secondary_Leaf.png 
沿用前面介绍的行头,加以secondary key field;
虽然二级索引的记录允许重复,但是innodb同一页的每条记录必须有一个unique identifier,故将cluster key field,包含进来;

Record_Secondary_Non_Leaf.png 
相比之下,non-leaf行比leaf行反而要大4字节,因为要存储child page number;

注:不管是cluster key还是secodary key,其non-leaf记录存储的均是child page的min key

0 0
原创粉丝点击