[MYSQL] mysql.ibd 文件解析 (sdi page) (非debug模式下查看隐藏系统表)

2024-09-18 15:12:15 浏览数 (1)

导读

在mysql 8.0版本,系统表的存储引擎由myisam改为了innodb, @@datadir/mysql目录下一堆的数据文件通通放到@@datadir/mysql.ibd文件中了. 但很多表在非debug模式下是无法查看里面的数据的. 这TM就很恼火. (刚学完innodb的磁盘结构, 我能受这气?). 所以我们现在来解析下mysql.ibd文件. (也顺便为 ibd2sql 2.0 做准备)

分析

我们先登录数据库, 随便查看张mysql库下面的表的DDL

代码语言:sql复制
show create table mysql.engine_cost;

发现使用的表空间都是mysql. 那我们再看下mysql表空间的类型

代码语言:sql复制
select * from information_schema.INNODB_TABLESPACES where NAME='mysql'G

发现是general的.(肯定的啊). 我们查看该表空间的路径:

general tablespace 可以存储多个表

代码语言:sql复制
select * from information_schema.files where TABLESPACE_NAME='mysql'G

发现就是@@datadir/mysql.ibd. 也就是我们这次要解析的文件.

mysql的相对路径都是基于@@datadir的

我们之前解析数据文件的时候都是基于File-Per-Table Tablespaces的, 即一个表空间对应一张表. 上来就是有难度的.

SDI PAGE

既然一个表空间能存储多张表, 那么sdi信息就会记录多张表的信息, 我们之前解析inode的时候, 发现里面第一对segment就是sdi page, 也就是说sdi page 也应该和普通索引一样的格式. 再加上解析sdi的时候也有trx,undoptr之类的信息. 所以sdi page也是数据行, 那么一张表1行就可以存储多张表的元数据信息了.啊,我真是个小天才

FIL_PAGE_INDEX = 17855, FIL_PAGE_SDI = 17853 号比较接近, 所以格式也一样 p_q

既然知道了格式, 那我们就可以通过如下代码来解析了(先不转为DDL), 为了加强记忆, 我们再来回顾下sdi的结构吧.

整体格式是这样的, 但可能存在多行数据, 并且(刚处理事情去了,忘记要说啥了-_-).

如果是 non-leaf page的话, 就没得type和数据之类的, 只有PK(id 8字节),trx(6),undoptr(7) 这21字节, 再加上4字节的PAGENO, 感兴趣的字自己去验证, 这里就不验证了.

由于都是数据行(btr ), 所以也是双向链表, 这就涉及到file_header的格式了.

12--16就是下1页的PAGENO. (checksum, space_id,lsn 是不是看着很眼熟, 前两篇解析的时候有用到). 结构介绍完了, 那就开整.

代码语言:python代码运行次数:0复制
import struct,json,zlib
PAGE_NEW_INFIMUM = 99
filename = '/data/mysql_3314/mysqldata/mysql.ibd'
f = open(filename,'rb')
data = f.read(16384)
# fsp的记录对应general tablespace是没得用的, 但出于礼貌, 我们还是给它留2行代码
sdi_version,sdi_pageno = struct.unpack('>II',data[150 40*256:150 40*256 8])

# inode
f.seek(16384*2,0)
data = f.read(16384)
sdi_segment = data[50:50 192*2]
sdi_leaf_pageno = struct.unpack('>L',sdi_segment[192:][64:68])[0]

