概述

除redo日志文件外,InnoDB的物理文件基本上具有相当统一的结构:固定大小页面,使用B-tree来管理。默认情况下,每个页面为16KB,也可以自定义配置该数值。对于压缩表,可以在建表时指定页大小,但在内存中表现的解压页依旧为统一的页大小。

从物理文件的分类来看,有日志文件、主系统表空间文件ibdata、undo tablespace文件、临时表空间文件、用户表空间。我们这里主要关心存储用户数据的用户表空间的物理文件结构。

用户表空间,顾名思义,就是用户创建的表空间,如果开启独立表空间参数,那么一个表空间会对应磁盘上的一个物理文件。

segment、extent和page

从外部来看,表空间是由连续的固定大小page构成。其实表空间文件内部还是组织为更复杂的逻辑结构,自顶向下可分为segment、extent和page。

表空间下一级称为segment。segment与数据库中的索引相映射。Innodb引擎内,每个索引对应两个segment:管理叶子节点的segment和管理非叶子节点segment。创建索引中很关键的步骤便是分配segment,Innodb内部使用INODE来描述segment。

segment的下一级是extent,extent代表一组连续的page,默认为64个page,大小1MB。extent的作用是提高page分配效率,批量分配在效率上总是优于离散、单一的page分配,另外在数据连续性方面也更佳,segment扩容时以extent为单位分配。Innodb内部使用XDES来描述extent。

page则是表空间数据存储的基本单位,innodb将表文件(xxx.ibd)按page切分,依类型不同,page内容也有所区别,最为常见的是存储数据库表的行记录。page格式会在后续文章中进行比较详细的剖析,本文中主要介绍一些特殊的存储表空间元信息的page。

表空间关键page

每个独立表空间都对应磁盘上的一个物理文件,命名形式为{table_name}.ibd。物理文件按page切分,这些page主要分为两类:存储表空间元信息的page和存储表空间用户数据的page。存储用户数据的page暂不关心,这里主要讨论表空间元信息page。

如下图,表空间的前三个page为主要的元信息page。

MySQL · 引擎特性 · Innodb 表空间 - 图1

FSP HEADER PAGE

FSP header page是表空间的root page,存储表空间关键元数据信息。由page file header、fsp header、xdes entries三大部分构成。完整格式如下图:

MySQL · 引擎特性 · Innodb 表空间 - 图2

root page也存在38字节头部信息。其中几个关键字段:

  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:存储表空间的space id

  • FIL_PAGE_SRV_VERSION:本来被用于存储前一个页面id,在root page中被用来存储SERVER版本号

  • FIL_PAGE_SPACE_VERSION:本来被用于存储后一个页面id,在root page中被用来存储TABLE SPACE版本号

