乐趣区

关于mysql:简单学习一下ibd数据文件解析

起源:原创投稿

作者:花家舍

简介:数据库技术爱好者。

  • GreatSQL 社区原创内容未经受权不得随便应用,转载请分割小编并注明起源。

简略学习一下数据文件解析

这是尝试应用 Golang 语言简略解析 MySQL 8.0 的数据文件(*.ibd)过程的一个简略介绍,解析是反序列化的一个过程,或者叫解码的过程。

1. 为什么要解析

尽管有很多开源的代码曾经实现了这个解码过程,例如应用 C 实现的 undrop-for-innodb[1],反对到 MySQL 5.7 版本,后续未作更新。

姜老师的 py_innodb_page_info.py 也能够对数据文件进行剖析(剖析页的类型),对 MySQL 8.0 版本的兼容如同还没来得及做。

还有 Ali 应用 Java 实现了数据文件的解析[2],并且肯定水平兼容 MySQL 8.0 版本。然而本人通过解析数据文件过程对数据文件编码 / 解码、复原已删除数据等进行深刻学习。

解析过程枯燥乏味,有种按图索骥的感觉,即通过文档或者源码形容的页构造,建设对应构造体来解析,难度可能并不简单,通过对存储建设更为平面全面的意识,补齐 MySQL 常识拼图很有意义。

2. 应用 Golang 解析数据文件

抉择应用 Golang 来实现这个过程,则是因为其比 C ++ 简略的多,易上手,不必过多关注垃圾回收问题,代码简洁,有丰盛的软件包以供援用,不需费劲本人实现。

当然还有更多有吸引力的个性,例如并发性(goroutine)与通道(channel),对 gRPC 与 Protocol Buffers 一流的反对等,这些优良的个性在本文解析 MySQL 数据文件时并没有用到,则无需赘言。

3. 文件

3.1 linux 文件

文件是对 I / O 设施的一个抽象概念,比方咱们不须要理解磁盘技术就能够解决磁盘文件内容,就是通过操作文件这个形象来进行的。

在 CSAPP(ComputerSystem: A programer perspective)中文版第三版中,对文件有过精辟的定义:文件就是字节序列,仅此而已。那么对 MySQL 数据文件的解析,可视为对字节序列的解析,解析的过程由此能够简略地视为对编码规定的逆向过程,即解码。

3.2 MySQL 8.0 数据文件

MySQL 在 8.0 版本中对数据字典做了从新调整,元数据对立存储到 InnoDB 数据字典表。不再在 Server 层中冗余表构造信息(不再存储在 ibdata 中),也不再在 frm 文件中存储。

不仅将元数据信息存储在数据字典表中,同时也冗余了一份在 SDI(Serialized Dictionary Information)中,而对于 InnoDB,SDI 数据则间接存储在 ibd 数据文件中,文件中有 SDI 类型页来进行存储。因为元数据信息均存储在 InnoDB 引擎表中,反对 crash-safe,联合一套 DDL_LOG 机制,以此实现了 DDL 原子性。

在文件的结尾(并不是十分靠前的地位,大略是第四个数据页,page number=3,这在之前的版本中是所谓的根页:root page)冗余表构造信息,十分相似于 Avro 的编码格局,即在蕴含几百万记录的大文件结尾蕴含模式(形容数据的组成,包含元素组成,数据类型等)信息,后续每个记录则不再冗余这部分信息,以达到节俭存储、传输老本目标。

而在解析数据时,模式是必要的,须要首先获取表构造信息,以便于后续数据解码过程。所以在这次解析数据文件时,首先须要获取数据文件中 SDI 页中的表构造(模式)。

为简化解析过程,默认数据库是设置了 UTF- 8 字符集,即数据文件是以 UTF- 8 进行编码的。并且数据库启用了 innodb_file_per_table 参数,表被存储在他们本人的表空间里(*.ibd)。

4. 解析文件

解析并不是非常复杂的过程。就程序角度而言首先是关上文件,而后是解析文件,最初是敞开文件。

然而过程很有可能产生其余未知的问题,例如不同的编码格局,简单的数据类型等,只是在本文解析中抉择了回避而已。

