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

Binlog详解及在TDSQL数据订阅中的实践

TDSQL数据库 2021-09-26
11178

一、Binlog是什么?

MySQLserver包含四种类型的日志,分别是:ERROR LOG 错误日志、GENERAL QUERY LOG 查询日志、Slow Query Log 慢查询日志、Binary Log 二进制日志。

错误日志记录了MySQL在运行过程中发生错误。查询日志记录了来自客户端的所有的SQL 语句,它完整的记录下了所有客户端发给server的所有操作,但是由于开启查询日志对MySQL的性能有所影响,因此生产环境下使用的情况较少。慢查询日志会记录下执行较慢的SQL,便于开发者进行性能调优。

二进制日志Binlog(Binary Log)以事件的形式记录下了MySQL所有的数据的更新以及潜在的更新。这里潜在的更新是指意图对数据进行更改但实际上没有更改的操作,如delete一条不存在的数据,这种情况也会在Binlog中记录下来(row格式除外)。


二、为什么需要Binlog,它有什么作用?

Binlog在MySQL中主要有以下几种用途:一、用来进行主备复制,MySQL Replication在Master端开启Binlog,Master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。二、数据备份与恢复,如基于时间点的恢复。三、MySQL自身重启时的数据恢复。在系统恢复的时候,会将redo log中没有提交但是已经记录的Binlog的事务进行重新提交。四、其他用途,如根据Binlog进行数据订阅等。


三、MySQL是如何记录Binlog的。

MySQL是如何记录Binlog的呢?要搞清楚这个问题,首先需要讲解下事务的提交过程。

当客户端发起提交commit命令时,事务首先进行prepare操作,该操作主要是对redo log进行fsync,过程中为了保证Binlog中的事务顺序与innodb事务的提交顺序的一致性,会使用prepare_commit_mutex锁。

在完成了redo log的fsync操作之后,会进行Binlog的fsync操作。结束之后,server会向innodb提交该事务,并释放prepare_commit_mutex锁。

在这里我们就可以看到,server是在事务写入commit log之前,写入Binlog的。

在这里需要明确,虽然看似都是对MySQL所做的事情的记录,但是redo/undo log与MySQL Binlog是完全不同的日志,其区别主要体现在一下几个方面:

a)所产生的层次不同。redo/undolog是属于innodb层的日志,而Binlog是MySQL server层的日志,Binlog跟采用何种引擎没有关系,记录的是所有引擎的更新操作的记录。

b)所记录的内容不同。redo/undo日志记录的是每个页的修改情况,属于物理日志+逻辑日志结合的方式(redo log记录的是页数据的变化,undo log采用的是逻辑日志),目的是保证事务的一致性。Binlog记录的都是事务操作内容,比如一条语句DELETE FROM TABLE WHERE id > 1之类的,不管采用的是什么引擎。

c)所记录的时间点不同。Binlog只在事务提交前写入,而redo日志是在事务进行过程中不断写入的,且并不是以事务提交顺序写入的。

其中*T1,*T2,*T3表示事务提交时的日志。

Binlog刷新到磁盘的时机跟sync_binlog参数相关,如果设置为0,则表示MySQL不控制Binlog的刷新,由文件系统去控制它缓存的刷新,而如果设置为不为0的值则表示每sync_binlog次事务,MySQL调用文件系统的刷新操作刷新Binlog到磁盘中。设为1是最安全的,在系统故障时最多丢失一个事务的更新。尽管当该值设置为1,意味着每次事务提交都要进行刷盘操作,但是由于在mysql5.6版本后,普遍采用了group commit技术来进行日志的提交,因此即使是sync_binlog的值为1,也不会对性能产生较大的影响。


四、Binlog的格式及事件结构详解

Binlog的格式主要分为三种,分别是Statement,Row以及Mixed。

Statement格式记录的方式是每条SQL语句作为一个事件。

Row格式的记录了表中每一行的变化情况,因此相较于statement格式的Binlog,row格式的Binlog的量会更大。另外需要注意的是,当Binlog格式为Row格式时,DDl的记录方式是把整条语句记录下来,例如drop table操作只会记录该条语句,而不是每条记录的变化。

Mixed格式的Binlog是前两种格式的结合,根据SQL的类型来确定采用row还是statement格式。

Binlog格式的指定通过变量Binlog_format指定。Binlog是两种类型文件的集合,分别是以.index为后缀的索引文件及以.Binlog为后缀的二进制文件。其中索引文件就是当前该实例的Binlog文件列表。

Binlog由Binlog event组成,其中每个Binlog event由header和data两部分构成。


4.1 FROMAT_DESC_EVENT、PREVIOUS_GTIDS_LOG_EVENT、ROTATE_EVENT


每一个Binlog文件的第一个event为format_desc事件,它的作用是用来描述Binlog文件的相关版本信息和格式。最后一个event的类型为rotate事件,用它来表征下一个Binlog的相关信息。下面我们首先来分析下这两个事件。

第一步,我们先执行flush logs操作,目的是关闭当前的Binlog,并创建新的Binlog文件。

从上面可以看出,在执行了flush logs之后,产生了新的Binlog文件,并且已经有了一部分内容,通过mysqlbinlog工具我们可以看下这部分内容。

从上面的图中我们可以看到两个Binlog事件,分别是第5、6行标识的事件和第13、14行标识的事件。

第5行表示该事件是从Binlog文件的第4个字节开始的,那么到哪里结束呢?第6行,end_log_pos标识的123,为该事件的结束位置。那么这里有一个问题,为什么第一个Binlog的第一个事件不是在第一个字节开始,而是在第4个字节开始呢?这是因为在MySQL中,每一个Binlog都是以一个4字节的魔数0x6e6962fe开头,同时从下图我们可以看到,Binlog是以小端序进行存储的。

从第4个字节到第123个字节,为该Binlog中的第一个事件,并且通过前面的介绍我们知道该事件的类型为format_desc_event,下面我们对照官方文档来介绍下format_desc_event的具体格式。

首先,MySQL v4版本的Binlog事件结构如下图所示,主要由event header和event data两部分组成。其中fixed part是event的固定长度和格式的数据。Variable part则是变长的数据部分。

不同事件的event data部分不同,下面是format_desc_event的具体格式。

我们首先分析下前19个字节,根据上图我们知道,前4个字节为时间戳,第5个字节为0x0f是event type,10进制数为15,通过MySQL的源代码我们可以明确该数字所代表的事件类型,Binlog_event.h文件中定义了枚举变量Log_event_type,其中15为format_desc 事件,如下图所示

接着4个字节0xa6ca7529是server_id,接下来4字节为0x00000077,标识该事件长度,值为119,恰好结束位置123=119+4。接下来4个字节为下一个事件的开始位置0x0000007b,其值为123。接着的2个字节的0x0001是flag(1为LOG_EVENT_BINLOG_IN_USE_F,标识Binlog还没有关闭,Binlog关闭后,flag会被设置为0)。以上就是前19个字节的事件头。

接下来是event data部分,其中从第19个字节开始往后2个字节为0x0004,表示Binlog版本,当前版本为V4。之后50个字节为当前的server的版本号,与select version()得到的值相同。接下来四个字节是Binlog创建时间为0;之后 1个字节0x13表示接下来所有event的公共头长度,值为19;后续43个字节为每种事件类型的fixed data部分的长度。我们可以发现,formart_desc_event没有variable data和extra_header部分。

该Binlog文件中的第二个事件为PREVIOUS_GTIDS_LOG_EVENT(第13、14行标识的事件)。开启GTID模式后,每个Binlog开头都会有一个PREVIOUS_GTIDS_LOG_EVENT事件,它的值是上一个Binlog的PREVIOUS_GTIDS_LOG_EVENT+GTID_LOG_EVENT,实际上,在数据库重启的时候,需要重新填充gtid_executed的值,该值即是最新一个Binlog PREVIOUS_GTIDS_LOG_EVENT加上 GTID_LOG_EVENT。

下面我们来分析下该事件的结构,从下图我们可以看到,该事件是从7b位置开始,前19个字节为事件头,在这里不做展开,我们可以注意到,前四个字节0x5933d2a2,为该事件的时间戳,与上一个事件的时间戳相同,也就是说,该事件是在创建这个新的Binlog时,与format_desc_event事件一同创建的。

以下是PREVIOUS_GTIDS_LOG_EVENT event data 部分的结构

字节数

含义

8

GTID中sid-number的组数

16

第一组sid-number的sid部分

8

第一组sid-number中, internal numbers的个数

8

第一组sid-number中, 第一个internal  number的起始number

8

第一组sid-number中, 第一个internal  number的结束number+1

8

第一组sid-number中, 第二个internal  number的起始number

8

第一组sid-number中, 第二个internal  number的结束number+1

16

第二组sid-number的sid部分

由此,我们可以看到前八个字节0x0000000000000001,表示只有一个sid。从MySQLBinlog的解析的结果中,我们也确实可以看到只有一组#9895cb54-4031-11e7-9328-6c0b84d5a88f:1-452159802。下面的十六个字节为sid,它的值为9895cb54-4031-11e7-9328-6c0b84d5a88f。之后的8个字节为internal numbers的个数,就是表示相同的sid下有几组GTID。从解析结果看只有一组,因此其二进制的值为0x0000000000000001。

之后的两个8字节分别是0x0000000000000001和0x00000000a1f3693b,分别代表其实GTID和终止的GTID。

接下来,我们直接flush logs,可以看到会添加一个新的rotate event。从下面我们可以看到,16行17行会产生一个新的rotateevent 事件。


此时Binlog的内容如下:

此时,我们可以分析得到,format_desc_event的flag部分的值从从0x0001变成了0x0000,表名该Binlog已经关闭,不会再有事件写入。从0xbe位置开始为rotate事件,首先前8个字节,0x00000004标识下个Binlog中第一个事件的位置的偏移量为4,其实所有的rotate事件的这个部分的值都是4。剩下的16个字节就是下一个Binlog文件的名称Binlog.003504。以上介绍的就是在没有任何请求的情况下的Binlog的组成,分别是format_desc_event、PREVIOUS_GTIDS_LOG_EVENT、rotate_event。


4.2 QUERY_EVENT,GTID_EVENT,XID_EVENT


设置当前的Binlog格式为statement,之后执行flush logs写入新的Binlog文件。之后执行下列操作。

在执行完上述操作之后,会产生三种类型的事件,分别是QUERY_EVENT,GTID_LOG_EVENT,XID_EVENT。其中XID_EVENT是事务提交语句(xid event是支持XA的存储引擎才有的,因为测试表test1是innodb引擎的,所以会有。如果是myisam引擎的表,也会有BEGIN和COMMIT,只不过COMMIT会是一个QUERY_EVENT而不是XID_EVENT)。GTID_LOG_EVENT表示随后的事务的GTID。所执行的SQL语句用query_event来表示。

我们首先来分析下query_event,其Binlog及mysqlbinlog解析后的结果如下所示

头部跟之前的事件类似,这里不再重复,需要注意的是type为0x02,长度是0x00000072,下一个事件的位置是0x0000028b。

Fixeddata:首先的4个字节0x00006424为执行该语句的thread id 25636,接下来的4个字节是执行的时间0(以秒为单位),接下来的1个字节0x04是语句执行时的默认数据库名字的长度,我这里数据库是summertest,所以长度为10(0x0a).接着的2个字节0x0000是错误码(slave db在复制时会执行后检查错误码是否一致,如果不一致,则复制过程会中止),接着2个字节0x0027为状态变量块的长度39。

Variabledata:从0x0027之后的39个字节为状态变量块,然后是默认数据库名summertest,以0x00结尾,然后是SQL语句insertinto test1 values (1,'a'),接下来就是第2个query event的内容了。

之后我们来看下xid事件。xid event为COMMIT语句。前19个字节是通用头部,type是16。data部分中Fixed data为空,而variable data为8个字节,这8个字节0x00380521e2是事务编号(注意事务编号不一定是小端字节序,因为该值是直接从内存中拷贝到磁盘的,所以跟机器的字节序相同)。下面是该事件的Binlog及解析结果。


4.3 TABLE_MAP_EVENT & WRITE_ROWS_EVENT


当Binlog的格式为row格式的时候,会出现这两种事件类型。同样的,我们开启一个新的Binlog的同时,设置Binlog的格式为row格式,之后创建一个测试表,并做一些增删改查的操作。

之后,Binlog文件中会产生四种新的Binlog事件,分别是,table_map_event ,write_rows_event ,update_rows_event , delete_rows_event。

Table_map_event,与其余三种类型的事件一同出现,其作用是表示发生数据更新的行所在的表的相关信息。下面我们对table_rows_event以及write_rows_event进行分析。

table_rows_event和write_rows_event的Binlog及解析结果如下所示

0x13593609,开始为table_map_event。除去头部19个字节,Fixed data为8个字节,前面6个字节0xe8=232为tableid,接着2个字节0x0001为flags。

Variabledata部分,首先1个字节0x0a为数据库名summertest的长度,然后5个字节是数据库名summertest+结束符。接着1个字节0x04为表名长度,接着5个字节为表名test2+结束符。接着1个字节0x02为列的数目。而后是2个列的类型定义,分别是0x03和0x0f(列的类型MYSQL_TYPE_LONG为0x03,MYSQL_TYPE_VARCHAR为0x0f)。接着是列的元数据定义,首先0x02表示元数据长度为2,因为MYSQL_TYPE_LONG没有元数据,而MYSQL_TYPE_VARCHAR元数据长度为2。接着的0x000a就是MYSQL_TYPE_VARCHAR的元数据,最后一个字节0x02为掩码,表示第一个字段不能为NULL。

write_rows_event从0x59360955开始,头部19个字节,之后的6个字节0xe8为tableid,然后两个字节0x0001为flags。接着的1个字节0x02为表中列的数目。然后1个字节0x00各个bit标识各列是否存在值,这里表示都存在。其中从0x02be开始的四个字节为第一列的值,0x00000002,之后为第二列的值`b`。rows相关的event还有update_rows_event和delete_rows_event等,在这里不再展开分析。


五、Binlog在TDSQL数据订阅中的应用。

为了满足不同实例之间数据同步及数据订阅服务的需求,TDSQL数据订阅服务通过对row格式的Binlog的解析,将Binlog事件封装成消息存储至分布式消息队列Kafka集群中,供第三方的消费者进行消费。其架构如下所示:

       首先通过Binlogproductor生产者从某个SET的第一备机(指与主机的时延最小的机器)中分析row格式的Binlog并存入Kafka集群,之后消费者进程会根据ZOOKEEPE的任务节点的配置,从Kafka上对消息进行消费并apply到目标SET。

       TDSQL数据订阅服务的消息生产环节,要求消息不丢,不重复,不乱序,同时在消息中包含表结构信息。同时用于数据同步的消费者在设计中,支持Binlog消息的重放且不会出错。具体的设计细节会在下一篇文章中给出。


文章转载自TDSQL数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论