# SDI
dd = {}
while sdi_leaf_pageno < 4294967295:
	_ = f.seek(16384*sdi_leaf_pageno,0)
	data = f.read(16384)
	sdi_leaf_pageno = struct.unpack('>L',data[12:16])[0]
	offset = PAGE_NEW_INFIMUM   struct.unpack('>H',data[97:99])[0]
	while True:
		offset  = struct.unpack('>h',data[offset-2:offset])[0] # 注意是有符号的. 但不涉及到数据修改, 其实也无所谓
		if offset > 16384 or offset == 112:
			break
		sdi_type,id = struct.unpack('>LQ',data[offset:offset 12])
		trx1,trx2,undo1,undo2,undo3 = struct.unpack('>LHLHB',data[offset 12:offset 25])
		trx = (trx1<<16)   trx2
		undo = (undo1<<24)   (undo2<<8)   undo3
		dunzip_len,dzip_len = struct.unpack('>LL',data[offset 25:offset 33])
		unzbdata = zlib.decompress(data[offset 33:offset 33 dzip_len])
		dic_info = json.loads(unzbdata.decode())
		dd[dic_info['dd_object']['name']] = dic_info

print('TOTAL TABLES:',len(dd))
for name in dd:
	print(name)
		

通过这段代码我们就能得到一个sdi dict. 有60张表

但是我们登录数据库里面查看,却只有37张

代码语言:txt复制
(root@127.0.0.1) [mysql]> select count(*) from information_schema.tables where table_schema='mysql';
 ---------- 
| count(*) |
 ---------- 
|       37 |
 ---------- 
1 row in set (0.00 sec)

那是因为像catalogs之类的被隐藏了. 要开debug模式才能查询, 而我们本篇的目的就是直接查询.

解析mysql.ibd文件

既然得到了sdi信息, 那么就可以开始解析那60张表的数据了. 由于存在json之类的数据格式, 所以我们就使用ibd2sql来做吧 (稍微改改就行, 把那几个类的使用方法重写一下)

数据行的结构和上面sdi的结构一致, 就不再介绍了

代码语言:python代码运行次数:0复制
from ibd2sql.innodb_page_sdi import *
from ibd2sql import __version__
from ibd2sql.ibd2sql import ibd2sql


class sdi2(sdi):
	def __init__(self,*args,**kwargs):
		super().__init__(*args,**kwargs)
		self.dd = kwargs['dd']
		self.table = TABLE()
		self._init_table()
		self.table._set_name()
	def get_dict(self):
		return self.dd

class ibd2sql2(ibd2sql):
	def init(self,):
		self.f = open(self.FILENAME,'rb')
		self.PAGE_ID = 2
		self.first_no_leaf_page = 82
		self.first_leaf_page = 0

for name in dd:
	aa = sdi2(b'x00'*16384,dd=dd[name])
	ddcw = ibd2sql2()
	ddcw.FILENAME = filename
	ddcw.IS_PARTITION = True
	ddcw.table = aa.table
	ddcw.replace_schema('ddcw') # 替换schema,方便导入数据库
	ddcw._init_table_name()
	ddcw.init()
	ddcw.first_no_leaf_page = int(dict([ y.split('=') for y in dd[name]['dd_object']['indexes'][0]['se_private_data'].split(';')[:-1]])['root'])
	ddcw.init_first_leaf_page()
	print(ddcw.get_ddl()) # 打印DDL
	print(ddcw.get_sql()) # 打印数据

让我们来瞧瞧

感觉挂怪的, 但好像又没得问题...

就不做更深入的测试了.

总结

  1. mysql 8.0的系统表使用innodb存储引擎, 存放在mysql.ibd文件中, 是general tablespace类型, 即多个系统表都存放在一个表空间里面.
  2. 正常情况需要debug才能看到的系统信息, 现在可以通过解析mysql.ibd文件来获取啦.
  3. 实际使用的时候, 建议cp到tmp目录之类的再解析.(虽然只要读权限就行, 但稳一手)
  4. 系统表的信息比较敏感,不要随便传(里面有mysql.user表的啊)

参考

https://github.com/ddcw/ibd2sql

https://dev.mysql.com/doc/refman/8.0/en/general-tablespaces.html

https://github.com/mysql/mysql-server/blob/trunk/utilities/ibd2sdi.cc

附源码

要结合ibd2sql来使用.

例子:

