由于运维、DBA误操作或业务bug,我们在运行过程中可能会不时误删数据。早期想要恢复数据,只能让业务人员根据线上操作日志构造误删数据,或者DBA使用binlog、备份等方式恢复数据。无论哪种方法,都非常费时费力,而且容易出错。直到彭立勋***在MySQL社区为mysqlbinlog扩展了闪回功能。在美团点评,我们也遇到过研发人员误删除主站配置信息,导致主站最长2小时不可用的情况。DBA同学使用技术团队自研的binlog2sql完成数据恢复,多次在线挽救因误删数据造成的严重故障。但是binlog2sql的恢复速度并不尽如人意,因此我们开发了一个新的工具——MyFlash,很好的解决了以上痛点,可以方便高效的进行数据恢复。现在该工具已正式开源,开源地址为:https://github.com/Meituan-Dianping/MyFlash。闪回工具的现状首先,让我们看一下目前市场上的恢复工具。从实现的角度,我们将它们分为以下几类。mysqlbinlog工具与sed和awk配合使用。该方法首先将binlog解析为类SQL文本,然后使用sed和awk将类SQL文本转换为真正的SQL。优点:当SQL中的字段类型比较简单时,可以快速生成需要的SQL,编程门槛比较低。缺点:当SQL中的字段类型比较复杂,尤其是字段中的文本包含HTML代码时,使用awk、sed等工具时,需要考虑极其复杂的转义等情况,出错概率为高的。修补数据库源代码。该方法扩展了mysqlbinlog的功能,增加了Flashback选项。优点:复用了MySQLServer层的binlog解析等代码。一旦稳定下来,就不用关心复杂的字段类型,效率高。缺点:修改前需要深入了解MySQL的复制代码结构和细节。版本敏感。MySQL5.6打的补丁基本不能用于MySQL5.7的回滚操作。升级难度大,因为补丁的代码分布在MySQL的各个文件和函数中。一旦MySQL代码发生变化,尤其是复制层的重构,升级的难度不亚于完全重写。使用业界提供的库来分析binlog,然后进行SQL构建,其优秀代表是binlog2sql。优点:使用了业界成熟的库,因此更稳定,上手难度更小。缺点:效率往往较低,实现受限于binlog库提供的功能。以上实现方式主要是提供了较少的过滤选项。比如不能提供基于SQL的过滤,需要回滚一条delete语句。因此回滚时需要结合awk、sed等工具进行过滤。总结以上工具的优缺点,我认为一个理想的闪回工具需要具备以下几个特点。A。不需要将binlog解析成文本再进行转换。b.提供基于库、表、SQL类型、位置、时间等的原生过滤方式。c.支持多个版本的MySQL。d.对数据库代码重构不敏感,方便升级。e.独立控制binlog解析,提供尽可能灵活的方式。其中,binlog分析是所有工作的基础。接下来介绍一下binlog的基本结构。binlog格式简介binlog格式概述一个完整的binlog文件以格式描述事件开始,以轮转事件结束,中间由多个其他事件组成。Binlog文件示例:每个事件由事件头和事件数据组成。下面简单介绍几种常见的binlog事件。①格式描述事件含义为:17090501:59:33serverid10end_log_pos123CRC320xed1ec563Start:binlogv4,serverv5.7.18-logcreated17090501:59:33②tablemap事件含义为:17090501:59:33serverid10end_log_pos339CRC320x3de40c0dTable_map:`test`.`test4`mappedtonumber238③updaterowevent表达的意思是:17090501:59:33serverid10end_log_pos385CRCUp320x179ef238flags:STMT_END_FUPDATE`test`.`test4`WHERE@1=3SET@1=13;binlogeventrollback根据上面的binlog介绍,可以看到每个binlogevent中的eventheader都有一个type_code,其中insert为30,update为31,delete为32。对于insert和delete这两个相反的操作,只要交换type_code即可,然后在binlogevent级别完成回滚。对于更新操作,格式如下。其中,BI指的是前像,AI指的是后像。我们只需要依次遍历修改前的数据和修改后的数据,一一交换即可。所以,整个回滚操作的难点在于回滚update语句,而update语句回滚的核心是计算每个AI和BI的长度。下面介绍长度的计算方法和部分字段。图像长度计算图像是由字段组成的,根据字段类型计算长度的方法不同。仅与字段类型相关。比如int占4个字节,bingint占8个字节。可以从表映射事件中获取类型信息。与字段类型及其参数有关。比如decimal(18,9)占9个字节,参数信息在tablemapevent中。与字段类型、参数、实际存储的值有关。比如varchar(10),有1个字节代表长度,后面的字节代表真正的数据。比如varchar(280),有2个字节表示长度。实际长度以数据为准。分析binlog中的几个关键点①lengthencodedintegerbinlog中的一个或多个字节组合表达不同的含义。例如,时间戳由固定的4个字节组成,事件类型用一个字节表示;数据库名和表名最多可以有64个字符,即使每个字符占3个字节,那么占用的字节数也是192<255。因此,最多使用一个字节,就可以完成实际的长度表示。但是,实际的列数可能需要1个字节、2个字节、3个字节甚至8个字节来表示。如果我们使用最大的8个字节来表示,那么在大多数情况下是一种存储空间的浪费。针对这种情况,长度编码整数应运而生。比如获取varchar类型的长度时,首先读取第一个字节,如果值小于251,那么varchar的长度就是第一个字节所代表的长度。如果第一个字节的值为0xFC,那么varchar的长度就是这个字节之后的最后两个字节组成,以此类推。②小数型小数由整数部分和小数部分组成。不管是整数还是小数,每9位数字,需要4个字节。如果不是9的倍数,则剩余小数位需要的字节数如下。为了描述方便,将这种关系定义为函数Fnum。比如对于decimal(18,10):整数部分可以显示为8,使用int,即4个字节。对于小数部分,需要的字节数为(10/9)*4+Fnum(10%9)=5。那么加起来就是4+5=9个字节。闪回工具架构在以上章节中,介绍了单个binlog事件的反转方法。在实践中,我们经常需要根据指定的条件对某个binlog进行过滤,从而过滤出需要的binlog并进行逆向。那么MyFlash如何实现这些目标呢?解析binlog首先将binlog文件解析成多个事件放入相关队列。在实现上,为了尽可能加快解析速度,用户可以指定解析的开始和结束位置。将binlog文件解析成binlogevent后,判断是否满足指定的时间条件,不满足则丢弃该event。注意:用户可以在不指定位置和时间的情况下解析整个文件。如果只指定了时间,那么也需要从文件开头解析,取出时间信息,然后再进行判断。因此,当需要回滚的binlog只占整个binlog的一小部分时,建议使用指定位置。重新组织事件,将binlog事件组成最小的执行单元。常见的binlog事件中,table_map事件包含了想要的表名、库名等元数据信息,而row_event(包括write_event、delete_event、update_event)则包含了真实的数据。因此在设计中采用了最小执行单元的概念。所谓最小执行单元,最小执行事件单元,通常包括一个table_map事件和若干个row_event。比如在binlog格式概述一节中介绍了table_map_event和update_row_event。如果只有update_row_event,那么我们无法知道这个事件对应的行记录变化对应的表。因此,一个完整的最小执行单元至少包含一个table_map_event和write_row_event、update_row_even、delete_row_event之一。为什么要用最小的执行单元?因为我们不能在闪回操作的时候简单的把每个事件倒过来,然后把所有事件的顺序倒过来。如果是这样的话,table_map事件会出现在row事件之后,这显然违反了binlog的执行逻辑。以最小执行单元,只需两步即可完成反演。A。反转最小执行单元中的行事件。b.最小执行单元队列倒序就够了。当然,也可以在反演之前加上滤波操作。比如filter库名、表名、SQL类型等。生成最小执行单元队列的binlog文件倒序生成后,只需要将每个最小执行单元依次输入到文件中即可。但是不要忘记修改每个binlog事件中的next_position来表示下一个binlog的位置。性能对比测试场景使用testFlashback2,插入100万条数据:CREATETABLE`testFlashback2`(`id`int(11)NOTNULLAUTO_INCREMENT,`nameShort`varchar(20)DEFAULTNULL,`nameLong`varchar(260)DEFAULTNULL,`amount`十进制(19,9)DEFAULTNULL,`amountFloat`floatDEFAULTNULL,`amountDouble`doubleDEFAULTNULL,`createDatetime6`datetime(6)DEFAULTNULL,`createDatetime`datetimeDEFAULTNULL,`createTimestamp`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,`nameText`text,`nameBlob`diumblob`mediumtext,PRIMARYKEY(`id`))ENGINE=InnoDBmysql>selectcount(*)fromtestFlashback2;+--------+|count(*)|+----------+|1048576|+------------+1rowinset(0.16sec)deletefromtestFlashback2;从上图测试结果可以看出,MyFlash是最快的。