fsp header主要存储表空间元信息,维护关键结构分配链表,主要成员有:

  • FSP_SIZE:表空间大小,以Page数量计算
  • FSP_FREE_LIMIT:目前在空闲的Extent上最小的尚未被初始化的Page的`Page Number
  • FSP_FREE:空闲extent链表,链表中的每一项为代表extent的xdes,所谓空闲extent是指该extent内所有page均未被使用
  • FSP_FREE_FRAG:free frag extent链表,链表中的每一项为代表extent的xdes,所谓free frag extent是指该extent内有部分page未被使用
  • FSP_FULL_FRAG:full frag extent链表,链表中的每一项为代表extent的xdes,所谓full frag extent是指该extent内所有Page均已被使用
  • FSP_SEG_ID:下次待分配的segment id,每次分配新segment时均会使用该字段作为segment id,并将该字段值+1写回
  • FSP_SEG_INODES_FULL:full inode page链表,链表中的每一项为inode page,该链表中的每个inode page内的inode entry都已经被使用
  • FSP_SEG_INODES_FREE:free inode page链表,链表中的每一项为inode page,该链表中的每个inode page内上有空闲inode entry可分配

上面提到了各种链表,这些链表其实是磁盘上的同类对象连接形成,可加速对象分配和管理。后文会描述其实现细节。需要注意的是,root page中存在两类链表:第一是链接描述extent的xdes,第二是链接inode page。

第三部分是描述extent的xdes(extent descriptor)信息,每个xdes page中均存储256个xdes,描述接下来连续的256个extent(16384个page)。

MySQL · 引擎特性 · Innodb 表空间 - 图3每个XDES项占据40字节,因此,一个xdes page可跟踪其后的256个extent的分配情况,XDES结构如下:

MySQL · 引擎特性 · Innodb 表空间 - 图4

由于单个xdes page只能描述256个extent,因此,每隔256个extent(16384个page)便需要一个xdes page。

IBUF BITMAP PAGE

作用暂时未知

INODE PAGE

表空间文件的第3个page的类型为FIL_PAGE_INODE,存储inode(index node),管理表空间的segment。每个inode对应一个segment。每个inode page默认存储FSP_SEG_INODES_PER_PAGE(85)个inode。每个索引使用2个segment,分别用于管理叶子节点和非叶子节点。

inode page由page header和inode entry组成,page header为38字节。inode为192字节。

MacrobitsDesc
FSEG_INODE_PAGE_NODE12INODE page在fsp header的某个链表节点,记录前后inode page的位置。该链表为header page的FSP_SEG_INODES_FULL或FSP_SEG_INODES_FREE。
Inode Entry 0192inode entry
Inode Entry 1192inode entry
…… 
Inode Entry 84192inode entry

Inode Entry的结构如下表:

MacrobitsDesc
FSEG_ID8该inode代表的Segment ID,若值为0表示该slot未被使用
FSEG_NOT_FULL_N_USED8FSEG_NOT_FULL链表上被使用的Page数量
FSEG_FREE16segment上所有page均空闲的extent链表
FSEG_NOT_FULL16至少有一个page分配给当前Segment的Extent链表,全部用完时,转移到FSEG_FULL上,全部释放时,则归还给当前表空间FSP_FREE链表
FSEG_FULL16segment上page被完全使用的extent链表
FSEG_MAGIC_N4Magic Number
FSEG_FRAG_ARR 04属于该Segment的frag page。总是先从全局分配独立的Page,当填满32个数组项时,就在每次分配时都分配一个完整的Extent,并在XDES PAGE中将其Segment ID设置为当前值
………… 
FSEG_FRAG_ARR 314Segment的第32个Frag Page

inode中最关键的也是各种extent链表,以加速segment中page分配。分配时,优先从空闲extent链表中分配,当无可用extent时,会向表空间中申请若干可用extent加入自身的空闲extent链表。另外,每个segment还有若干(32)碎片page,当segment初始扩容时,首先分配这些碎片page,直到分配完毕才会进入extent分配。

MySQL · 引擎特性 · Innodb 表空间 - 图5

MySQL · 引擎特性 · Innodb 表空间 - 图6

关键流程

创建表空间

创建表空间最终落实到函数fil_create_tablespace,创建ibd文件并初始化fsp header:

  1. dberr_t dict_build_tablespace(trx_t *trx, Tablespace *tablespace) {
  2. ...
  3. err = fil_ibd_create(space, tablespace->name(), datafile->filepath(),
  4. tablespace->flags(), FIL_IBD_FILE_INITIAL_SIZE);
  5. ...
  6. // 初始化fsp header的其他字段
  7. bool ret = fsp_header_init(space, FIL_IBD_FILE_INITIAL_SIZE, &mtr, false);
  8. ...
  9. }
  10. static dberr_t fil_create_tablespace(space_id_t space_id, const char *name,
  11. const char *path, ulint flags,
  12. page_no_t size, fil_type_t type)
  13. {
  14. ...
  15. // 创建ibd文件
  16. file = os_file_create(
  17. type == FIL_TYPE_TEMPORARY ? innodb_temp_file_key : innodb_data_file_key,
  18. path, OS_FILE_CREATE | OS_FILE_ON_ERROR_NO_EXIT, OS_FILE_NORMAL,
  19. OS_DATA_FILE, srv_read_only_mode && (type != FIL_TYPE_TEMPORARY),
  20. &success);
  21. ...
  22. // 这里会将新创建的ibd文件填0以预分配空间
  23. // 调试显示size=7,即分配的空间为7*16KB,但os_file_set_size内会将其向上调整至8*16KB
  24. // buf2 = static_cast<byte *>(ut_malloc_nokey(buf_size + UNIV_PAGE_SIZE));
  25. success = os_file_set_size(path, file, 0, size * page_size.physical(),
  26. srv_read_only_mode, true);
  27. ...
  28. // 初始化fsp header pages,其中前3个page作用比较关键:
  29. // page 0: root page,也是fsp header page
  30. // page 1: ibuf_bitmap page,作用暂时不明
  31. // page 2: inode page,管理segment分配,每个inode entry对应一个segment
  32. buf2 = static_cast<byte *>(ut_malloc_nokey(3 * page_size.logical()));
  33. page = static_cast<byte *>(ut_align(buf2, page_size.logical()));
  34. memset(page, '\0', page_size.logical());
  35. flags = fsp_flags_set_page_size(flags, page_size);
  36. fsp_header_init_fields(page, space_id, flags);
  37. mach_write_to_4(page + FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID, space_id);
  38. mach_write_to_4(page + FIL_PAGE_SRV_VERSION, DD_SPACE_CURRENT_SRV_VERSION);
  39. mach_write_to_4(page + FIL_PAGE_SPACE_VERSION, DD_SPACE_CURRENT_SPACE_VERSION);
  40. ...
  41. }

创建segment

innodb的space中,inode、extent和page之间环环相扣,inode对应的是segment,extent 对应的是页簇,page是页。必需先创建segment,然后再从segment内分配extent和page。创建segment核心是从inode page中分配空闲的inode。

  1. buf_block_t *fseg_create_general(
  2. space_id_t space_id,
  3. page_no_t page, // 调用者多传入0,表示需要分配一个新page作为segment header
  4. ulint byte_offset,
  5. ibool has_done_reservation,
  6. mtr_t *mtr)
  7. {
  8. fil_space_t *space = fil_space_get(space_id);
  9. mtr_x_lock_space(space, mtr);
  10. const page_size_t page_size(space->flags);
  11. ...
  12. space_header = fsp_get_space_header(space_id, page_size, mtr);
  13. // 分配inode entry,代表该segment
  14. inode = fsp_alloc_seg_inode(space_header, mtr);
  15. // 从table space的page header中读取下一个要使用的segment id
  16. // 将其设置在inode entry的FSEG_ID字段中
  17. // 同时将刚刚已分配的segment id递增1并写回space的page header的FSP_SEG_ID字段
  18. seg_id = mach_read_from_8(space_header + FSP_SEG_ID);
  19. mlog_write_ull(space_header + FSP_SEG_ID, seg_id + 1, mtr);
  20. mlog_write_ull(inode + FSEG_ID, seg_id, mtr);
  21. mlog_write_ulint(inode + FSEG_NOT_FULL_N_USED, 0, MLOG_4BYTES, mtr);
  22. // 初始化inode中的各种extent链表
  23. flst_init(inode + FSEG_FREE, mtr);
  24. flst_init(inode + FSEG_NOT_FULL, mtr);
  25. flst_init(inode + FSEG_FULL, mtr);
  26. // 初始化segment的frag page数组为空(未分配任何page)
  27. mlog_write_ulint(inode + FSEG_MAGIC_N, FSEG_MAGIC_N_VALUE, MLOG_4BYTES, mtr);
  28. for (i = 0; i < FSEG_FRAG_ARR_N_SLOTS; i++) {
  29. fseg_set_nth_frag_page_no(inode, i, FIL_NULL, mtr);
  30. }
  31. // 且该page的类型是FIL_PAGE_TYPE_SYS
  32. // 该page是Change Buffer的header page,主要用于对ibuf btree的Page管理。
  33. if (page == 0) {
  34. block = fseg_alloc_free_page_low(space, page_size, inode, 0, FSP_UP,
  35. RW_SX_LATCH, mtr, mtr);
  36. header = byte_offset + buf_block_get_frame(block);
  37. mlog_write_ulint(buf_block_get_frame(block) + FIL_PAGE_TYPE,
  38. FIL_PAGE_TYPE_SYS, MLOG_2BYTES, mtr);
  39. }
  40. // 初始化page header
  41. mlog_write_ulint(header + FSEG_HDR_OFFSET, page_offset(inode), MLOG_2BYTES,
  42. mtr);
  43. mlog_write_ulint(header + FSEG_HDR_PAGE_NO,
  44. page_get_page_no(page_align(inode)), MLOG_4BYTES, mtr);
  45. mlog_write_ulint(header + FSEG_HDR_SPACE, space_id, MLOG_4BYTES, mtr);
  46. ...
  47. }
  48. static fseg_inode_t *fsp_alloc_seg_inode(
  49. fsp_header_t *space_header,
  50. mtr_t *mtr)
  51. {
  52. ...
  53. // 如果FSP_SEG_INODES_FREE链表为空
  54. // 该链表用于链接那些尚有inode空间可分配的inode_page
  55. // 如果该链表为空,那就要分配一个新的inode page了,这个过程我们在下面描述
  56. // 调用fsp_alloc_seg_inode_page创建一个新的inode page
  57. if (flst_get_len(space_header + FSP_SEG_INODES_FREE) == 0 &&
  58. !fsp_alloc_seg_inode_page(space_header, mtr)) {
  59. return (NULL);
  60. }
  61. ...
  62. // 从inode page中分配一个空闲inode
  63. const page_size_t page_size(mach_read_from_4(FSP_SPACE_FLAGS + space_header));
  64. const page_id_t page_id(
  65. page_get_space_id(page_align(space_header)),
  66. flst_get_first(space_header + FSP_SEG_INODES_FREE, mtr).page);
  67. block = buf_page_get(page_id, page_size, RW_SX_LATCH, mtr);
  68. page = buf_block_get_frame(block);
  69. // 遍历该inode page的所有inode entry
  70. // 如果发现某个Inode Entry的FSEG_ID字段未设置
  71. // 即认为该inode entry尚未分配
  72. n = fsp_seg_inode_page_find_free(page, 0, page_size, mtr);
  73. inode = fsp_seg_inode_page_get_nth_inode(page, n, page_size, mtr);
  74. // 如果由于本次分配导致该inode page内不再有可用的inode空间
  75. // 那需要将该inode page从space的FSP_SEG_INODES_FREE链表摘除并加入FSP_SEG_INODES_FULL链表尾部
  76. if (FIL_NULL == fsp_seg_inode_page_find_free(page, n + 1, page_size, mtr)) {
  77. flst_remove(space_header + FSP_SEG_INODES_FREE, page + FSEG_INODE_PAGE_NODE,
  78. mtr);
  79. flst_add_last(space_header + FSP_SEG_INODES_FULL,
  80. page + FSEG_INODE_PAGE_NODE, mtr);
  81. }
  82. return (inode);
  83. }

优先从table space的FSP_SEG_INODES_FREE链表中获取inode page并从中分配未使用的inode。

如果FSP_SEG_INODES_FREE链表为空,说明当前已无空闲的inode page,此时需分配新的inode page,参考函数fsp_alloc_seg_inode_page

  1. // 从space中分配一个未使用的inode page
  2. static ibool fsp_alloc_seg_inode_page(
  3. fsp_header_t *space_header,
  4. mtr_t *mtr)
  5. {
  6. space = page_get_space_id(page_align(space_header));
  7. const page_size_t page_size(mach_read_from_4(FSP_SPACE_FLAGS + space_header));
  8. // fsp_alloc_free_page这个函数比较有意思,值得仔细研究一番
  9. // 分配一个空闲page作为inode page
  10. block = fsp_alloc_free_page(space, page_size, 0, RW_SX_LATCH, mtr, mtr);
  11. page = buf_block_get_frame(block);
  12. // 设置新分配的PAGE类型为FIL_PAGE_INODE
  13. mlog_write_ulint(page + FIL_PAGE_TYPE, FIL_PAGE_INODE, MLOG_2BYTES, mtr);
  14. // 初始化inode page中的所有inode信息为空闲
  15. for (page_no_t i = 0; i < FSP_SEG_INODES_PER_PAGE(page_size); i++) {
  16. inode = fsp_seg_inode_page_get_nth_inode(page, i, page_size, mtr);
  17. mlog_write_ull(inode + FSEG_ID, 0, mtr);
  18. }
  19. // 将新分配的inode page加入至FSP_SEG_INODES_FREE链表的尾部
  20. flst_add_last(space_header + FSP_SEG_INODES_FREE, page + FSEG_INODE_PAGE_NODE, mtr);
  21. return (TRUE);
  22. }

分配extent

fsp_alloc_free_extent被用来分配一个extent,即xdes,一般segment内无可用page时会向表空间申请分配空闲extent。

分配extent的核心是分配空闲xdes,因为根据xdes我们可以很迅速地定位extent。

  1. // 参数@hint给出一个线索:可以尝试先使用该page所在的extent,如果其状态为free的话
  2. // hint表明调用者建议从该page开始分配空间以获得更好的连续性
  3. // 该函数会优先分配该hint page所在的extent,如果其尚未被分配的话
  4. static xdes_t *fsp_alloc_free_extent(space_id_t space_id,
  5. const page_size_t &page_size,
  6. page_no_t hint, mtr_t *mtr) {
  7. fsp_header_t *header;
  8. fil_addr_t first;
  9. xdes_t *descr;
  10. buf_block_t *desc_block = NULL;
  11. header = fsp_get_space_header(space_id, page_size, mtr);
  12. // 计算hint page所在的extent的xdes对象位置
  13. // 这个函数会在下面仔细分析
  14. descr = xdes_get_descriptor_with_space_hdr(header, space_id, hint, mtr, false,
  15. &desc_block);
  16. fil_space_t *space = fil_space_get(space_id);
  17. // 该xdes状态是FREE,表明该extent未分配给其他人使用
  18. if (descr && (xdes_get_state(descr, mtr) == XDES_FREE)) {
  19. /* Ok, we can take this extent */
  20. } else {
  21. // 需要从table space header上的FSP_FREE链表中获取一个可分配的extent
  22. first = flst_get_first(header + FSP_FREE, mtr);
  23. if (fil_addr_is_null(first)) {
  24. // 如果FSP_FREE链表为空,那么只能分配一个新的xdes了,其磁盘位置记录在first中(page, boff)
  25. fsp_fill_free_list(false, space, header, mtr);
  26. first = flst_get_first(header + FSP_FREE, mtr);
  27. }
  28. descr = xdes_lst_get_descriptor(space_id, page_size, first, mtr);
  29. }
  30. flst_remove(header + FSP_FREE, descr + XDES_FLST_NODE, mtr);
  31. space->free_len--;
  32. return (descr);
  33. }

该函数的调用者会给该函数一个提示hint,指示优先分配该hint page所在的extent,如果该extent是空闲状态(描述extent的xdes有state状态位),便分配该extent,否则需重新分配:

  1. 从table space的FSP_FREE链表上获取第一个可分配的xdes,将其从FSP_FREE链表摘除并直接返回即可。
  2. 如果FSP_FREE链表为空,则需分配新的xdes了,调用函数fsp_fill_free_list。它用来分配extent并返回其相应的xdes。
  1. static void fsp_fill_free_list(bool init_space, fil_space_t *space,
  2. fsp_header_t *header, mtr_t *mtr) {
  3. page_no_t limit;
  4. page_no_t size;
  5. ulint flags;
  6. xdes_t *descr;
  7. ulint count = 0;
  8. page_no_t i;
  9. // size代表table space当前的大小(可以理解为ibd文件的大小)
  10. size = mach_read_from_4(header + FSP_SIZE);
  11. // limit: 暂时还不理解
  12. limit = mach_read_from_4(header + FSP_FREE_LIMIT);
  13. flags = mach_read_from_4(header + FSP_SPACE_FLAGS);
  14. const page_size_t page_size(flags);
  15. // 需要扩张ibd文件,调用fsp_try_extend_data_file
  16. // 扩张ibd文件逻辑在下面仔细研究
  17. if (size < limit + FSP_EXTENT_SIZE * FSP_FREE_ADD) {
  18. if ((!init_space && !fsp_is_system_tablespace(space->id) &&
  19. !fsp_is_global_temporary(space->id)) ||
  20. (space->id == TRX_SYS_SPACE &&
  21. srv_sys_space.can_auto_extend_last_file()) ||
  22. (fsp_is_global_temporary(space->id) &&
  23. srv_tmp_space.can_auto_extend_last_file())) {
  24. fsp_try_extend_data_file(space, header, mtr);
  25. size = space->size_in_header;
  26. }
  27. }
  28. // 文件成功扩张后,开始分配extent
  29. // limit到底是什么意思呢?
  30. i = limit;
  31. // 结束条件:
  32. // 1. 分配的extent未超过当前表空间物理文件大小(size)
  33. // 2. 分配的extent数量超过了FSP_FREE_ADD(4):这是因为程序中限制了每次填充extent的最大个数
  34. while ((init_space && i < 1) ||
  35. ((i + FSP_EXTENT_SIZE <= size) && (count < FSP_FREE_ADD))) {
  36. // 什么时候init_xdes为true呢? 还没思考明白
  37. // #define ut_2pow_remainder(n, m) ((n) & ((m)-1))
  38. bool init_xdes = (ut_2pow_remainder(i, page_size.physical()) == 0);
  39. space->free_limit = i + FSP_EXTENT_SIZE;
  40. mlog_write_ulint(header + FSP_FREE_LIMIT, i + FSP_EXTENT_SIZE, MLOG_4BYTES, mtr);
  41. // 需要初始化xdes page
  42. if (init_xdes) {
  43. ...
  44. }
  45. buf_block_t *desc_block = NULL;
  46. // 根据上面分配的extent计算其对应的xdes所在的xdes page以及在page内的偏移
  47. descr = xdes_get_descriptor_with_space_hdr(header, space->id, i, mtr,
  48. init_space, &desc_block);
  49. xdes_init(descr, mtr);
  50. if (init_xdes) {
  51. fsp_init_xdes_free_frag(header, descr, mtr);
  52. } else {
  53. // 将刚创建的extent加入至fsp header的FSP_FREE链表尾部
  54. flst_add_last(header + FSP_FREE, descr + XDES_FLST_NODE, mtr);
  55. count++;
  56. }
  57. i += FSP_EXTENT_SIZE;
  58. }
  59. space->free_len += (uint32_t)count;
  60. }

这个函数有几个比较有趣的地方:

  1. fsp_try_extend_data_file:尝试扩张table space文件,分配extent说到底需要底层物理存储空间
  2. init_xdes的计算
  3. xdes_get_descriptor_with_space_hdr:根据分配的extent计算其对应的xdes所在的page以及page内偏移

我们接下来一一阐述。

fsp_try_extend_data_file用于扩充ibd文件:

  1. static UNIV_COLD ulint fsp_try_extend_data_file(fil_space_t *space,
  2. fsp_header_t *header,
  3. mtr_t *mtr)
  4. {
  5. page_no_t size;
  6. page_no_t size_increase;
  7. size = mach_read_from_4(header + FSP_SIZE);
  8. const page_size_t page_size(mach_read_from_4(header + FSP_SPACE_FLAGS));
  9. if (space->id == TRX_SYS_SPACE) {
  10. ...
  11. } else if (fsp_is_global_temporary(space->id)) {
  12. ...
  13. } else {
  14. // 首先判断如果当前ibd文件大小小于extent_size(典型是1MB)
  15. // 首先将其extent至extent_size(典型是1MB)
  16. page_no_t extent_pages = fsp_get_extent_size_in_pages(page_size);
  17. if (size < extent_pages) {
  18. /* Let us first extend the file to extent_size */
  19. if (!fsp_try_extend_data_file_with_pages(space, extent_pages - 1, header,
  20. mtr)) {
  21. }
  22. size = extent_pages;
  23. }
  24. // 接下来判断每次extent的幅度,其算法是:
  25. // 1. 如果当前ibd大小小于32个extent_size,每次extent单位为extent_size
  26. // 2. 否则,每次extent单位为FSP_FREE_ADD(4) * extent_size;
  27. size_increase = fsp_get_pages_to_extend_ibd(page_size, size);
  28. }
  29. if (!fil_space_extend(space, size + size_increase)) {
  30. DBUG_RETURN(false);
  31. }
  32. // 修改fsp header中的大小
  33. space->size_in_header =
  34. ut_calc_align_down(space->size, (1024 * 1024) / page_size.physical());
  35. fsp_header_size_update(header, space->size_in_header, mtr);
  36. }

xdes_get_descriptor_with_space_hdr用来根据extent的起始page no计算该extent对应的xdes所在的page以及在page内的位置。

  1. // offset为extent的起始page no
  2. UNIV_INLINE MY_ATTRIBUTE((warn_unused_result)) xdes_t
  3. *xdes_get_descriptor_with_space_hdr(fsp_header_t *sp_header,
  4. space_id_t space, page_no_t offset,
  5. mtr_t *mtr, bool init_space = false,
  6. buf_block_t **desc_block = NULL) {
  7. ulint limit;
  8. ulint size;
  9. page_no_t descr_page_no;
  10. ulint flags;
  11. page_t *descr_page;
  12. const page_size_t page_size(flags);
  13. descr_page_no = xdes_calc_descriptor_page(page_size, offset);
  14. buf_block_t *block;
  15. if (descr_page_no == 0) {
  16. /* It is on the space header page */
  17. descr_page = page_align(sp_header);
  18. block = NULL;
  19. } else {
  20. block = buf_page_get(page_id_t(space, descr_page_no), page_size,
  21. RW_SX_LATCH, mtr);
  22. descr_page = buf_block_get_frame(block);
  23. }
  24. if (desc_block != NULL) {
  25. *desc_block = block;
  26. }
  27. return (descr_page + XDES_ARR_OFFSET +
  28. XDES_SIZE * xdes_calc_descriptor_index(page_size, offset));
  29. }

这个函数是根据extent的起始page no来计算其xdes所在的page以及page内偏移。从前文的描述中我们知道,每个extent对应了一个描述它的xdes,且xdes存储在XDES_PAGE之中(当然 root page中也会存储xdes),每个XDES_PAGE内可存储256个xdes结构,用来描述其后连续256个extent(1M)的情况,因此,一旦我们知道了某个extent的起始page no,便可以反推出其对应的xdes所在的page,xdes_calc_descriptor_page函数我们会在后面仔细描述。如下:

MySQL · 引擎特性 · Innodb 表空间 - 图7

分配page

  1. static buf_block_t *fseg_alloc_free_page_low(fil_space_t *space,
  2. const page_size_t &page_size,
  3. fseg_inode_t *seg_inode,
  4. page_no_t hint, byte direction,
  5. rw_lock_type_t rw_latch,
  6. mtr_t *mtr, mtr_t *init_mtr
  7. )
  8. {
  9. ...
  10. }

fseg_alloc_free_page_low函数中实现了7种情况从segment中分配page:

  1. segment的hint位置的页是空闲状态,直接返回对应的page
  2. xdes是空闲状态,但segment中的空闲page数量小于总page数量的1/8且碎片页被全部用完,为其分配一个空闲extent,并直接分配hint对应的page
  3. 如果xdes不是空闲状态,且segment inode中的空闲page数量 < 1/8,在inode当中获得一个空闲的extent,并且将这个extent descr对应的页返回。
  4. descr是XDES_FSEG状态,且这个extent中还有空闲page,从其中获取一个page.
  5. 除了以上情况外,如果descr不是空闲的,但是inode还有其他的空闲extent,从其他的extent获得一个空闲
  6. 如果其他的extent没有空闲页,但是fragment array还有空闲的碎片page,从空闲的碎片page中获得一个空闲页。
  7. 如果连碎页也没有,直接申请分配一个新的extent,并在其中获取一个空闲的page.

关键辅助对象和函数

磁盘链表

Innodb的磁盘链表主要是用来连接存储在磁盘上的对象。链表每项并非基于内存指针的,而是基于对象在磁盘中的位置(page no + offset)来描述。

  1. typedef struct fil_addr_struct
  2. {
  3. ulint page; /*page在space中的编号*/
  4. ulint boffset; /*page中的字节偏移量,在内存中使用2字节表示*/
  5. } fil_addr_t;
  6. typedef byte flst_node_t;
  7. typedef byte flst_base_node_t;
  8. /* The physical size of a list base node in bytes */
  9. constexpr ulint FLST_BASE_NODE_SIZE = 4 + 2 * FIL_ADDR_SIZE;
  10. /* The physical size of a list node in bytes */
  11. constexpr ulint FLST_NODE_SIZE = 2 * FIL_ADDR_SIZE;

flst_node_t代表链表上的一个节点,其大小为12字节,其中前6字节(page:4 boffset:2)表示链表中前一个节点的fil_addr_t信息,后6个字节表示链表中下一个节点的fil_addr_t信息。

flst_base_node_t则用于描述链表的整体信息,包括:

链表节点个数FLST_LEN(4字节)

链表首节点的fil_addr_t (6字节)

链表尾节点的fil_addr_t(6字节)

每个磁盘空间链表的结构如下所示:

MySQL · 引擎特性 · Innodb 表空间 - 图8

Innodb 表空间的fsp header page中定义了多种链表,这在前面已经作过描述,不再赘述。这些链表在fsp header_init时被初始化,初始时链表为空:

  1. bool fsp_header_init(space_id_t space_id, page_no_t size, mtr_t *mtr,
  2. bool is_boot)
  3. {
  4. flst_init(header + FSP_FREE, mtr);
  5. flst_init(header + FSP_FREE_FRAG, mtr);
  6. flst_init(header + FSP_FULL_FRAG, mtr);
  7. flst_init(header + FSP_SEG_INODES_FULL, mtr);
  8. flst_init(header + FSP_SEG_INODES_FREE, mtr);
  9. }
  10. UNIV_INLINE
  11. void flst_init(flst_base_node_t *base, mtr_t *mtr)
  12. {
  13. mlog_write_ulint(base + FLST_LEN, 0, MLOG_4BYTES, mtr);
  14. flst_write_addr(base + FLST_FIRST, fil_addr_null, mtr);
  15. flst_write_addr(base + FLST_LAST, fil_addr_null, mtr);
  16. }
  17. buf_block_t *fseg_create_general(
  18. space_id_t space_id,
  19. page_no_t page,
  20. ulint byte_offset,
  21. ibool has_done_reservation,
  22. mtr_t *mtr)
  23. {
  24. ...
  25. flst_init(inode + FSEG_FREE, mtr);
  26. flst_init(inode + FSEG_NOT_FULL, mtr);
  27. flst_init(inode + FSEG_FULL, mtr);
  28. ...
  29. }

每个要连接在该链表上的对象中均需要预留12个字节来记录前后节点位置。例如,xdes和inode page中均有该12字节的预留。

MySQL · 引擎特性 · Innodb 表空间 - 图9

MySQL · 引擎特性 · Innodb 表空间 - 图10

xdes_calc_descriptor_page

该函数根据page no计算描述其所在的extent的xdes对象位置(即xdes所在的page no)。

  1. // 以m为16384为例,如果n < 16384,计算的结果是0
  2. // 如果16384 =< n < 32768,计算的结果是16384
  3. // 如果32768 =< n < 49152,计算的结果是32768
  4. // 以此类推
  5. #define ut_2pow_round(n, m) ((n) & ~((m)-1))
  6. UNIV_INLINE
  7. page_no_t xdes_calc_descriptor_page(const page_size_t &page_size,
  8. page_no_t offset)
  9. {
  10. return (ut_2pow_round(offset, page_size.physical()));
  11. }

根据我们上面的知识储备,每个xdes page管理随后的连续256个extent的空间(假如page size为16KB的话,那总大小就是256MB)。假如每个page是16KB,256MB包含的页面数是256MB/16KB = 16384,因此,所有page no小于16384的page其xdes都会位于第0个page(root page),而page no介于16384~32768的page其xdes page便是page 16384,与xdes_calc_descriptor_page计算是吻合的。

参考