Golang 提供了 os.OpenFile()函数来关上文件(倡议以只读模式关上),并提供了 file.Close()函数来敞开文件,留下了解析过程来本人实现。

4.1 获取表构造

像 JSON 或者 XML 这种自我形容的数据,其自我形容局部比拟冗余,在大文件中,冗余过多信息很不明智。而在 MySQL 的数据文件中,数据增长是个显著问题,自我形容的局部须要精简,不能每写入一条数据,就要追随写入表名、列名。

所以 MySQL 仅在文件结尾保留了表构造信息(在更早的版本,表构造信息则是存储在 frm 文件中,并在数据字典中冗余)。

这种形式的编码能够很节俭存储空间,但解析关上数据文件,输入的是字节序列,显然是人类不可读的,须要找到模式(表构造信息)来解读成人类可读,才达到解析的目标。

如果能够通过数据库,很容易取得表构造信息,但如果仅有一个数据文件,获取表后果信息只能从 SDI 中解析获取,过程比拟麻烦。

侥幸的是,MySQL 官网提供了专有工具:ibd2sdi。通过 ibd2sdi 默认获取的是所有的列和索引信息,是以 JSON 格局输入的。

4.2 构造体和切片

MySQL 的数据文件中是以页(默认大小为 16k)为单位存储,索引能够利用 B -tree 的查找个性疾速的定位到页,在页内的数据则是通过十分相似于二分查找的形式来定位。

在解析数据文件时,也是以页为单位,InnoDB 中大略有 31 种类型页,在源码 storage/innobase/include/fil0fil.h 中能够看到相干定义。

真正存储用户数据的是 FIL_PAGE_INDEX(数字编码是 17855,解析到 page_type=17855,代表这是一个索引页)类型的页,这不代表其余类型的页没有用途,在形容文件中页的应用的状况(FIL_PAGE_TYPE_FSP_HDR),空页调配(FIL_PAGE_TYPE_ALLOCATED),insert buffer(FIL_PAGE_IBUF_FREE_LIST),压缩页(FIL_PAGE_COMPRESSED)等,须要各种类型页进行存储不同的信息。

而不同的页,在解析过程中,则应用 Golang 的构造体(struct)来进行形象示意,这相似于 Java 的类,是对具体事务的形象,也十分相似于 C 语言的构造体。解析不同的数据页,则应用相应的构造体,为了简化解析过程(毕竟是学习过程),只对大略六种页进行了解析。

不光是页在解析过程中应用构造体,在页中的一些局部也是用了构造体来进行形象,例如每个数据页的结尾会有 38 个字节的 File Header[3],在解析过程中,也是通过构造体进行解析的。

第 3 节中说文件是字节序列,InnoDB 数据文件通常很大,整体关上后则是一个超长的字节数组,并不不便进行整体解析,或者数据文件远超内存大小,全副载入内存不太可能,须要对其逐渐读取,或者说叫做进行切割,例如依照页的默认大小进行切割,行将数据文件切割成若干个 16k 大小的片段(数据文件必然是页大小的整数倍)。

在 Golang 中,这种数组片段的数据结构叫做切片,即 slice。这让人想到早餐面包,一整块面包并不不便抹上果酱,那么能够切成面包片,后续就能够很好的解决了。在解析局部的代码中,会常常的应用构造体和切片这两种数据结构进行解析字节序列。

4.3 页

4.3.1 页构造

在数据文件中,页是以 16K 大小,即 16384 Byte(这是一个默认大小,由 innodb_page_size 参数管制)的字节序列存储的,一个数据文件蕴含很多个数据页。在内存中,则以 B -tree 模式来组织,B-tree 的每一个节点,就是一个页,非叶子节点通过指针指向下一层节点(页)。

当事务在记录上加逻辑锁时,在 B -tree 上则是增加的栓锁(latch)来避免页(在页决裂和膨胀时,还可能蕴含页的父节点)被其余线程批改。规范数据页(INDEX page)蕴含七个局部,组成构造如下列表格所示[4]。

Name Size Remarks
File Header 38 页的一些通用信息
Page Header 56 不同数据页专有的一些信息
Infimum/Supremum 26 两个虚构的行记录,即最大值最小值
User Records 理论存储的行记录内容
Free Space 页中尚未应用的空间
Page Directory 页中的某些记录的绝对地位(slot)
File Trailer 8 校验信息,4byte 的 checksum,4byte 的 Low32lsn

