ITPub博客

首页 > 数据库 > 数据库开发技术 > 成为数据治理专家:MySQL Binlog 协议篇

成为数据治理专家:MySQL Binlog 协议篇

原创 数据库开发技术 作者:DevXTalk 时间:2024-01-13 10:00:00 0 删除 编辑

成为数据治理专家:MySQL Binlog 协议篇

Replication 过程

  1. 通过 HandShake 协议进行 Client 和 MySQL 的握手认证。
  2. 握手成功以后,Client 对 MySQL 发送 show master status 命令,此命令中回带回当前最新 binlog 存储在哪个文件,以及对应哪个偏移量。如果想从当前开始接收 binlog, 则在后面发送 binlog dump 命令的时候用这两个值就好。
  1. 发送 show global variables like 'binlog_checksum' 命令,这是由于 binlog event 发送回来的时候需要,在最后获取 event 内容的时候,会增加 4 个额外字节做校验用。mysql5.6.5 以后的版本中 binlog_checksum=crc32, 而低版本都是 binlog_checksum=none。如果不想校验,可以使用 set 命令设置 set binlog_checksum=none
  2. 最后终于到了发送 Dump 命令的阶段。

当客户端向服务端发送 COM_BINLOG_DUMP 指令后, MySQL 服务端便开始向客户端以字节流的形式传送 Binlog, 对于 Statement-Based Binlog, 日志是由事件序列构成的, 事件有多种类型, 具体的事件列表可以参看 Binlog-EventType, MySQL 服务端在传送 Binlog 时, 会在每个事件前面添加 0x00 这样一个分界符字节, 每个事件都由事件头和主体构成, 主体进一步分为固定部分和可变部分, 因此也可以理解为事件是由三部分构成的, 事件头(EventHeader), 主体的固定部分(PostHeader) 和主体的可变部分(Payload), 所有的 Binlog 文件都是以固定的 4 字节开头, 这 4 个字节分别是 0xfe 0x62 0x69 0x6e, 即 0xfe 'b''i''n', 在解析 Binlog 数据时, 如果开头不是这 4 个字节可以认为数据是非法的或者文件已损坏, 在 Binlog 文件中, 第一个事件比较特殊, 在早期版本的 MySQL 中, 第一个事件是 START_EVENT_V3 事件, 记录了 Binlog 的版本, MySQL 服务端的版本以及文件创建的时间戳等, 在 MySQL 5.0 之后, 使用 FORMAT_DESCRIPTION_EVENT 来替代 START_EVENT_V3 结构, FORMAT_DESCRIPTION_EVENT 拥有比 START_EVENT_V3 更丰富的字段信息, 并具有扩展性, 除了在 START_EVENT_V3 中的诸如 Binlog 版本、MySQL Server 版本以及时间戳之外, 还描述了每种事件类型对应的事件头(EventHeader)的长度, 客户端可以根据这些信息来解码读取对应的事件类型, 在 MySQL 服务端, Binlog 是以普通二进制文件存放的, 它们的名字形如 mysql-bin.NNNNNN(如 mysql-bin.000001、mysql-bin.000002 等)每个 Binlog 文件都有最大限制, 可以在 MySQL 的配置文件中设置单个 Binlog 的最大长度, 当 Binlog 文件长度超过特定的限制后, MySQL Server 便会创建一个新的 Binlog 文件并继续保存相应的日志(另外当执行 FLUSH LOGS 时也会新创建 Binlog 文件), 除了日志文件本身, MySQL Server 还维护了一个 mysql-bin.index 文件, 该文件是用文本形式存放的, 记录了当前已有的 Binlog 文件名。

