初探 MySQL 的 Binlog

2015-08-11 10:10:44 +08:00
 XadillaX

原文地址: https://xcoder.in/2015/08/10/mysql-binlog-try/


  花瓣网的搜索架构需要重构,尤其是在索引建立或者更新层面。

  目前的一个架构导致的结果就是时间越久,数据本体与搜索引擎索引中的数据越不同步,相差甚大。

  新的一个架构打算从 MySQL 的 Binlog 中读取数据更新、删除、新增等历史记录,并把相应信息提取出来丢到队列中慢慢去同步。

  所以我就在这里小小去了解一下 Binlog。

准备工作

什么是 Binlog

  MySQL Server 有四种类型的日志——Error Log、General Query Log、Binary Log 和 Slow Query Log。

  第一个是错误日志,记录 mysqld 的一些错误。第二个是一般查询日志,记录 mysqld 正在做的事情,比如客户端的连接和断开、来自客户端每条 Sql Statement 记录信息;如果你想准确知道客户端到底传了什么瞎 [哔哔] 玩意儿给服务端,这个日志就非常管用了,不过它非常影响性能。第四个是慢查询日志,记录一些查询比较慢的 SQL 语句——这种日志非常常用,主要是给开发者调优用的。

  剩下的第三种就是 Binlog 了,包含了一些事件,这些事件描述了数据库的改动,如建表、数据改动等,也包括一些潜在改动,比如 DELETE FROM ran WHERE bing = luan,然而一条数据都没被删掉的这种情况。除非使用 Row-based logging,否则会包含所有改动数据的 SQL Statement。

  那么 Binlog 就有了两个重要的用途——复制和恢复。比如主从表的复制,和备份恢复什么的。

启用 Binlog

  通常情况 MySQL 是默认关闭 Binlog 的,所以你得配置一下以启用它。

  启用的过程就是修改配置文件 my.cnf 了。

  至于 my.cnf 位置请自行寻找。例如通过 OSX 的 brew 安装的 mysql 默认配置目录通常在

/usr/local/Cellar/mysql/$VERSION/support-files/my-default.cnf

  这个时候需要将它拷贝到 /etc/my.cnf 下面。

详见 <StackOverflow - MySQL 'my.cnf' location?>。

  紧接着配置 log-binlog-bin-index 的值,如果没有则自行加上去。

log-bin=master-bin
log-bin-index=master-bin.index

  这里的 log-bin 是指以后生成各 Binlog 文件的前缀,比如上述使用 master-bin,那么文件就将会是 master-bin.000001master-bin.000002 等。而这里的 log-bin-index 则指 binlog index 文件的名称,这里我们设置为 master-bin.index

  如果上述工作做完之后重启 MySQL 服务,你可以进入你的 MySQL CLI 验证一下是否真的启用了。

$ mysql -u $USERNAME ...

  然后在终端里面输入下面一句 SQL 语句:

SHOW VARIABLES LIKE '%log_bin%';

  如果结果里面出来这样类似的话就表示成功了:

+---------------------------------+---------------------------------------+
| Variable_name                   | Value                                 |
+---------------------------------+---------------------------------------+
| log_bin                         | ON                                    |
| log_bin_basename                | /usr/local/var/mysql/master-bin       |
| log_bin_index                   | /usr/local/var/mysql/master-bin.index |
| log_bin_trust_function_creators | OFF                                   |
| log_bin_use_v1_row_events       | OFF                                   |
| sql_log_bin                     | ON                                    |
+---------------------------------+---------------------------------------+
6 rows in set (0.00 sec)

  更多的一些相关配置可以参考这篇《MySQL 的 binary log 初探》。

随便玩玩

  然后你就可以随便去执行一些数据变动的 SQL 语句了。当你执行了一堆语句之后就可以看到你的 Binlog 里面有内容了。

  如上表所示,log_bin_basename 的值是 /usr/local/var/mysql/master-bin 就是 Binlog 的基础文件名了。

  那我们进去看,比如我的这边就有这么几个文件:

  很容易发现,里面有 master-bin.indexmaster-bin.000001 两个文件,这两个文件在上文中有提到过了。

  我们打开那个 master-bin.index 文件,会发现这个索引文件就是一个普通的文本文件,然后列举了各 binlog 的文件名。而 master-bin.000001 文件就是一堆乱码了——毕竟人家是二进制文件。

结构解析

索引文件

  索引文件就是上文中的 master-bin.index 文件,是一个普通的文本文件,以换行为间隔,一行一个文件名。比如它可能是:

master-bin.000001
master-bin.000002
master-bin.000003

  然后对应的每行文件就是一个 Binlog 实体文件了。

Binlog 文件

  Binlog 的文件结构大致由如下几个方面组成。