其中,38 个字节长度的 File Header 在简直所有类型的页中都存在。其构造蕴含八个局部,组成构造如下:

Name Size Remarks
FIL_PAGE_SPACE 4 ID of the space the page
FIL_PAGE_OFFSET 4 ordinal page number from start of space
FIL_PAGE_PREV 4 offset of previous page in key order
FIL_PAGE_NEXT 4 offset of next page in key order
FIL_PAGE_LSN 8 log serial number of page’s latest log record
FIL_PAGE_TYPE 2 current defined page type
FIL_PAGE_FILE_FLUSH_LSN 8 log serial number
FIL_PAGE_ARCH_LOG_NO 4 archived log file number

而 56 个字节长度的 Page Header 蕴含 14 个局部,组成构造如下:

Name Size Remarks
PAGE_N_DIR_SLOTS 2 number of directory slots
PAGE_HEAP_TOP 2 record pointer to first record in heap
PAGE_N_HEAP 2 number of heap records; initial value = 2
PAGE_FREE 2 record pointer to first free record
PAGE_GARBAGE 2 number of bytes in deleted records
PAGE_LAST_INSERT 2 record pointer to the last inserted record
PAGE_DIRECTION 2 either PAGE_LEFT, PAGE_RIGHT, or PAGE_NO_DIRECTION
PAGE_N_DIRECTION 2 number of consecutive inserts in the same direction
PAGE_N_RECS 2 number of user records
PAGE_MAX_TRX_ID 8 the highest ID of a transaction
PAGE_LEVEL 2 level within the index (0 for a leaf page)
PAGE_INDEX_ID 8 identifier of the index the page belongs to
PAGE_BTR_SEG_LEAF 10 file segment header for the leaf pages in a B-tree
PAGE_BTR_SEG_TOP 10 file segment header for the non-leaf pages in a B-tree

以上是对 INDEX 类型的数据页构造的形容,不再赘述其余类型页。像页中的 file hader、page header 局部或者其余类型的页,在代码中都会有相应的构造体来形容其数据结构。

更多对于数据页构造信息除了参考官网,还能够参考 Jeremy Cole 在 GitHub 上开的一个我的项目,InnoDB Diagrams[5]。InnoDB Diagrams 可能更多的是基于 MySQL 5.7 版本,然而仍有很好的借鉴意义。

4.3.2 页类型

上文说到 InnoDB 中大略有 30 余种类型页,这里只解析了其中 6 种,次要目标则是解析 FIL_PAGE_INDEX 中的数据,其余类型页的解析则互有偏重。

  • FIL_PAGE_TYPE_FSP_HDR

用于调配、治理 extent 和 page,蕴含了表空间以后应用的 page 数量,调配的 page 数量,并且存储了 256 个 XDES Entry(Extent),保护了 256M 大小的 Extent,如果用完 256 个区,则会追加生成 FIL_PAGE_TYPE_XDES 类型的页

  • FIL_PAGE_IBUF_BITMAP

用于跟踪随后的每个 page 的 change buffer 信息,应用 4 个 bit 来形容每个 page 的 change buffer 信息

  • FIL_PAGE_INODE

治理数据文件中的 segement,每个索引占用 2 个 segment,别离用于治理叶子节点和非叶子节点。每个 inode 页能够存储 FSP_SEG_INODES_PER_PAGE(默认为 85)个记录(Inode Entry(Segment))。FIL_PAGE_INODE 治理段,而段信息管理 Extent(区)信息

  • FIL_PAGE_SDI

存储 Serialized Dictionary Information(SDI, 词典序列化信息), 存储了这个表空间的一些数据字典 (Data Dictionary) 信息

  • FIL_PAGE_INDEX

在内存中结构 B -tree 所须要的数据就寄存在这个类型的页中,B-tree 的每一个叶子节点就指向了这样的一个页。构造在各处都有具体介绍,其中的 File Header,Page Header 局部在上文有介绍。

  • FIL_PAGE_TYPE_ALLOCATED

曾经调配但还未应用的页,即空页。

4.4 解码

4.4.1 记录解析