整个主从同步过程是这样的, MySQL Server 当有数据变更时会将数据按设定的格式写入到形如 mysql-bin.NNNNNN 的文件中, 当接收到 Slave 的 DUMP 请求后, Server 端便开启 Dump Thead, 用于并发地读取 Binlog 文件内容并发送给 Slave, 而 Slave 设置了一个 IO Thread 来接收客户端发来的字节流, 并其写入到 Relay log 中, Relay log 与 Binlog 的格式完全相同, 所以 IO Thread 其实并不会对数据做任何修改, 只是单纯地读取字节流并写入本地文件, Relay log 设置的目的是为了缓冲, 若没有 Relay log, 则 Slave 必须以完全同步的方式接收并执行 Master 发来的数据, 同时在 Slave 端也设置了一个 SQL Thread 它并发地读取 Relay Log, 并将数据应用到数据库中, 整个过程可以用如下的图示来描述。

binlog 协议

在 MySQL 协议中数据类型只有两大类, 分别是整数(Integers) 和字符串(Strings), 这两种数据类型又会分为多个更小的数据类型, 对于整数类型, 可以进一步分为定长整数(Fixed-Length Integer) 和长度编码型整数(Length-Encoded Integer), 对于定长整数, 顾名思义, 其长度是固定的, 定长整数有 int<1>、int<2>、int<3>、int<4>、int<6>、int<8> 共 6 种, 定长整数都是以小端字节序存放的, 即最低有效位在前, 举例来说, 对于 int<4> 的数字 2, 其字节概况为 10 00 00 00; 而长度编码型整数, 它所占用的字节数不是固定的, 取决于值的大小, 记为 int, 这是一种变长编码方式, 长度编码型整数占用的字节数有 1、3、4、9 字节共 4 种情况, 数字越小, 占用的字节数越少, 当数字小于 0xfb 时, 编码后其占用一个字节, 当数字大于等于 0xfb 并且小于等于 0x7ffff 时, 其编码后将占用 3 个字节(固定的 0xfc + 2 个字节数值位), 当数字大于等于 0x10000 且小于等于 0x7fffff 时, 其编码后将占用 4 个字节(固定的 0xfd + 3 字节的数值位), 当数字大于等于 0x1000000 且小于等于 0x7ffffffff 时, 其编码后将占用 9 个字节(固定的 0xfe + 8 字节的数值位), 根据这样的编码规则, 反过来我们也能得到长度编码型整数的解码规则, 识别数据的第一个字节便可以知道其后所跟随的数值位的字节数, 进而可以获得数值本身, 也就是说长度编码型整数实际上是用开头的一字节来指示其后所跟的数据的字节长度, 而其后的数值字节仍然是以小端字节序存储, 这里实际上有点类似于 Redis 的 ziplist 结构中的结点, 在 Redis ziplist 结构中, 其结点的第一个元素 previous_entry_length 也是采用类似的结构来表示前一个结点的长度, 若前一结点的长度小于 254 个字节, 则 previous_entry_length 的本身就是前一结点的长度, 若前一结点的长度大于等于 254 个字节, 则 previous_entry_length 本身占用 5 个字节的长度, 其第一字节的值固定为 0xfe, 而后所跟的 4 个字节指示了其前一个结点的长度。

详情参考 MySQL 官方文档:https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html

字符串类型(Strings) 是字节序列构成的数据类型, 它可以细分为固定长度的字符串 string、空终结字符串 string、变长字符串 string、长度编码型字符串以及特殊的结尾字符串 string, 所谓固定长度字符串, 顾名思义, 其长度是固定不变的, 而空终结字符串是以 0x00 字节指示字符串结尾, 长度编码型字符串是在开头指示了字符串的长度 NN, 然后其后跟 NN 字节的固定长度字符串, 在长度编码型字符串中, 开头的字符串长度是使用长度编码型整数, 即 int指示的, 而特殊的结尾字符串则用于在数据包的末尾, 因为数据包有自然的边界, 所以结尾处的字符串不需要特别设置一个结束标志位。

详情参考 MySQL 官方文档:https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_strings.html

MySQL Packet

