由于运维、DBA的误操作或是业务bug,我们在操作中时不时会出现误删除数据情况。早期要想恢复数据,只能让业务人员根据线上操作日志,构造误删除的数据,或者DBA使用binlog和备份的方式恢复数据,不管那种,都非常费时费力,而且容易出错。直到彭立勋***在MySQL社区为mysqlbinlog扩展了闪回功能。
在美团点评,我们也遇到过研发人员误删主站的配置信息,从而导致主站长达2个小时不可用的情况。DBA同学当时使用了技术团队自研的binlog2sql完成了数据恢复,并多次挽救了线上误删数据导致的严重故障。不过,binlog2sql在恢复速度上不尽如人意,因此我们开发了一个新的工具——MyFlash,它很好地解决了上述痛点,能够方便并且高效地进行数据恢复。
现在该工具正式开源,开源地址为:。
闪回工具现状
先来看下目前市面上已有的恢复工具,我们从实现角度把它们划分成如下几类。
mysqlbinlog工具配合sed、awk。该方式先将binlog解析成类SQL的文本,然后使用sed、awk把类SQL文本转换成真正的SQL。
给数据库源码打patch。该方式扩展了mysqlbinlog的功能,增加Flashback选项。
使用业界提供的解析binlog的库,然后进行SQL构造,其优秀代表是binlog2sql。
上述几种实现方式,主要是提供的过滤选项较少,比如不能提供基于SQL类型的过滤,需要回滚一个delete语句,导致在回滚时,需要结合awk、sed等工具进行筛选。
总结了上述几种工具的优缺点,我认为理想的闪回工具需要有以下特性。
a. 无需把binlog解析成文本,再进行转换。
b. 提供原生的基于库、表、SQL类型、位置、时间等多种过滤方式。
c. 支持MySQL多个版本。
d. 对于数据库的代码重构不敏感,利于升级。
e. 自主掌控binlog解析,提供尽可能灵活的方式。
在这些特性中,binlog的解析是一切工作的基础。接下来我会介绍binlog的基本结构。
binlog格式初探
binlog格式概览
一个完整的binlog文件是由一个format description event开头,一个rotate event结尾,中间由多个其他event组合而成。
binlog文件实例:
每个event都是由event header 和event>
表达的含义是:
170905 01:59:33 server id 10 end_log_pos 123 CRC32 0xed1ec563
Start: binlog v 4, server v 5.7.18-log created 170905 01:59:33
② table map event
表达的含义是:
170905 01:59:33 server id 10 end_log_pos 339 CRC32 0x3de40c0d
Table_map: `test`.`test4` mapped to number 238
③ update row event
表达的含义是:
170905 01:59:33 server id 10 end_log_pos 385 CRC32 0x179ef6dd
Update_rows: table id 238 flags: STMT_END_F
UPDATE `test`.`test4` WHERE @1=3 SET @1=13;
binlog event回滚
根据上面的binlog介绍,可以看到每个binlog event中event header有个type_code,其中insert为30,update为31,delete为32。对于insert和delete两个相反的操作,只需把type_code互换,则在binlog event级别完成回滚。
而对于update操作,其格式如下。
其中,BI是指before image,AI是指after image。
我们只需依次遍历修改前的数据和修改后的数据,并一一互换即可。因此整个回滚操作的难点在于回滚update语句,而update语句回滚的核心在于计算出每个AI、BI的长度。下面介绍下长度以及部分字段的计算方法。
镜像长度计算
镜像是由一个个字段组成的,根据字段类型的不同,其计算长度的方法也不一样。
解析binlog中的若干个关键点
① length encoded integer
binlog中一个或者多个字节组合,分别表示了不同的含义。比如,timestamp是由固定的4个字节组成,event类型由一个字节表示;数据库名和表名最长为64个字符,即使每个字符占用3个字节,那么占用的字节数为192<255。因此最多使用一个字节,就可以完成实际长度表示。
然而列的实际数量,可能需要超过1个字节、2个字节、3个字节甚至8个字节去表示。如果我们使用***的8个字节去表示,那么在绝大多数情况下都是浪费存储空间的。针对这种情况,length encoded integer应运而生。
比如在获取一个varchar类型的长度时,首先读取***个字节,如果值小于251,那么varchar的长度就是***个字节表示的长度。如果***个字节的值为0xFC,那么varchar的长度是由该字节之后的后两个字节组成,以此类推。
② decimal类型
decimal是由整数部分和小数部分组成。无论是整数还是小数,每9个数字,需要4个字节。如果不是9的倍数,剩余的小数位,需要的字节数如下,为方便描述,将该关系定义为函数Fnum。
举例,对于 decimal(18,10):
闪回工具架构
在上面的章节中,介绍了单个binlog event的反转方法。在实践中,我们往往需要把某个binlog,按照指定的条件,过滤出需要的binlog,并进行反转。那么MyFlash是如何完成这些目标的呢?
解析binlog
首先把binlog文件,解析成多个event,放入到相关队列中。在实现上,为了尽可能加快解析速度,可以让用户指定解析的开始与结束位置。把binlog文件解析成binlog event后,再判断下是否符合指定的时间条件,若不符合,则丢弃该event。
注意:用户可以不指定位置和时间,则解析整个文件。如果只指定时间,那么也需要从文件开始处解析,取出时间信息,再进行判断。因此,当需要回滚的binlog只占整个binlog的一小部分时,推荐使用指定位置。
重组event
把binlog event组成最小执行单元。在常见的binlog event中table_map event包含了所要了表名、库名等元数据信息,而row_event(包含write_event、delete_event、update_event)包含了真正的数据。因此在设计中使用了一个最小执行单元概念。所谓的最小执行单元,即least execution event unit,通常包含一个table_map event和若干个row_event。
比如在binlog格式概览一节中,介绍了table_map_event和update_row_event。如果只有update_row_event,那么我们无法知道这个event对应的行记录变更对应的表。因此一个完整的最小执行单元最少包含一个table_map_event和write_row_event、update_row_even、delete_row_event中的一个。
为什么我们需要使用最小执行单元?因为我们在闪回操作时,不能简单的把每个event反转之后,然后再将所有event的顺序反转过来。如果这样的话,就会出现table_map event在row event之后,这显然是违反binlog执行逻辑的。
有了最小执行单元之后,只需两步,即可完成反转。
a. 反转最小执行单元中的row event。
b. 逆序最小执行单元队列,即可。
当然在反转前,也可以增加过滤操作。比如过滤库名、表名和SQL类型等。
生成binlog文件
有了逆序的最小执行单元队列后,只需把每个最小执行单元依次输入到文件即可。不过不要忘了修改每个binlog event里的next_position,用来表示下一个binlog的位置。
性能对比
测试场景
使用testFlashback2,插入100万条数据:
`id``nameShort``nameLong``amount``amountFloat``amountDouble``createDatetime6`datetime(6)`createDatetime`datetime`createTimestamp`mysql>+++1row
测试结果
从上述图表中可以看出,MyFlash的速度最快。