编码和解码常常组合呈现,但因为咱们曾经默认拿到了数据文件(实现了编码),不再介绍编码过程。

在本文中的解码,是将字节序列解码为人类可读的字符串。也就是解码为数据在存入数据库时,业务写入时的样子。

通常编程语言会提供 encode 和 decode 办法进行编码和解码操作,在这次解析中,是参考 MySQL 源码后自实现的解码过程。

实际上是按存储数据的字节长度不同,对一个字节长度的数据进行位运算左移(左移位数依据字节长度确定,左移 n 位就是乘以 2 的 n 次方),而后每个字节进行或计算。

这里贴源码来,下列代码是 unittest\gunit\innodb\lob\mach0data.h 中的一段,是对 4 个间断字节的数据写入和读取的过程,即 mach_write_to_4()函数和 mach_read_from_4()函数,能够看到位运算的移位操作。

留神:源码在读取字节数据时,将数据转换为 ulint 类型,这个类型为无符号类型。

inline void mach_write_to_4(byte *b, ulint n) {b[0] = (byte)(n >> 24);
  b[1] = (byte)(n >> 16);
  b[2] = (byte)(n >> 8);
  b[3] = (byte)n;
}

/** The following function is used to fetch data from 4 consecutive
bytes. The most significant byte is at the lowest address.
@param[in] b pointer to four bytes.
@return ulint integer */
inline ulint mach_read_from_4(const byte *b) {return (((ulint)(b[0]) << 24) | ((ulint)(b[1]) << 16) | ((ulint)(b[2]) << 8) |
          (ulint)(b[3]));
}

在写入时对每个字节进行了右移,在读取时则须要左移,这里波及到了大小端的内容,有趣味能够抉择深入研究,不再过多介绍。

4.4.2 行记录格局

上一大节是对具体记录(对应数据库中列的具体值)的字节解析过程介绍。

那么如何定位到记录(在 Index 数据页中的 User RECORD 局部中定位),除了定位到记录数据的页,还受到参数 innodb_default_row_format 的设置的影响。

MySQL 数据记录是以行的模式存储的的,而行记录的格局在 MySQL 5.7.9 及当前版本,默认值是 DYNAMIC。

而在 MySQL 5.6 之前的版本,这个参数默认值还是 COMPACT。innodb 通过 fsp 页(数据文件的的第一个页,类型是 FIL_PAGE_TYPE_FSP_HDR)的 flags(长度 4 byte)的不同 bit 位来判断表空间的各种属性,比方是否压缩,是否加密,是否为共享表空间、是否是长期表空间等等。

其中决定行格局类型是 flags 构造中的 FSP_FLAGS_WIDTH_ATOMIC_BLOBS 属性,在 flags 长度 4 byte 的 32 个 bit 地位中是第 6 位。

为缩小程序复杂度,这里默认行格局为 DYNAMIC(有趣味可自行深刻理解,具体可参考源码:storage\innobase\include\fsp0types.h)。
COMPACT 行格局

记录额定数据 记录的数据内容
变长字段的长度列表 NULL 值标记位 记录头信息 col1 Data col2 Data coln Data

在记录头信息中,应用 5 个字节长度来标记,其中有一个 delete_mask 标记位用来标记以后记录是否曾经被删除,更多更具体的行记录格局解释能够参考阿里内核月报:
https://www.bookstack.cn/read/aliyun-rds-core/24bcb047feb68f13.md
每行记录都依照以上格局存储数据。

DYNAMIC 与 COMPACT 比拟相似,不同处在于,看待解决行溢出的解决及策略,Dynamic、Compressed 行格局会把记录中数据量过大的字段值全副存储到溢出页中,在原页中保留 20 字节长度的指针,而不会在该记录的数据内容的相应字段处存储该字段值的 768 个字节长度的前缀数据了。

在这次解析中不会波及 BLOB 等大字段类型,所以解析其实是依照 COMPACT 格局解析的。

4.5 编写代码

代码程度无限,介绍代码程度更无限,文末会有代码地址,可供具体参考。这里捡重点的简明扼要介绍一下。

4.5.1 关上文件

