暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

MYSQL 文件解析 (1) binlog 文件解析

原创 大大刺猬 2023-04-23
1373

写在前面

本文有点长, 不耐心的可以直接看总结.

说明

也可以使用gdb查看生成binlog过程的, 但是太复杂了… 还是看源码注释方便点.

本文主要介绍的binlog 4的格式,下面使用的均是binlog4的情况, 然后使用python解析该格式与mysqlbinlog做对比.

解析binlog的工具有: mysqlbinlog, binlog2sql, pymysqlreplication等.

下面的int类型未特殊说明均使用小端(little), 均为无符号

我的环境:

mysql 5.7.38-log

binlog_format = ROW

binlog_row_image = FULL

binlog_checksum = None #不影响,反正是最后4字节

可变长度计算方法:

本文例子均只考虑第一种情况. 主要是演示
image.png

BINLOG文件格式

官网介绍binlog文件 由开头的 4字节(0xFE 'bin’) 加上一些列的 event 组成.

第一个event是START_EVENT_V3或者FORMAT_DESCRIPTION_EVENT.

最后一个event是STOP_EVENT或者rROTATE_EVENT

所以我们着重看event组成即可.
image.png

BINLOG EVENT

binlog event组成

binlog event是 由 event_header(固定19字节) 加上 event_body(由各事件决定大小)组成

| event header(19字节) | event body |

event_header

从左到右的字节为如下表内容
image.png

event_body

event_body由 post_header body crc32(可选,由变量binlog_checksum决定)组成

| post_header | body | crc32 |

不同的event的内容不一样, 下面讲下常用的event格式

binlog event分类

image.png

常见binlog event结构

FORMAT_DESCRIPTION_EVENT

每个Binlog文件的第一个event, 这个event记录如下数据
image.png

TABLE_MAP_EVENT

这个是row格式独有的, 记录下个row_event的表名,各字段类型.

除了开头8字节外(post_header)外, 均为可变长度…

image.png

ROWS_EVENT

row_event 包含Delete_rows_log_event 和 Write_rows_log_event 和 Update_rows_log_event

继承关系如下图
image.png
Delete_rows_log_event 和 Write_rows_log_event 和 Update_rows_log_event 基本上一样, 都是继承自row_log_event

区别在于 Write_rows_log_event(insert) 没得Cols_before_image delete_rows_log_event没得Cols_after_image

         +-------------------------------------------------------+
         | Event Type | Cols_before_image | Cols_after_image     |
         +-------------------------------------------------------+
         |  DELETE    |   Deleted row     |    NULL              |
         |  INSERT    |   NULL            |    Inserted row      |
         |  UPDATE    |   Old     row     |    Updated row       |
         +-------------------------------------------------------+

结构如下

也是只有开头的8字节(post header)固定

image.png

验证

生成测试数据

另起一个binlog 方便观察

(root@127.0.0.1) [(none)]> flush logs;
Query OK, 0 rows affected (0.01 sec)

(root@127.0.0.1) [(none)]> create table db1.t20230310(id int primary key, name varchar(20));
Query OK, 0 rows affected (0.01 sec)

(root@127.0.0.1) [(none)]> begin;
Query OK, 0 rows affected (0.00 sec)

(root@127.0.0.1) [(none)]> insert into db1.t20230310 values(1,'first'),(2,'ddcw');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

(root@127.0.0.1) [(none)]> delete from db1.t20230310 where id=1;
Query OK, 1 row affected (0.00 sec)

(root@127.0.0.1) [(none)]> update db1.t20230310 set name = 'ddcw update' where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

(root@127.0.0.1) [(none)]> commit;
Query OK, 0 rows affected (0.01 sec)

(root@127.0.0.1) [(none)]> show master status\G
*************************** 1. row ***************************
             File: m3308.001008
         Position: 1027
     Binlog_Do_DB: 
 Binlog_Ignore_DB: 
Executed_Gtid_Set: 6d650f1f-ba4e-11ed-99ab-000c2980c11e:1-29253,
7ab066ef-c1be-11ec-92dd-000c2980c11e:2579-2584:2700,
90bdfbb7-cbe2-11ec-a870-000c2980c112:25178542,
90bdfbb7-cbe2-11ec-a870-000c2980c11e:1-14138280:25178542,
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-283382
1 row in set (0.00 sec)

hexdump解析

这个就是人工解析了, 只解析一部分. 重复的工作应该机器做

只解析第一个event吧…

12:23:55 [root@ddcw21 ~]#hexdump -C /data/mysql_3308/mysqllog/binlog/m3308.001008 
00000000  fe 62 69 6e 35 b0 0a 64  0f 6c 30 ad 18 77 00 00  |.bin5..d.l0..w..|
00000010  00 7b 00 00 00 01 00 04  00 35 2e 37 2e 33 38 2d  |.{.......5.7.38-|
00000020  6c 6f 67 00 00 00 00 00  00 00 00 00 00 00 00 00  |log.............|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 13  |................|
00000050  38 0d 00 08 00 12 00 04  04 04 04 12 00 00 5f 00  |8............._.|

前4个字节 fe 62 69 6e 查询ascii表得 ‘\xfebin’ 和上面官方说的一致
image.png
再来看看第一个event, 前19字节是header, 不看了, 太多了. 从 4 + 19 字节看起走

注意是使用的小端
image.png
image.png

Python解析

人工解析确实太费劲了, 我们使用python来解析

脚本见文末. 此脚本未解析 row(需要TABLE_MAP_EVENT) 和 crc32

import row_event
aa = row_event.parse_event('/data/mysql_3308/mysqllog/binlog/m3308.001008',1000)
for x in aa:
	print(x)

image.png
我们人工解析下最后个update的row

数据 b’\xfc\x02\x00\x00\x00\x04ddcw\xfc\x02\x00\x00\x00\x0bddcw update’

类型 [3, 15] 查表 得 3对应 int<4> 15对应varchar

未使用binlog crc32校验(mgr), 有的话, row的最后4字节就是crc32校验

>>> def btoint(bdata,t='little'):
...         return int.from_bytes(bdata,t)
... 
>>> aa = b'\xfc\x02\x00\x00\x00\x04ddcw\xfc\x02\x00\x00\x00\x0bddcw update'
>>> aa[0:1] #befor image null_bit_map
b'\xfc'
>>> btoint(aa[1:1+4]) #before image first column
2
>>> btoint(aa[5:6]) #查看varchar记录的长度, 仅考虑0-250(1字节的情况)
4
>>> aa[6:6+4] #before image seccond column
b'ddcw'
>>> 
>>> aa[10:11] #after image的 null_bit_map
b'\xfc'
>>> btoint(aa[11:15]) #after image的第一列 
2
>>> btoint(aa[15:16]) #after image的 第二列的长度
11
>>> aa[16:27]
b'ddcw update'
>>> 

得到 Update前数据 (2,‘ddcw’) update后数据为(2,‘ddcw update’)

然后使用mysqlbinlog 解析对比一下, 发现对的上, 说明没有解析错
image.png

总结

  1. binlog文件由开头固定4字节和 各个event组成 (relay log也是)

  2. 每个event由 header(固定19字节) 和 body组成, body又由post header 和 data组成(若剩余4字节就是crc32校验码)

  3. 每个row_event前面都有个table_map_event记录表名,字段类型等信息.

  4. 最后一个event如果是stop_event, 那就说明服务器停止了(下次启动字段切换), 如果是rota_event就说明文件切换了.

  5. Delete_rows_log_event 和 Write_rows_log_event 和 Update_rows_log_event 都是继承自row_log_event, 区别在于
    Write_rows_log_event(insert) 没得Cols_before_image

    delete_rows_log_event没得Cols_after_image

PYTHON源码

row_event.py

import binlog_event_type
import struct
def btoint(bdata,t='little'):
	return int.from_bytes(bdata,t)

def event_header(bdata):
	timestamp, event_type, server_id, event_size, log_pos, flags = struct.unpack("<LBLLLh",bdata[0:19])
	return {"timestamp":timestamp,'event_type':event_type,'server_id':server_id,'event_size':event_size,'log_pos':log_pos,'flags':flags,}


def first_event(bdata):
	#FORMAT_DESCRIPTION_EVENT
	ethl = len(bdata) - 57 #2 50 4 1 var
	ff = f'<h50sLB{ethl}s'
	binlog_version, mysql_server_version, create_timestamp, event_header_length, event_type_header_length = struct.unpack(ff,bdata)
	mysql_server_version = mysql_server_version.decode('ascii').replace('\x00','') #美化一下
	event_type_header_length = [ int(x) for x in event_type_header_length ] #event specific header length. 比如TABLE_MAP_EVENT = 8 (table_id:6 + flag:2) #记录其它event的post header的长度
	return {'binlog_version':binlog_version, 'mysql_server_version':mysql_server_version, 'create_timestamp':create_timestamp, 'event_header_length':event_header_length, 'event_type_header_length':event_type_header_length,}

def table_map_event(bdata):
	post_header = {'table_id':btoint(bdata[0:6]), 'flags':btoint(bdata[6:8])} #flags保留字段
	offset = 8
	database_length = btoint(bdata[offset:offset+1])
	offset +=1
	database_name = bdata[offset:offset+database_length].decode() #0x00 结尾
	offset += database_length + 1
	table_length = btoint(bdata[offset:offset+1])
	offset +=1
	table_name = bdata[offset:offset+table_length].decode() #0x00 结尾, 但是我不读,计数的时候别忘了就行
	offset += table_length + 1
	column_count = btoint(bdata[offset:offset+1]) #Packed Integer 我只考虑0-250个字段. 也就是占用1字节 计算方式https://dev.mysql.com/doc/dev/mysql-server/latest/classbinary__log_1_1Binary__log__event.html#packed_integer
	offset += 1
	column_type_list = []
	for x in range(column_count):
		column_type_list.append(btoint(bdata[offset:offset+1])) #先不做转换了.具体类型参考https://dev.mysql.com/doc/dev/mysql-server/latest/classbinary__log_1_1Table__map__event.html
		offset += 1

	#metadata_length和column_count一样, 但是我不想写了
	#省略 metadata_length metadata null_bits optional metadata fields
	return {
		'post_header':post_header,
		'body':{
			'database_name':database_name,
			'table_name':table_name,
			'column_type_list':column_type_list,
		}
		}
	


def row_event(bdata,imaget):
	#不解析具体的字段, 因为需要table_map才知道对应的字段类型
	#columns_before_image delete,update
	#columns_after_image  insert,update
	data = {}
	post_header = {'table_id':btoint(bdata[0:6]), 'flags':btoint(bdata[6:8])} #flags保留字段
	data['post_header'] = post_header
	data['body'] = {}
	offset = 8
	width = btoint(bdata[offset:offset+1])
	offset += 1
	_toff = int((width+7)/8)
	cols = btoint(bdata[offset:offset+_toff])
	offset += _toff
	extra_row_info = btoint(bdata[offset:offset+1])
	offset += 1
	if imaget == 30 or imaget == 31: #30 write   31 update   32 delete
		columns_after_image = btoint(bdata[offset:offset+_toff])
		offset += _toff
		data['body']['columns_after_image'] = columns_after_image
	if imaget == 32 or imaget == 31:
		columns_before_image = btoint(bdata[offset:offset+_toff])
		offset += _toff
		data['body']['columns_before_image'] = columns_before_image
	data['body']['row'] = bdata[offset:]
	data['body']['width'] = width
	data['body']['cols'] = cols
	data['body']['extra_row_info'] = extra_row_info
	return data
		



def parse_event(filename,n=10): #默认只解析前面10个event 
	data = []
	with open(filename,'rb') as f:
		magic = f.read(4)
		if magic != b'\xfebin':
			return False
		for x in range(n):
			event_data = None
			try:
				common_header = event_header(f.read(19))
			except:
				break
			event_bdata = f.read(common_header['event_size']-19)
			if common_header['event_type'] == binlog_event_type.FORMAT_DESCRIPTION_EVENT:
				event_data = first_event(event_bdata)
				common_header['event_type'] = 'FORMAT_DESCRIPTION_EVENT'
			elif common_header['event_type'] == binlog_event_type.WRITE_ROWS_EVENT:
				event_data = row_event(event_bdata,common_header['event_type'])
				common_header['event_type'] = 'WRITE_ROWS_EVENT'
			elif common_header['event_type'] == binlog_event_type.UPDATE_ROWS_EVENT:
				event_data = row_event(event_bdata,common_header['event_type'])
				common_header['event_type'] = 'UPDATE_ROWS_EVENT'
			elif common_header['event_type'] == binlog_event_type.DELETE_ROWS_EVENT:
				event_data = row_event(event_bdata,common_header['event_type'])
				common_header['event_type'] = 'DELETE_ROWS_EVENT'
			elif common_header['event_type'] == binlog_event_type.TABLE_MAP_EVENT:
				event_data = table_map_event(event_bdata)
				common_header['event_type'] = 'TABLE_MAP_EVENT'
			elif common_header['event_type'] == binlog_event_type.GTID_LOG_EVENT:
				common_header['event_type'] = 'GTID_LOG_EVENT'
			elif common_header['event_type'] == binlog_event_type.XID_EVENT:
				common_header['event_type'] = 'XID_EVENT'
			elif common_header['event_type'] == binlog_event_type.QUERY_EVENT:
				common_header['event_type'] = 'QUERY_EVENT'
				event_data = event_bdata
			elif common_header['event_type'] == binlog_event_type.STOP_EVENT:
				common_header['event_type'] = 'STOP_EVENT'
			elif common_header['event_type'] == binlog_event_type.PREVIOUS_GTIDS_LOG_EVENT:
				common_header['event_type'] = 'PREVIOUS_GTIDS_LOG_EVENT'
			elif common_header['event_type'] == binlog_event_type.ROTATE_EVENT:
				common_header['event_type'] = 'ROTATE_EVENT'
			data.append({'event_header':common_header,'event_body':event_data})
	return data

binlog_event_type.py

从源码 libbinlogevents/include/binlog_event.h 里面复制出来的

# -*- coding: utf-8 -*-
# libbinlogevents/include/binlog_event.h
UNKNOWN_EVENT= 0
START_EVENT_V3= 1
QUERY_EVENT= 2
STOP_EVENT= 3
ROTATE_EVENT= 4
INTVAR_EVENT= 5
LOAD_EVENT= 6
SLAVE_EVENT= 7
CREATE_FILE_EVENT= 8
APPEND_BLOCK_EVENT= 9
EXEC_LOAD_EVENT= 10
DELETE_FILE_EVENT= 11
NEW_LOAD_EVENT= 12
RAND_EVENT= 13
USER_VAR_EVENT= 14
FORMAT_DESCRIPTION_EVENT= 15
XID_EVENT= 16
BEGIN_LOAD_QUERY_EVENT= 17
EXECUTE_LOAD_QUERY_EVENT= 18

TABLE_MAP_EVENT = 19

PRE_GA_WRITE_ROWS_EVENT = 20
PRE_GA_UPDATE_ROWS_EVENT = 21
PRE_GA_DELETE_ROWS_EVENT = 22

WRITE_ROWS_EVENT_V1 = 23
UPDATE_ROWS_EVENT_V1 = 24
DELETE_ROWS_EVENT_V1 = 25

INCIDENT_EVENT= 26

HEARTBEAT_LOG_EVENT= 27

IGNORABLE_LOG_EVENT= 28
ROWS_QUERY_LOG_EVENT= 29

WRITE_ROWS_EVENT = 30
UPDATE_ROWS_EVENT = 31
DELETE_ROWS_EVENT = 32

GTID_LOG_EVENT= 33
ANONYMOUS_GTID_LOG_EVENT= 34

PREVIOUS_GTIDS_LOG_EVENT= 35 #描述之前的gtid信息(不需要扫描之前的binlog文件)

TRANSACTION_CONTEXT_EVENT= 36

VIEW_CHANGE_EVENT= 37

XA_PREPARE_LOG_EVENT= 38
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论