• binlog 格式浅析

    背景

    作为运维或者DBA,MySQL的binlog可以算作是老朋友了,主从复制、备份、恢复等重要操作都是基于它进行的,所以我们对他的特性应该非常清楚,比如它的内容,今天我们就来浅析一下MySQL binlog文件里面的内容以及格式。本文章主要围绕MySQL 官方文档篇:

    https://dev.mysql.com/doc/internals/en/binary-log.html

    As we all know , MySQL binlog 是从MySQL 3.23.14引进的,用于记录mysql的数据更新或者潜在更新(比如DELETE语句执行删除而实际并没有符合条件的数据),binlog 是一个二进制文件集合,当然除了我们看到的 mysql-bin.xxxxxx这些binlog文件外,还有个binlog索引文件mysql-bin.index

    好了,言归正传,我们来看看一个binlog文件的具体内容及格式:

    binlog文件的开头 —— 魔数

    binlog文件以一个值为 0xfe 0x62 0x69 0x6e = 0xfe ‘b”i”n’ (详见 log_event.h). 的魔数开头,如下红色部分:

    这个魔数的主要起到一个标识一个binlog文件的作用。 当你用mysqlbinlog 去解析一个binlog文件时,它有一项就是检查这个魔数,如果没有检测到,或者检测错误,它就有可能会报如下错误:

    • the file is not a binary log (这不是一个binlog文件)
    • the binary log is horrifically corrupt (这个binlog文件可能有严重的损坏)

    比如我用binlog文件解析一个随便造的文件:

    binlog文件的内容 —— Event

    binlog 文件主要是由一个个的Event组成的,这里要区分 Event(事件) 和 Transaction(事务) 的区别。 前者是binlog文件的主要组成单位,接下来我们会讲它的类型和内容。 后者就是我们传统意义上说的具有ACID特性的事务。二者是两码事,不要混淆。

    这里注意了,binlog文件的格式也是分版本的,不同版本的binlog文件的内容是不同的,主要分为三个版本:

    • v1: Used in MySQL 3.23
    • v3: Used in MySQL 4.0.2 though 4.1
    • v4: Used in MySQL 5.0 and up

    我们这里主要以v4版本为主进行讲解。

    首先,一个binlog由一系列的binlog event构成,而每个binlog event包含header和data两部分。

    • header部分提供的是event的公共的类型信息,包括event的创建时间,服务器等等。
    • data部分提供的是针对该event的具体信息,如具体数据的修改。

    v4版本中,第一个event我们称之为 v4 format description event,这个event的格式如下

    我们从字面意思也可以看出,主要是标识一些binlog本身的基本信息,我们来结合具体例子看看:

    我先在数据库里面执行Flush logs 生成一个全新的binlog文件,然后来解读它:

    解析这个新的binlog文件:

    mysql日志是小端字节序(低字节存于内存低地址;高字节存于内存高地址)。

    前面4个字节是固定的magic number,值为0x6e6962fe。

    后面就是我们的v4 format description event,我们先看header部分。

    前四个字节:0x59ec85c2 是timestamp时间戳,转换过来是 2017/10/22 19:49:22,我和执行的时间大致一致。

    第五个字节:0x0f代表event type,这里对应的是15,我们从log_event.h中就可以看到所有的event类型:

    第六到九字节:0x0000007b代表server id 为123:

    第十到十三字节:0x00000077是整个event的长度为119字节。

    第十四到十七字节:0x0000007b是下一个event的起始位置123。

    最后的两个字节:0x0001是flag,1为LOG_EVENT_BINLOG_IN_USE_F,标识binlog还没有关闭,binlog关闭后,flag会被设置为0。

    到此为止的19个字节就和v4版本的第一个event的header部分对上号了。

    接下来是event data部分,我们来看看:

    开始的两个字节:0x0004 为binlog的版本号为 v4。

    接着的50个字节:是MySQL Server的版本号,与select version() 查看的结果一致。

    接下来的4个字节:0x00000000 是binlog的创建时间,这里是0。

    然后的1个字节:0x13是指之后所有event的公共头长度,这里都是19。

    剩下的字节代表mysql已知的event的各个长度信息等。


    在第一个v4 format description event 完了之后,后面跟着的是一系列类型的其他event:

    具体的类型和含义挺多的,可以参考对应的官方文档的介绍,这里挑几个比较常见的说一下:

    query event

    这是我们最常见的event之一,这个event发生在我们执行一个更新语句的时候。

    举个例子:

    如上,我刷新了binlog,生成了一个新的binlog文件:mysql-bin.000006,并且创建了一个表,插入了一条测试语句,我们来看看产生了哪些event:

    如上,会产生3个event,包括2个query event和1个xid event。其中2个query event分别是BEGIN以及INSERT 语句,而xid event则是事务提交语句(xid event是支持XA的存储引擎才有的,因为测试表test是innodb引擎的,所以会有。如果是myisam引擎的表,也会有BEGIN和COMMIT,只不过COMMIT会是一个query event而不是xid event)。

    我们对照mysqlbinlog命令的解析结果来看看吧:

    我们看到,每个event都以 at 加偏移量开头,以 /*!*/结尾。

    table_map event & write_rows event

    这两个event是在binlog_format = row 时才使用。我们可以从对应的官方文档中看到这个类型的Event具体的组成结构,很详细,这里懒得说了,就直接摘过来吧:

    TABLE_MAP_EVENT

    Used for row-based binary logging beginning with MySQL 5.1.5.

    Fixed data part:

    • 6 bytes. The table ID.
    • 2 bytes. Reserved for future use.

    Variable data part:

    • 1 byte. The length of the database name.
    • Variable-sized. The database name (null-terminated).
    • 1 byte. The length of the table name.
    • Variable-sized. The table name (null-terminated).
    • Packed integer. The number of columns in the table.
    • Variable-sized. An array of column types, one byte per column. To find the meanings of these values, look atenum_field_types in the mysql_com.h header file.
    • Packed integer. The length of the metadata block.
    • Variable-sized. The metadata block; see log_event.h for contents and format.
    • Variable-sized. Bit-field indicating whether each column can be NULL, one bit per column. For this field, the amount of storage required for N columns is INT((N+7)/8) bytes.

    WRITE_ROWS_EVENT

    Used for row-based binary logging beginning with MySQL 5.1.18.

    [TODO: following needs verification; it’s guesswork]

    Fixed data part:

    • 6 bytes. The table ID.
    • 2 bytes. Reserved for future use.

    Variable data part:

    • Packed integer. The number of columns in the table.
    • Variable-sized. Bit-field indicating whether each column is used, one bit per column. For this field, the amount of storage required for N columns is INT((N+7)/8) bytes.
    • Variable-sized (for UPDATE_ROWS_LOG_EVENT only). Bit-field indicating whether each column is used in theUPDATE_ROWS_LOG_EVENT after-image; one bit per column. For this field, the amount of storage required for N columns is INT((N+7)/8) bytes.
    • Variable-sized. A sequence of zero or more rows. The end is determined by the size of the event. Each row has the following format:
      • Variable-sized. Bit-field indicating whether each field in the row is NULL. Only columns that are “used” according to the second field in the variable data part are listed here. If the second field in the variable data part has N one-bits, the amount of storage required for this field is INT((N+7)/8) bytes.
      • Variable-sized. The row-image, containing values of all table fields. This only lists table fields that are used (according to the second field of the variable data part) and non-NULL (according to the previous field). In other words, the number of values listed here is equal to the number of zero bits in the previous field (not counting padding bits in the last byte).The format of each value is described in the log_event_print_value() function in log_event.cc.
      • (for UPDATE_ROWS_EVENT only) the previous two fields are repeated, representing a second table row.

    For each row, the following is done:

    • For WRITE_ROWS_LOG_EVENT, the row described by the row-image is inserted.
    • For DELETE_ROWS_LOG_EVENT, a row matching the given row-image is deleted.
    • For UPDATE_ROWS_LOG_EVENT, a row matching the first row-image is removed, and the row described by the second row-image is inserted.

    接下来我们来看一下吧:将binlog_format设置为row,并且创建一张测试表,和插入数据,和刚才一样。

    如上,这个语句产生了一个Query event,一个 Table_map类型的event,一个Write_map类型的event,一个Xid类型的Event。其对应的binlog解析内容如下:

    rotate event

    这个event出现在每个binlog文件的结尾处,用来标识接下来一个binlog文件的信息,我们再每个binlog文件的最后就可以看到:

    还有一些很重要的binlog event我们如果感兴趣可以阅读这部分官方文档,相信对我们日后的binlog处理很有帮助。

     

     

  • binlog 刷新与复制策略

    背景

    当我决定开始写这篇博文时,我的心里有很多问题。感觉自己貌似对MySQL的主从复制原理有了一些了解,但是又不是很清楚一些细节。所以在处理实际的一些故障时思维会受限。静下心来细细思考后,发现其实还是自己对一些细节掌握的不够明确导致的。虽然知道原因了,但一下子又不知从何下手,所以决定将暂时能想到的问题一一写下来,然后逐个击破。

    1. MySQL的主库什么时间将产生的 binlog 真正刷到文件中 ?
    2. sync_binlog 分别设置为 0、1、2的时候,数据库突然宕机会对主从复制产生什么影响,怎么避免或修复?
    3. MySQL 5.6 的新特性中的crash_safe 的存在意义是什么,有了双1配置了为什么还要有它 ?
    4. 从库IO线程如何知道从哪个位置读取主库的 binlog event 的,又是什么时间点从主库取binlog的 ?
    5. 从库的SQL线程如何记录执行到的 relay log 的位点 ?

    其实,上面的问题总结起来主要就是说一个事务在主库上开始执行写入到binlog_cache,然后commit,从binlog_cache中刷新到binlog文件,再刷新到磁盘。这个过程中,从库什么时候将这个事务从主库拿走?如何拿走?假如任何一个环节数据库宕机了,从库会产生什么情况?

    我觉得在开始分析解决上面的问题之前,首先得清楚一下基本概念,否则可能在理解上有偏差或困难:

    • binlog 与 redo log的区别,为什么有binlog做保障了还需要redo log?
    • sync_binlog 参数的概念,和代表的意义是什么?
    • MySQL主从复制基本原理,涉及到哪些线程?

    这些问题就不在这里详细赘述了。一些概念性的东西可能我们大家容易忘记,但是查一查就很快能回忆起来了。

    问题1 && 问题2

    之前的博客中已经提及如何从调试深入到MySQL源码去定位和分析问题,今天我们刚好就实践一下。

    当我们用gdb进入MySQL的调试以后,打断点到 MYSQL_BIN_LOG::ordered_commit函数处,执行得到函数对应的源码文件和位置,然后查看函数内容。

    1. 先将 binlog cache 写入到 binlog 文件中,但并没有执行fsync()操作,即只将文件内容写入到 OS 缓存中。
    2. 执行sync操作(具体与sync_binlog参数设置有关)。
    3. 最后commit提交事务。

    总结来说,commit 时,会判断是否将产生的 binlog flush 到文件中,当sync_binlog = 1 时,即执行 sync操作,每提交一个事务,会 fsync 一次binlog file。 当 sync_binlog != 1 的时候,每次事务提交的时候,不一定会执行 fsync 操作,binlog 的内容只是缓存在了 OS(是否会执行fsync操作,取决于OS缓存的大小),此时备库可以读到主库产生的 binlog

    有人不禁会问,那假如这时候主库突然宕机了怎么办?

    如果此时突然宕机,主库上的存在于binlog缓存中的内容全部都没了,所以主库相当于没执行。而从库就不一定了,因为这时候从库已经有可能将当时binlog缓存中的内容给读走并执行了。

    在这种情况下,当主库机器挂掉时,可能会有以下两种意外情况:

    1. 主备同步无延迟,此时主库机器恢复后,备库接着之前的位点重新拉binlog, 但是主库由于没有fsync最后的binlog,所以会返回1236 的错误:
      MySQL error code 1236 (ER_MASTER_FATAL_ERROR_READING_BINLOG): Got fatal error %d from master when reading data from binary log: '%-.256s'
    2. 备库没有读到主库失去的binlog,此时备库无法同步主库最后的更新,备库不可用(备库无法作为新的主库顶上去,因为有数据丢失)。

    那么怎么解决和避免这种问题?  就是设置双1参数中的sync_binlog为1。 使得事务一旦提交以后,不仅会将binlog缓存中的内容刷新到文件,而且还会将binlog文件中的内容直接sync到磁盘上。

    到此我想问题1 和问题2 中的答案大家应该都心中有数了。

    问题3 && 问题4 && 问题5

    MySQL  crash_safe 的定义是指当master/slave任何一个节点发生宕机等意外情况下,服务器重启后master/slave的数据依然能够保证一致性。

    而crash_safe 包括两个方面,一个是crash_safe master 和 crash_safe slave。 通俗的说就是主库宕机重启后不会导致数据丢失,而从库宕机重启后不会导致复制错误。 (个人理解

    crash-safe master相对比较简单,只要使用事务的存储引擎,并且正确的配置就能达到crash safe的效果。对于最为常见的InnoDB存储引擎而言,只需要设置双1 参数就可以。

    crash_safe slave 的情况就有些复杂了。 在了解crash-safe slave 之前,我们先分析一下MySQL 5.6 之前的版本出现 crash-unsafe 的原因。

    导致不能实现crash-safe slave有两方面的原因,即replication中的SQL thread和IO thread。首先来看SQL thread,其主要完成两个操作:

    • 运行relay log中对应的事务信息
    • 更新relay-info.log文件

    更新relay-info.log文件是为了记录已经执行relay log中的位置,当slave重启后可以根据这个位置继续同步relay log。MySQL数据库默认对于文件relay-info.log是写入到操作系统缓存,因此即使写入了relay-info文件,但是如果没有刷新回磁盘,在发生宕机时就可能导致大量的已更新位置的丢失,从而导致重复执行SQL语句,最终的现象就是slave不断的报1062错误,或者发现主从数据不一致(特别是表没有主键的情况)。MySQL 从5.5  版本开始新增加了一个参数sync_relay_log_info,可以控制每次事务更新relay-info.log后就进行一次fdatasync操作,但这加重了系统负担。

    但是即使将这个参数设为1也并不能解决所有问题,因为这两个操作不是在一个事务中,一个是数据库操作,一个是文件操作,因此不能达到原子的效果。 所以如果,执行完第一步,刚好数据库宕机了,那么当数据库重启以后有可能重复执行已经执行过的。

    MySQL 5.6采用了另一种方法,就是将relay-info.log的信息保存在InnoDB的事务表中,这时两个操作都是数据库操作,在一个事务中就能得到原子性。例如对于slave的日志回放,其过程为:

    接下来我们再看IO线程:

    IO thread和SQL thread 一样,他的主要工作也是两步:

    • 将收到的二进制日志写入到relay log,每个二进制日志由多个log event组成。
    • 每接受到一个log event就需要更新master-info.log。

    master-info.log 也是写入操作系统缓存,参数sync_master_info可以控制fdatasync的时间从而避免缓存写的问题。而从IO thread的工作原理来看,它没有办法 将写入master info和拉取binlog放到同一个事务中而保持原子操作,因此其是对数据一致性会产生影响,设想一个log event传送到了relay log中两次的情形。

    不过好在从MySQL 5.5版本开始提供了参数relay_log_recovery,当发生crash导致重连master时,其不根据master-info.log的信息进行重连,而是根据relay-info中执行到master的位置信息重新开始拉master上的日志数据(不过需要确保日志依然存在于master上,否则就。。。)

    那么我们该如何使用crash_safe呢?

    • 停止slave的mysql实例

    • my.cnf文件中添加     master-info-repository=TABLE     relay-log-info-repository=TABLE     relay-log-recovery

    • 重启slave的mysql实例

    如果是MySQL 5.6.5 或者更早期。slave_master_info 和 slave_relay_log_info 表默认使用MyISAM 引擎。所以还得修改成innodb,如下:

    ALTER TABLE mysql.slave_master_info ENGINE=InnoDB;

    ALTER TABLE mysql.slave_relay_log_info ENGINE=InnoDB

     

    由此,我们知道了MySQL 通过 sync_binlogsync_master_infosync_relay_log_infosync_relay_log 来记录相关的位点信息,这些都是保证数据库crash_safe的重要参数选项。

     

    参考:

    http://www.innomysql.com/article/34.html

    http://blog.itpub.net/22664653/viewspace-1752588/