倡议应用只读模式关上文件,对文件进行简略的校验。对文件可对数据页中的 checksum 值进行校验,这须要按页读取时进行校验,这里偷懒只对文件大小进行了简略校验。关上文件:

file, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm)

4.5.2 按页读取

数据文件既然是字节序列,那么其实每次能够读取任意多个字节到内存(如果内存容许),但事实上 MySQL 数据文件是按页来治理,那么读入内存则以页为单位读取。

在 MySQL 实例运行时,大部分时候,BTREE 的根页(root page)以及第二层甚至第三层的页都是常驻内存的,在主键应用 bigint 类型时,则非叶子节点中中每个目录项长度是主键 8 字节长度 + 指针 6 字节长度,每个页可寄存 16384/(8+6)≈1170 个目录项,即每个节点的扇出为 1170。
那么,BTREE 的前几层大小:

第 0 层:16,384 byte

第 1 层:16384 * 1170 = 19,169,289 byte

第 2 层:16384 1170 1170 = 22,428,057,600 byte

可见前三层的数据大略在 20GB 左右,将其全副放入内存压力不大,前两层数据甚至只有几十 MB 而已,BTREE 的高度达到 4 层就能够寄存千万条数据。

那么真正须要磁盘读取的数据更可能集中在第 2 层或以上,因为内存的中页面读取遍历很快,缩小磁盘操作,所以效率很高。

在这里解析时,则是按序来读取页,当然能够并发读取,那么解析输入的数据程序则无奈保障了。

// 每读取一页,参数 j 减少 16384,来标记文件的读取地位
now, err := file.Seek(j, io.SeekStart)
// mysql_def.UNIV_PAGE_SIZE=16384
b := make([]byte, mysql_def.UNIV_PAGE_SIZE)
// 读取文件,每读取一页,将其放入数组中寄存,Golang 读取 [] 文件会默认将其存储在无符号字节数组中:[]uint8,而 Java 读取文件会放入有符号数组中:int8[]
n, err := file.Read(b)

4.5.3 结构切片

读取的数据放入数组中,在 Golang 中,切片是依据动静数组来构建,解析地位会随解析进度变动,据此结构了一个 struct:

type Input struct {Data     []byte
 Position int64
}
var IN Input

Input 构造体的元素 Data 用于寄存读取文件的字节序列,长度固定为 16384。

元素 Position 则标记解析地位,随解析递增。这样在读取文件后,就能够将字节序列传递给构造体 Input 的申明变量 IN 了。

// 每页解析从第 0 为开始
slice.IN.Position = 0
// 存放数据页字节序列的数组传递给构造体 Data 元素
slice.IN.Data = b

4.5.4 结构页构造体并解析数据

MySQL 数据文件蕴含不同类型的页,页中蕴含不同的局部,例如 File Header,File Trailer,Page Header 等。

对每一种构造都建设与之对应的构造体,这有点相似于 Java 的面向对象思维。为的是前面解析数据到雷同的构造地位时,代码可形象重用。

这里以 File Header 为例,在 4.3.1 节中介绍了它的组成,蕴含 8 个局部共 38 字节长度,对应的构造体如下:

type FilHeader struct {
 PageSpace        int64
 PageOffset       int64
 PagePrev         int64
 PageNext         int64
 PageLsn          int64
 PageType         string
 PageFileFlushLsn int64
 PageArchLogNo    int64
}

因为数据页中每个页在页头都蕴含 File Header,所以对其 38 个字节长度的解析,总是从第 0 字节开始到第 37 地位完结。FileHeader 的解析过程如下:

func FileHeaderReadSlice() FilHeader {
  // 申明 FilHeader 构造体变量
 var fh FilHeader
 fh.PageSpace = slice.ReadUnsignedInt()
 fh.PageOffset = slice.ReadUnsignedInt()
 fh.PagePrev = slice.IfUndefined(slice.ReadUnsignedInt())
 fh.PageNext = slice.IfUndefined(slice.ReadUnsignedInt())
 fh.PageLsn = slice.ReadLongInt64()
 fh.PageType = mysql_def.ParsePageType(slice.ReadShortInt16())
 fh.PageFileFlushLsn = slice.ReadLongInt64()
 fh.PageArchLogNo = slice.ReadUnsignedInt()
 return fh
}

代码中的

fh.PageSpace = slice.ReadUnsignedInt()

是在字节序列中读取四个字节长度并解析后赋值给 PageSpace(page 的 space ID),解析后把地位递增 4(int 寄存长度为 4 字节)。

解析过程是借鉴 MySQL 源码中的过程,在 4.4.1 节中有过介绍。例如 4 字节 Int 解析过程如下:

func decodeInt32(in Input) int64 {
 data := in.Data
 index := in.Position
 checkPositionIndexes(index, index+mysql_def.SIZE_OF_INT)
 return (int64(int8(data[index+3])) & 0xff) | (int64(int8(data[index+2]))&0xff)<<8 | (int64(int8(data[index+1]))&0xff)<<16 | (int64(int8(data[index]))&0xff)<<24
}

依照这种程序,对 File Header 的八个局部解析后,获取必要信息(例如 PageType,页类型),后续的解析就是依据依据页类型来调用不同构造体的解析来实现全副数据解析。

所不同者就是解析不同字节长度差异,而不同页构造或页中构造长度在 MySQL 文档或者源码中曾经表明,尤其是索引页中的 User record,受到 row_format 参数管制。文章篇幅限度,只介绍了 File Header 解析流程。

代码实现对页类型的解析,对索引页中中的记录解析还在进行,有趣味能够持续关注。

5. 补充

不可否认,解析过程对很多中央进行了简化,数据页只解析了无限的几种,编码方式只限定了 UTF-8,数据类型更是只抉择了 int 和 varchar,对简单的数据类型没有波及。

对文件也没有进行校验,只是默认了咱们解析的是一个失常的数据文件。这是一个简略的解析代码,后续会持续欠缺。

另外,在解析过程中,没有应用并发,后续能够思考引入并发来放慢解析过程。例如数据文件进行切片后,是按程序解析的,这里可用 Golang 的协程来放慢解析,不同的页能够调度到不同的协程进行解析。

兴许数据文件中大部分都是索引页,这样按页类型并发度不够,这时能够引入线程池,调度不同数量的索引页在不同的线程上进行解析。
另外,后续有减少解析已删除数据的代码。数据被谬误的删除,如果应用备份 +binlog 进行复原,耗时费劲。

迫切需要一种相似于 Oracle 的闪回工具来对数据进行疾速闪回,复原到数据被批改前的状态。

在现有的几种日志中,在 binlog 中的更新 event 中,因为记录了更新前后的数据,使得解析 binlog event 并反转操作成为可能,基于 binlog 提供的这项个性,开源社区实现了闪回工具,例如 Python 实现的 binlog2sql 和美团 DBA 团队开发的 MyFalsh,能够闪回 DML 以及局部 DDL。

除了 binlog 以外,还有 redo 记录了数据更新日志,但因为 redo 记录的属于 change vector(更新向量,即对 page 的更改,不记录更新前数据状态),且可能蕴含未提交事务。另外一种日志 undo 因为 page cleaner 线程的存在,会被不定期革除,因而只借助 redo 实现闪回则不太可能。

复原 delete 的数据,无妨借助数据文件在删除数据时的 mark 机制来进行复原,毛病是须要在数据被谬误删除时,须要立刻进行锁表,避免进一步的数据操作重用 mark 为 delete 的空间被重用。

6. 后记

代码地址:https://gitee.com/huajiashe_byte/ibdreader
心愿通过抛砖引玉,促成常识交换,独特晋升对数据库文件存储的意识。程度所限,难免会呈现纰漏和谬误,欢送斧正。

正文

注 1:undrop-for-innodb:

https://github.com/twindb/undrop-for-innodb

Ali 有专文对这个工具进行介绍:

https://www.bookstack.cn/read/aliyun-rds-core/2dd363c8cc64171b.md

注 2:

`https://github.com/alibaba/in…
`

注 3:参考:

https://dev.mysql.com/doc/internals/en/innodb-fil-header.html

注 4:这部分的具体介绍可参见官网网址:

https://dev.mysql.com/doc/internals/en/innodb-page-overview.html

注 5:地址是:

https://github.com/jeremycole/innodb_diagrams

Enjoy GreatSQL :)

本文由博客一文多发平台 OpenWrite 公布!

退出移动版