文件头

  文件头由一个四字节 Magic Number,其值为 1852400382,在内存中就是 "\xfe\x62\x69\x6e",参考 MySQL 源码的 [log_event.h](://github.com/mysql/mysql-server/blob/a2757a60a7527407d08115e44e889a25f22c96c6/sql/log_event.h#L187),也就是 '\0xfe' 'b' 'i' 'n'

  与平常二进制一样,通常都有一个 Magic Number 进行文件识别,如果 Magic Number 不吻合上述的值那么这个文件就不是一个正常的 Binlog。

事件

  在文件头之后,跟随的是一个一个事件依次排列。每个事件都由一个事件头和事件体组成。

  事件头里面的内容包含了这个事件的类型(如新增、删除等)、事件执行时间以及是哪个服务器执行的事件等信息。

  第一个事件是一个事件描述符,描述了这个 Binlog 文件格式的版本。接下去的一堆事件将会按照第一个事件描述符所描述的结构版本进行解读。最后一个事件是一个衔接事件,指定了下一个 Binlog 文件名——有点类似于链表里面的 next 指针。

  根据《[High-Level Binary Log Structure and Contents](High-Level Binary Log Structure and Contents)》所述,不同版本的 Binlog 格式不一定一样,所以也没有一个定性。在我写这篇文章的时候,目前有三种版本的格式。

  实际上还有一个 v2 版本,不过只在早期 4.0.x 的 MySQL 版本中使用过,但是 v2 已经过于陈旧并且不再被 MySQL 官方支持了。

通常我们现在用的 MySQL 都是在 5.0 以上的了,所以就略过 v1 ~ v3 版本的 Binlog,如果需要了解 v1 ~ v3 版本的 Binlog 可以自行前往上述的《High-level...》文章查看。

事件头

  一个事件头有 19 字节,依次排列为四字节的时间戳、一字节的当前事件类型、四字节的服务端 ID、四字节的当前事件长度描述、四字节的下个事件位置(方便跳转)以及两字节的标识。

  用 ASCII Diagram 表示如下:

+---------+---------+---------+------------+-------------+-------+
|timestamp|type code|server_id|event_length|next_position|flags  |
|4 bytes  |1 byte   |4 bytes  |4 bytes     |4 bytes      |2 bytes|
+---------+---------+---------+------------+-------------+-------+

  也可以字节编造一个结构体来解读这个头:

struct BinlogEventHeader
{
    int   timestamp;
    char  type_code;
    int   server_id;
    int   event_length;
    int   next_position;
    char  flags[2];
};

如果你要直接用这个结构体来读取数据的话,需要加点手脚。

因为默认情况下 GCC 或者 G++ 编译器会对结构体进行字节对齐,这样读进来的数据就不对了,因为 Binlog 并不是对齐的。为了统一我们需要取消这个结构体的字节对齐,一个方法是使用 #pragma pack(n),一个方法是使用 __attribute__((__packed__)),还有一种情况是在编译器编译的时候强制把所有的结构体对其取消,即在编译的时候使用 fpack-struct 参数,如:

$ g++ temp.cpp -o a -fpack-struct=1

  根据上述的结构我们可以明确得到各变量在结构体里面的偏移量,所以在 MySQL 源码里面(libbinlogevents/include/binlog_event.h)有下面几个常量以快速标记偏移:

#define EVENT_TYPE_OFFSET    4
#define SERVER_ID_OFFSET     5
#define EVENT_LEN_OFFSET     9
#define LOG_POS_OFFSET       13
#define FLAGS_OFFSET         17

  而具体有哪些事件则在 libbinlogevents/include/binlog_event.h#L245 里面被定义。如有个 FORMAT_DESCRIPTION_EVENT 事件的 type_code 是 15、UPDATE_ROWS_EVENTtype_code 是 31。

  还有那个 next_position,在 v4 版本中代表从 Binlog 一开始到下一个事件开始的偏移量,比如到第一个事件的 next_position 就是 4,因为文件头有一个字节的长度。然后接下去对于事件 n 和事件 n + 1 来说,他们有这样的关系:

next_position(n + 1) = next_position(n) + event_length(n)

  关于 flags 暂时不需要了解太多,如果真的想了解的话可以看看 MySQL 的相关官方文档

事件体

  事实上在 Binlog 事件中应该是有三个部分组成,headerpost-headerpayload,不过通常情况下我们把 post-headerpayload 都归结为事件体,实际上这个 post-header 里面放的是一些定长的数据,只不过有时候我们不需要特别地关心。想要深入了解可以去查看 MySQL 的官方文档。

  所以实际上一个真正的事件体由两部分组成,用 ASCII Diagram 表示就像这样:

+=====================================+
| event  | fixed part (post-header)   |
| data   +----------------------------+
|        | variable part (payload)    |
+=====================================+

  而这个 post-header 对于不同类型的事件来说长度是不一样的,同种类型来说是一样的,而这个长度的预先规定将会在一个“格式描述事件”中定好。

格式描述事件

  在上文我们有提到过,在 Magic Number 之后跟着的是一个格式描述事件(Format Description Event),其实这只是在 v4 版本中的称呼,在以前的版本里面叫起始事件(Start Event)。

  在 v4 版本中这个事件的结构如下面的 ASCII Diagram 所示。

+=====================================+
| event  | timestamp         0 : 4    |
| header +----------------------------+
|        | type_code         4 : 1    | = FORMAT_DESCRIPTION_EVENT = 15
|        +----------------------------+
|        | server_id         5 : 4    |
|        +----------------------------+
|        | event_length      9 : 4    | >= 91
|        +----------------------------+
|        | next_position    13 : 4    |
|        +----------------------------+
|        | flags            17 : 2    |
+=====================================+
| event  | binlog_version   19 : 2    | = 4
| data   +----------------------------+
|        | server_version   21 : 50   |
|        +----------------------------+
|        | create_timestamp 71 : 4    |
|        +----------------------------+
|        | header_length    75 : 1    |
|        +----------------------------+
|        | post-header      76 : n    | = array of n bytes, one byte per event
|        | lengths for all            |   type that the server knows about
|        | event types                |
+=====================================+

  这个事件的 type_code 是 15,然后 event_length 是大于等于 91 的值的,这个主要取决于所有事件类型数。

  因为从第 76 字节开始后面的二进制就代表一个字节类型的数组了,一个字节代表一个事件类型的 post-header 长度,即每个事件类型固定数据的长度。

  那么按照上述的一些线索来看,我们能非常快地写出一个简单的解读 Binlog 格式描述事件的代码。

如上文所述,如果需要正常解读 Binlog 文件的话,下面的代码编译时候需要加上 -fpack-struct=1 这个参数。

#include <cstdio>
#include <cstdlib>

struct BinlogEventHeader
{
    int  timestamp;
    unsigned char type_code;
    int  server_id;
    int  event_length;
    int  next_position;
    short flags;
};

int main()
{
    FILE* fp = fopen("/usr/local/var/mysql/master-bin.000001", "rb");
    int magic_number;
    fread(&magic_number, 4, 1, fp);

    printf("%d - %s\n", magic_number, (char*)(&magic_number));

    struct BinlogEventHeader format_description_event_header;
    fread(&format_description_event_header, 19, 1, fp);

    printf("BinlogEventHeader\n{\n");
    printf("    timestamp: %d\n", format_description_event_header.timestamp);
    printf("    type_code: %d\n", format_description_event_header.type_code);
    printf("    server_id: %d\n", format_description_event_header.server_id);
    printf("    event_length: %d\n", format_description_event_header.event_length);
    printf("    next_position: %d\n", format_description_event_header.next_position);
    printf("    flags[]: %d\n}\n", format_description_event_header.flags);

    short binlog_version;
    fread(&binlog_version, 2, 1, fp);
    printf("binlog_version: %d\n", binlog_version);

    char server_version[51];
    fread(server_version, 50, 1, fp);
    server_version[50] = '\0';
    printf("server_version: %s\n", server_version);

    int create_timestamp;
    fread(&create_timestamp, 4, 1, fp);
    printf("create_timestamp: %d\n", create_timestamp);

    char header_length;
    fread(&header_length, 1, 1, fp);
    printf("header_length: %d\n", header_length);

    int type_count = format_description_event_header.event_length - 76;
    unsigned char post_header_length[type_count];
    fread(post_header_length, 1, type_count, fp);
    for(int i = 0; i < type_count; i++) 
    {
        printf("  - type %d: %d\n", i + 1, post_header_length[i]);
    }

    return 0;
}

  这个时候你得到的结果有可能就是这样的了:

1852400382 - �binpz�
BinlogEventHeader
{
    timestamp: 1439186734
    type_code: 15
    server_id: 1
    event_length: 116
    next_position: 120
    flags[]: 1
}
binlog_version: 4
server_version: 5.6.24-log
create_timestamp: 1439186734
header_length: 19
  - type 1: 56
  - type 2: 13
  - type 3: 0
  - type 4: 8
  - type 5: 0
  - type 6: 18
  - ...

  一共会输出 40 种类型(从 1 到 40),如官方文档所说,这个数组从 START_EVENT_V3 事件开始(type_code 是 1)。

跳转事件

  跳转事件即 ROTATE_EVENT,其 type_code 是 4,其 post-header 长度为 8。

  当一个 Binlog 文件大小已经差不多要分割了,它就会在末尾被写入一个 ROTATE_EVENT——用于指出这个 Binlog 的下一个文件。

  它的 post-header 是 8 字节的一个东西,内容通常就是一个整数 4,用于表示下一个 Binlog 文件中的第一个事件起始偏移量。我们从上文就能得出在一般情况下这个数字只可能是四,就偏移了一个魔法数字。当然我们讲的是在 v4 这个 Binlog 版本下的情况。

  然后在 payload 位置是一个字符串,即下一个 Binlog 文件的文件名。

各种不同的事件体

  由于篇幅原因这里就不详细举例其它普通的不同事件体了,具体的详解在 MySQL 文档中一样有介绍,用到什么类型的事件体就可以自己去查询。

小结

  本文大概介绍了 Binlog 的一些情况,以及 Binlog 的内部二进制解析结构。方便大家造轮子用——不然老用别人的轮子,只知其然而不知其所以然多没劲。

  好了要下班了,就写到这里过吧。

参考

  1. MySQL's binary log 结构简介,目测原文在 TaobaoDBA(已无法访问)
  2. MySQL Binlog 的介绍
  3. MySQL 的 binary log 初探
  4. High-Level Binary Log Structure and Contents and related official documents
  5. #pragma pack vs -fpack-struct for Intel C
5215 次点击
所在节点    MySQL
48 条回复
choury
2015-08-11 12:24:04 +08:00
为什么要解析binlog文件呢,都打算自己造轮子了,干脆直接按照主从复制协议连到master上面,假装自己是slave,然后等着mysql把事件推送过来就好了,基本就没有延时了
XadillaX
2015-08-11 12:47:21 +08:00
@Andiry -. - 对于第一点我只是想知其然并且知其所以然,学习下人家的设计。第二点,我们的数据库是主从库,Binlog 在从库开启。第三点,是因为我们当初的 proxy 设计不好,并且没有通过 mysql,而是直接在需要更新的逻辑代码里面 RPC 到 proxy。
iannil
2015-08-11 12:47:26 +08:00
纯来支持的。
XadillaX
2015-08-11 12:48:16 +08:00
@choury 那你的假装 slave 挂掉怎么办?这期间的数据就丢失了。Binlog 是半持久化的,你读到某个点了然后挂掉,重启了继续读就好。
choury
2015-08-11 12:51:51 +08:00
@XadillaX 你可以仔细看下协议,slave可以指定事件位置
babyname
2015-08-11 12:54:31 +08:00
这个架构有问题 利用binlog进行索引只是取巧的方式。
XadillaX
2015-08-11 12:57:28 +08:00
@babyname 然而我只是个臭写代码的,架构什么的并不是我来左右的 -。 -
Andiry
2015-08-11 13:01:54 +08:00
@XadillaX 真是奇怪的设计。。Binary log在从库上,也就是说从库接收到了所有的MySQL命令而主库不是,所以又反馈回主库?那MySQL命令又是怎么到从库的?
daoluan
2015-08-11 13:05:48 +08:00
m
XadillaX
2015-08-11 13:14:04 +08:00
@Andiry 从库是给内部用的,做各种乱七八糟的事情。如果在主库做的话性能会有问题。
pixstone
2015-08-11 13:22:05 +08:00
如果仅仅是搜索这一次层面的 增量更新,其实可以ORM层发送MQ,然后推送更新到搜索服务器上。不过走 Bin log 应用逻辑上是能优雅很多。 除了搜索更新外,缓存推送更新,各种事件的触发也能从 Bin log 走。PO 主加油。。
pubby
2015-08-11 13:22:10 +08:00
但是主库没有binlog的话,如何同步到从库的?
pubby
2015-08-11 13:24:21 +08:00
走主从复制协议,伪装slave更优雅
XadillaX
2015-08-11 13:25:52 +08:00
@pixstone -。 - 的确我们目前是另启一个服务分析 Binlog 然后把有用的信息发送到 RabbitMQ 然后自然有后续的服务去消化。

并且这样做好以后各种其它可能的服务也可以用到这一层东西,只需要在解析 Binlog 这里做适配就好了。
XadillaX
2015-08-11 13:29:00 +08:00
@pubby 伪装 Slave 没有研究过啊协议什么的没有研究过啊,既然上头说要用 Binlog 我乖乖用就是了 -。 -
pubby
2015-08-11 13:34:33 +08:00
@XadillaX 也是读取binlog数据复制到从库的,没研究过,传的可能就是类似的数据。

听闻蘑菇街某些业务就是通过这样的方式更新cache信息的
XadillaX
2015-08-11 13:36:13 +08:00
@pubby 我们的 Cache 直接集成在自己的 ORM 轮子里面的。
phoenixlzx
2015-08-11 13:39:37 +08:00
GJ
linea
2015-08-11 13:39:43 +08:00
@Andiry 死月外号 “轮子月”
happyface
2015-08-11 13:42:43 +08:00
前段时间也想对binlog动手,用于同步内容进搜索引擎。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/212280

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX