写在前面
本文有点长, 不耐心的可以直接看总结.
说明
也可以使用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字节
可变长度计算方法:
本文例子均只考虑第一种情况. 主要是演示
第一个字段值 | 占用大小 取值范围 |
0-250 | 1字节 0-250 |
252 | 2字节 251-0xffff |
253 | 3字节 0xffff-0xffffff |
254 | 8字节 0xffffff-0xfffffffffffffff |
BINLOG文件格式
官网介绍binlog文件 由开头的 4字节(0xFE 'bin’
) 加上一些列的 event 组成.
第一个event是START_EVENT_V3或者FORMAT_DESCRIPTION_EVENT.
最后一个event是STOP_EVENT或者rROTATE_EVENT
所以我们着重看event组成即可.
BINLOG EVENT
binlog event组成
binlog event是 由 event_header(固定19字节) 加上 event_body(由各事件决定大小)组成
| event header(19字节) | event body |
event_header
从左到右的字节为如下表内容
数据类型 | 名字 | 描述 |
int<4> | timestamp | 时间戳 |
int<1> | event_type | event类型(5.7.38就有43种...) |
int<4> | server-id | server_id |
int<4> | event-size | event大小(含event_header的19字节) |
int<4> | log-pos | 下个event的起始地址(本event的结束地址) |
int<2> | flags | flag |
event_body
event_body由 post_header body crc32(可选,由变量binlog_checksum决定)组成
| post_header | body | crc32 |
不同的event的内容不一样, 下面讲下常用的event格式
binlog event分类
只列举部分.
管理类:
主要是控制识别binlog file的
- START_EVENT_V3 第一个event
- FORMAT_DESCRIPTION_EVENT 第一个event,替代start_event_v3的, 格式同start_event_v3
- STOP_EVENT 最后一个event, 表示服务器已经停止运行(下次启动自动轮转)
- ROTATE_EVENT 最有一个event, 服务器还在运行(自动轮转或者flush log)
- SLAVE_EVENT
- INCIDENT_EVENT
- HEARTBEAT_EVENT
语句类:
- QUERY_EVENT 存SQL的, 比如DDL
- INTVAR_EVENT
- RAND_EVENT
- USER_VAR_EVENT
- XID_EVENT 2pc提交的时候会写入这个event. 当作事务结束的标志
ROW格式类:
下面会讲这个的详细结构
- TABLE_MAP_EVENT 每个row event前面都有个table_map_event记录表名和字段数据类型
- DELETE_ROWS_EVENT 记录delete的
- UPDATE_ROWS_EVENT 记录update的
- WRITE_ROWS_EVENT 记录insert的
常见binlog event结构
FORMAT_DESCRIPTION_EVENT
每个Binlog文件的第一个event, 这个event记录如下数据
名字 | 大小(字节) | 描述 |
binlog_version | int<2> | 记录binlog版本的, 均为4 |
mysql_server_version | char<50> | 记录mysql版本的, 不足的填充0 |
create_timestamp | int<4> | 创建时间 |
event_header_length | int<1> | 每个event的event_header的大小,固定值19 |
event_type_header_length | header的event_size减去上面的大小(19), 为数组类型, 每个记录值为1字节 | 每个event的event body的post header的大小 |
event_type_header_length 记录值参考(5.7.38-log) [56, 13, 0, 8, 0, 18, 0, 4, 4, 4, 4, 18, 0, 0, 95, 0, 4, 26, 8, 0, 0, 0, 8, 8, 8, 2, 0, 0, 0, 10, 10, 10, 42, 42, 0, 18, 52, 0, 1, 20, 121, 129, 83]
TABLE_MAP_EVENT
这个是row格式独有的, 记录下个row_event的表名,各字段类型.
除了开头8字节外(post_header)外, 均为可变长度....
名字 | 大小(字节) | 描述 |
table_id | int<6> | 表打开的id, 不是数据库里面的table_id |
flags | 2 | 保留字段 |
database_name_length | 可变长度 | 数据库名长度 |
database_name | 取决于database_name_length | 数据库名(以0x00结尾, 这个字节不计算在database_name_length中) |
table_name_length | 可变长度 | 表名长度 |
table_name | 取决于table_name_length | 表名(以额外的0x00结尾, 就是不在table_name_length的计算中) |
column_count | 可变长度 | 多少个字段 |
column_type_list | 取决于column_count | list类型, 每个字段的数据类型,用1字节表示(比如3表示int<4> 详情) |
..... | ..... | 暂时用不上其它的 |
ROWS_EVENT
row_event 包含Delete_rows_log_event 和 Write_rows_log_event 和 Update_rows_log_event
继承关系如下图
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)固定
名字 | 大小(字节) | 描述 |
table_id | 6 | |
flags | 2 | |
width | 可变长度 | 表有多少列 |
cols | INT((width + 7) / 8) | 是否使用该列, 每列对于一个bit位, 对字节向上取整(数据读写只能按字节读写) |
extra_row_info | Extra_row_info | 暂不考虑, 以1字节算 |
columns_before_image | INT((width + 7) / 8) | 仅update和delete有. 与binlog_row_image有关 |
columns_after_image | INT((width + 7) / 8) | 仅update和insert有 |
Null_bit_mask | INT((width + 7) / 8) | 某列是否为空, 每列对应一个比特位 |
row | table_map_event记录了大小 | 具体的数据(before_image + after_image), 如果还剩4字节的话, 就是CRC32校验. 每个image数据前面都有null_bit_mask |
验证
生成测试数据
另起一个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'
和上面官方说的一致
再来看看第一个event, 前19字节是header, 不看了, 太多了. 从 4 + 19 字节看起走
注意是使用的小端
名字 | 二进制数据 | ascii编码后 |
binlog_version | b'\x04\x00' | 4 |
mysql_server_version | b'5.7.38-log\x00\x00' 为方便观察,去掉了后面的填充字段 | 5.7.38-log (去掉了填充字段) |
create_timestamp | b'\x00\x00\x00\x00' | 0 |
event_header_length | b'\x13' | 19 |
event_type_header_length | b'8\r\x00\x08\x00 .... \x00m/Lp' 为方便观察省略了中间的数据 | [56, 13, 0, 8, 0, 18, 0, 4, 4, 4, 4, 18, 0, 0, 95, 0, 4, 26, 8, 0, 0, 0, 8, 8, 8, 2, 0, 0, 0, 10, 10, 10, 42, 42, 0, 18, 52, 0, 1, 20, 121, 129, 83] |
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)
每个row_event上面都有个TABLE_MAP_EVENT
我们人工解析下最后个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 解析对比一下, 发现对的上, 说明没有解析错
总结
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