写在前面
本文有点长, 不耐心的可以直接看总结.
说明
也可以使用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字节
可变长度计算方法:
本文例子均只考虑第一种情况. 主要是演示
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
从左到右的字节为如下表内容
event_body
event_body由 post_header body crc32(可选,由变量binlog_checksum决定)组成
| post_header | body | crc32 |
不同的event的内容不一样, 下面讲下常用的event格式
binlog event分类
常见binlog event结构
FORMAT_DESCRIPTION_EVENT
每个Binlog文件的第一个event, 这个event记录如下数据
TABLE_MAP_EVENT
这个是row格式独有的, 记录下个row_event的表名,各字段类型.
除了开头8字节外(post_header)外, 均为可变长度…
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)固定
验证
生成测试数据
另起一个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 字节看起走
注意是使用的小端
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)
复制
我们人工解析下最后个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 解析对比一下, 发现对的上, 说明没有解析错
总结
-
binlog文件由开头固定4字节和 各个event组成 (relay log也是)
-
每个event由 header(固定19字节) 和 body组成, body又由post header 和 data组成(若剩余4字节就是crc32校验码)
-
每个row_event前面都有个table_map_event记录表名,字段类型等信息.
-
最后一个event如果是stop_event, 那就说明服务器停止了(下次启动字段切换), 如果是rota_event就说明文件切换了.
-
Delete_rows_log_event 和 Write_rows_log_event 和 Update_rows_log_event 都是继承自row_log_event, 区别在于
Write_rows_log_event(insert) 没得Cols_before_imagedelete_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
复制