MySQL 的客户端和服务端交互是以数据包(Packet) 为单位进行的, 每个包的大小长度有限制, 最长为 224−1224−1 个字节(即 16MB), 若包长度过大, 则客户端需要自行将包分片, 使得每段的长度在 MySQL 包的最大长度之下, MySQL Packet 由 Header 和 Body 组成, Header 包含两个字段: 包长度(payload_length)、序列号(sequence_id), Body 则是包的主体部分, 它的长度由 Header 中的 payload_length 字段指示, 包长度字段固定使用 int<3> 类型, 序列号固定使用 int<1> 类型, 当客户端和服务端开始交互时, 由客户端初始化 0 序列号, 之后每次交互, 数据发出方都基于前一个 sequence_id 增一, sequence_id 不是恒定唯一的, 当交互次数足够多后, 它会重新回到 0, MySQL 的 Packet 可以用如下所示的图来直观表示。

详情参考 MySQL 官方文档:https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_packets.html#sect_protocol_basic_packets_packet

binlog 报文

binlog 报文由 Event Header 和 Event Body 组成。

  1. 前4比特,是当前binlogEvent发生的时间戳。
  2. 1byte的event类型。
  3. 4byte的serverId,是发送event的主库标识。
  4. 4byte的event_length,是指包含当前eventHeader的整个body的长度。
  5. 最后2byte是标志位。

Event Header 结构:

下面会介绍几种不同类型的 Event Body 。

「Rotate Event」

当MySql的binlog文件从file1滚动到file2的时候会发生此事件,会在文件末尾写入 ROTATE_EVENT,指向序列中的下一个文件。该事件是让从属设备知道它将要接收的下一个二进制日志的名称的信息。


「TableMap Event」

在基于行的模式下,每个行操作事件之前都有一个 Table_map_event , 也就是在任何的行变更之前都会记录一个 TableMapEvent。会记录那个数据库的那张表发生了行变更。


「FORMAT_DESCRIPTION Event」

MySQL Binlog v3以前版本, 二进制日志文件的第一个事件是START_EVENT_V3, 从v4版本开始第一个事件为FORMAT_DESCRIPTION_EVENT(以下简称FD事件),替代掉START_EVENT_V3。具体到MySQL服务器版本来说,MySQL 5.0以前版本二进制日志的第一个事件是START_EVENT_V3,而后续版本的第一个事件都是FD事件。


「XID Event」

XID 事件是为修改支持 XA 的存储引擎的一个或多个表的事务提交而生成的。


「QUERY Event」

Rows查询事件类型,它是Ignorable_event的子类,用于在RBR中记录原始查询的rows事件。


「Write Rows Event」

插入行时触发该事件。

  1. 8bytes的tableId
  2. 一个复杂的Bit集合,和下面的Row配合使用
  3. 一个Row的列表,表明了插入的行

「Update Rows Event」

有行更新时触发该事件。

  1. byte的tableId,表明唯一一张表
  2. 一个复杂的Bit集合,有更新的Row在更新之前的值在下面Row列表中的位置
  3. 一个复杂的Bit集合,有更新的Row在更新之后的值在下面Row列表中的位置
  4. 一个Row的列表,配合上面两个bitSet使用

「Delete Rows Event」

有行删除时触发该事件。


「GTID Event」

GTID 代表 Global Transaction IDentifier 它由两部分组成:

SID 表示源标识符,以及 GNO 代表组号。基本思想是 将标识符(全局事务标识符或 GTID)关联到每个事务。当事务被复制到从属设备、在从属设备上重新执行并写入从属设备的二进制日志时,GTID 会被保留。当从站连接到主站时,从站使用GTID而不是(文件,偏移量)。

其它还有很多中类型的事件详情参考 :https://dev.mysql.com/doc/dev/mysql-server/latest/namespacemysql_1_1binlog_1_1event.html


欢迎加入 DevXTalk 的技术交流群,在这里你可以和大佬们一起交流分享技术,行业经验,提升自我。结识新的业内朋友,扩大自己的圈子,当你想换工作的时候也许就能拿到不错的 offer 。总之期待你的加入...


「看完本文有收获?转发给更多的朋友吧」

「DevXTalk 不止于技术」

原文作者:DevXTalk;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-03-23

  • 博文量
    178
  • 访问量
    82575