MYSQL Binlog协议分析
此处不讨论建立连接,验证和handshake的交互协议
Binlog协议
一个MYSQL 通信包由包头包体组成
包体根据具体的交互协议有自身的组成结构, 在binlog消息体组成结构如下
+=====================================+
| event | timestamp 0 : 4 |
| header +----------------------------+
| | type_code 4 : 1 |
| +----------------------------+
| | server_id 5 : 4 |
| +----------------------------+
| | event_length 9 : 4 |
+=====================================+
| event | fixed part 13 : y |
| data +----------------------------+
| | variable part |
+=====================================+
注意: 消息体里组成是包括2部分的,第一个字节是master发送给slave的errorcode, 第二个字节开始才是具体消息, 所以一个完整的MYSQL binlog通信包组成如下
一次binlog复制通信包含若干个binlog通信包
我们通过抓包工具抓取一次binlog复制来分析
我们在master里更新了一条数据, 产生了 41 这条通信数据, 里面包含了4个mysql binlog通信包
第一个binlog事件消息
头3个字节 45 00 00就是消息长度, 注意,在mysql协议里,数值类型是用小序列来传输数据的(小序列: 低位先传), 这条消息里实际消息长度内容是 00 00 45 , 也就是69(45是16进制, 69是10进制)
第4个字节2c是master传过来的序号
第5个字节00时errorcode, 0表示没有错误, 其他是错误
第6~9 4个字节是timestamp
第10 个字节02 是event type, 比如query, insert, update,delete等的类型码
第11-14个字节是serverid, 这里是02 00 00 00, 也是小序列
第15~19个字节是eventlengthoffset, 这里是44 00 00 00, 也是小序列
第二个binlog事件消息
结构和第一个基本一样, 只是消息长度,序号,eventtype等内容不一样
以上是原生mysql复制binlog的交互协议情况, 如果开启了semi半同步, 协议就发生变化, tcp通信次数, 包体结构等都和原来的有差异
抓包分析:
我们在master里更新了一条数据, 产生了 5 这条通信数据, 里面包含了5个mysql binlog通信包, 比原生的多了1条, 而且包结构也变成了以下结构
第一个binlog事件消息是新增的事件
第6个字节是新增的字节码, ef是固定的值,表示这是个semi的消息
第7个字节是新增的字节码, 0或者1, 0表示不需要回一个ack给master, 1表示需要, 一次复制通信(包含5个binlog事件)只有最后一个binlog事件的值为1
第8个字节开始就是具体消息, 和原生的一样
最后一个binlog事件消息
第7个字节值是1 , 表示要返回一个ack给master
以下就是ack给master的内容
Slave ack给master 的消息是7这条通信数据
第1-3个字节师消息长度,这里的内容是12 00 00, 十进制内容是18
第4个字节是序号 , 这里是0, (注意: 每次tcp交互里这个序号都被重至为0)
第5个字节是semi标示 ,固定为ef
第6~14个字节,一共8个字节表示位置offset, ef 0a 00 00 00 00 00 00 同样是小序列
第15个字节开始是binlog的文件名 每个字节存储的是ascii码, 如这里的4f=O, 4e=N,2e=.,30=0,36=6, 就是ON.000006
如果安装了semi插件后,并不启用semi同步,就不会发送ack给master, 就是上面7这个数据包不会发送
Master要发送semi字节给slave前提是slave连接上master后必须发送SET @rpl_semi_sync_slave= 1 指令给master, 看下面抓包效果
1c 00 00 00 是包头
0x03 是指令码 COM_QUERY
53开始后面的字节都是具体字符串的ascii码
完整事件流
Slave连接上master后, master 首先会发ROTATE_EVENT和FORMAT_DESCRIPTION_EVENT 2个事件给slave, ROTATE_EVENT事件告诉slave下一个要读取的binlog(可以理解成初始化要读取的binlog)
Master发生一个插入sql, 如insert into test1 values(15), 分别发送给slave的事件是ANONYMOUS_GTID_LOG_EVENT-->QUERY_EVENT-->TABLE_MAP_EVENT-->WRITE_ROWS_EVENT-->XID_EVENT
(1)
ANONYMOUS_GTID_LOG_EVENT
(2)
QUERY_EVENT
header {
version: 1
logfileName: "20170105-162017-bin.000001"
logfileOffset: 1920
serverId: 1
serverenCode: "UTF-8"
executeTime: 1508809530000
sourceType: MYSQL
schemaName: ""
tableName: ""
eventLength: 72
}
entryType: TRANSACTIONBEGIN
storeValue: " 354 01"
(3)
TABLE_MAP_EVENT
(4)
WRITE_ROWS_EVENT
header {
version: 1
logfileName: "20170105-162017-bin.000001"
logfileOffset: 2040
serverId: 1
serverenCode: "UTF-8"
executeTime: 1508809530000
sourceType: MYSQL
schemaName: "test"
tableName: "test1"
eventLength: 40
eventType: INSERT
}
entryType: ROWDATA
storeValue: "333 01 20 01P 00b 35 22 33 00 20 04 32 02id 00( 010 00B 0215Raint(11)"
(5)
XID_EVENT
header {
version: 1
logfileName: "20170105-162017-bin.000001"
logfileOffset: 2080
serverId: 1
serverenCode: "UTF-8"
executeTime: 1508809530000
sourceType: MYSQL
schemaName: ""
tableName: ""
eventLength: 31
}
entryType: TRANSACTIONEND
storeValue: " 22 03184"
PS:
如果本地没有存储position,在slave启动的时候就会调用show master status 获取master 最新的position更新到本地
在实验中发现 master 的timeout时间设置很长, 当新日志送到canal, canal关闭,master由于收不到ack,一直hold着,再次启动canal, master hold住的事务能够进行下去,数据也落盘到master硬盘里,但再次启动的canal就没有收到之前中断掉的binlog(开始position是master最后的position)