代码语言:shell复制
wget https://github.com/ddcw/ibd2sql/archive/refs/heads/main.zip
unzip main.zip
cd ibd2sql-main/
vim get_mysql_ibd.py # 粘贴下面的那段代码
python get_mysql_ibd.py /data/mysql_3314/mysqldata/mysql.ibd > /tmp/t20240918.sql

源码:

代码语言:python代码运行次数:0复制
import struct,json,zlib
from ibd2sql.innodb_page_sdi import *
from ibd2sql import __version__
from ibd2sql.ibd2sql import ibd2sql
import sys,os



PAGE_NEW_INFIMUM = 99
if len(sys.argv)!=2:
	print('USAGE: python3 get_mysql_ibd.py /data/mysql.ibd')
	sys.exit(2)
filename = sys.argv[1]
if not os.path.exists(filename):
	print(filename,' not exists')
	sys.exit(1)
f = open(filename,'rb')
data = f.read(16384)
# fsp的记录对应general tablespace是没得用的, 但出于礼貌, 我们还是给它留2行代码
sdi_version,sdi_pageno = struct.unpack('>II',data[150 40*256:150 40*256 8])

# inode
f.seek(16384*2,0)
data = f.read(16384)
sdi_segment = data[50:50 192*2]
sdi_leaf_pageno = struct.unpack('>L',sdi_segment[192:][64:68])[0]

# SDI
dd = {}
while sdi_leaf_pageno < 4294967295:
	_ = f.seek(16384*sdi_leaf_pageno,0)
	data = f.read(16384)
	sdi_leaf_pageno = struct.unpack('>L',data[12:16])[0]
	offset = PAGE_NEW_INFIMUM   struct.unpack('>H',data[97:99])[0]
	while True:
		offset  = struct.unpack('>h',data[offset-2:offset])[0] # 注意是有符号的. 但不涉及到数据修改, 其实也无所谓
		if offset > 16384 or offset == 112:
			break
		sdi_type,id = struct.unpack('>LQ',data[offset:offset 12])
		trx1,trx2,undo1,undo2,undo3 = struct.unpack('>LHLHB',data[offset 12:offset 25])
		trx = (trx1<<16)   trx2
		undo = (undo1<<24)   (undo2<<8)   undo3
		dunzip_len,dzip_len = struct.unpack('>LL',data[offset 25:offset 33])
		unzbdata = zlib.decompress(data[offset 33:offset 33 dzip_len])
		dic_info = json.loads(unzbdata.decode())
		dd[dic_info['dd_object']['name']] = dic_info

f.close()




class sdi2(sdi):
	def __init__(self,*args,**kwargs):
		super().__init__(*args,**kwargs)
		self.dd = kwargs['dd']
		self.table = TABLE()
		self._init_table()
		self.table._set_name()
	def get_dict(self):
		return self.dd

class ibd2sql2(ibd2sql):
	def init(self,):
		self.f = open(self.FILENAME,'rb')
		self.PAGE_ID = 2
		self.first_no_leaf_page = 82
		self.first_leaf_page = 0

for name in dd:
	try:
		aa = sdi2(b'x00'*16384,dd=dd[name]) # 有空了再改, 先临时用着.... # 其实就是最后一个对象(mysql, 不是表, 即没得mysql.mysql)
	except:
		continue
	ddcw = ibd2sql2()
	ddcw.FILENAME = filename
	ddcw.IS_PARTITION = True
	ddcw.table = aa.table
	ddcw.replace_schema('ddcw') # 替换schema,方便导入数据库
	ddcw._init_table_name()
	ddcw.init()
	ddcw.first_no_leaf_page = int(dict([ y.split('=') for y in dd[name]['dd_object']['indexes'][0]['se_private_data'].split(';')[:-1]])['root'])
	try:
		ddcw.init_first_leaf_page()
		print(ddcw.get_ddl()) # 打印DDL
		sql = ddcw.get_sql()
		if sql is not None:
			print(sql)  # 打印数据
	except:
		pass

0 人点赞