游戏运维 | MySQL Flashback拯救手抖党

黄Z程

2022-11-141227次浏览

0评论

0收藏

0点赞

分享

关于作者

网易游戏资深运维工程师,曾参与多款网易代理游戏产品的运营维护工作,后逐渐转向数据库管理维护领域。目前主要工作方向为网易游戏 Relational DBaaS 的后台功能开发和数据库管理维护,在 MySQL 性能调优、故障诊断等方面有丰富的实战经验,爱好学习开发和数据库内核技术知识,致力于成为一名了解业务、熟悉开发的DBA。

Outline

1.简单介绍:在 mysql 中 binlog 的功能,flashback 介绍,在 MySQL 业界现有的 flashback 的几种实现。

2.着重讲解:  flashback 的代码实现细节

3.延伸介绍:对比一下其他 DB 如 MariaDB、Oracle、TiDB 等的数据回滚

4.延伸思考:flashback 真的 flash 吗?回滚需要注意些什么?

一、简介:MySQL binlog 与 flashback

MySQL 的 Flashback 功能最初是由淘宝彭立勋在大概 7 年前并实现的一个很强劲的作品,起初他在 2012 年提交到 Feature Request 到官方 Oracle MySQL 团队的,点击链接可见目前信息仍大都停留在了 2012 年。直至今年稍早时候,又有其他开发者重新提出希望实现这个 feature 的声音。

另一边厢, MariaDB 分支从 10.2.4 版本开始支持 Flashback 功能,起初我们曾尝试使用 MariaDB 版的 mysqlbinlog 来直接实现回滚功能,但无奈经过我们的测试发现由于两个分支的一些实现不同(如 JSON 字段),使用 MariaDB 分支的 mysqlbinlog 工具来解析 MySQL 分支的 binlog 文件会出现错误。

因此为实现这一轻快方便的重要功能,网易游戏数据库团队,在参考前人设计思想(彭立勋,MySQL 下实现闪回的设计思路,美团 MyFlash 闪回工具等等)以及 MariaDB 分支源代码的基础上,自行将这一套 FLASHBACK  Patch 移植到网易游戏自有的 MySQL 代码中去,目前在日常测试和各项应用均表现出色。

本文则着重为各位读者讲解数据库团队在实现 FLASHBACK 上所做的事情,一说就要说彻底,因此我们从最底层的概念科普开始。

1.binlog 是什么, flashback 又是什么?

binlog 是 MySQL sever 层维护的一种二进制日志,其主要是用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句,并以「事务」的形式保存在磁盘中。可以简单将 binlog 理解成为记录数据变更的日志。

有了解 MySQL 或者阅读过 MySQL 数据同步机制介绍的童鞋应该对上面这张数据同步的经典图例不陌生,Binlog 的其中一个重要作用就是用于主从间的数据同步。此外,由于 Binlog 记录的是数据的变更,因此 Binlog 在很多场景中也被用于数据恢复。我们今天谈论的主角 Flashback 也正是基于此。

Flashback 按字面理解是闪回,即将数据回滚到某个时刻的「状态」,市面上现有的 MySQL flashback 工具,均是基于 binlog 这种数据修改日志 ,对数据再次进行与原有的修改相反的操作,从而实现将数据回滚到某个时刻的「状态」。

Note: 当前的 flashback 实现上仅考虑针对 DML 的回滚,针对 drop table/database 等 DDL 暂无法实现(思考为什么?下面详述 binlog 格式会给出相关答案)。

下面给一个简单的 flashback 实现图形举例: 

如上图,假设一个学生表有如下操作,在 10:01 分只有 Tony 和 Lucy 两个学生,在 10:05 分新增 Lee 学生,在 10:10 该 Lee 学生年龄被 update 为 33 岁,在 10:15 分从表中删除该 lee 学生。

假设 10:15 分之后没有任何其他数据变更操作,业务在 10:16 发现该删除是误操作,希望将表数据恢复至 10:05 分的状态,那么我们的数据回滚方案即是反转原有的数据操作,并逆序执行:先将该 LEE 学生 INSERT 回表中(此时该学生年龄为 33 岁),再执行 update 操作,将该学生年龄从 33 岁变更为 23 岁。完成这两部操作之后,数据即回滚到 10:05 的状态了。

2、当前 MySQL 业界的几种 flashback 实现

①mysqlbinlog 工具配合 sed、awk:先将 binlog 解析成类 SQL 的文本,再使用 sed、awk 把类 SQL 文本转换成真正的 SQL,同时实现语句的翻转。

优点:当 SQL 中字段类型比较简单时,可以快速生成需要的 SQL ,且编程门槛也比较低。

缺点:需要考虑极其复杂的转义等情况,出错概率很大。事实上基本也不会有人这么干的了

②直接使用 MariaDB 的 mysqlbinlog 工具,该工具已支持 flashback

优点:「官方」出品;直接使用

缺点:由于部分功能的实现不同(例如JSON字段的实现),容易出现不兼容原生 MySQL 的 binlog 文件的情况:目前已知的是使用 MariaDB 的 mysqlbinlog 工具对原生 MySQL server 产生的 binlog 中带有 json 字段则会解析失败

③基于业界开源的 binlog 解析库(python-mysql-replication)进行二次开发,直接实现回滚 SQL 的构造,优秀代表是美团点评团队:binlog2sql

优点:使用现成解析库方便就手,信手拈来,上手难度低

缺点:实现上受制于开源库提供的功能,运行效率低,需要使用开源解析库,将 binlog 解析成文本,再操作文本回滚

④自行开发工具对 binlog 文件进行解析与修改:优秀代表同样是美团点评团队在2017年开源的MyFLash

优点:binary 层面的操作,只须关注 binlog 格式与字段类型计算,对于数据库的代码重构不敏感,只要 binlog 版本不变,则不需要大改

缺点:实现上只适配了 v4 版本的 binlog 格式,不能解析 v4 以前的老版本;由于不能复用 binlog 解析代码,需要处理 binlog 中复杂的字段大小关系

⑤给源码打 patch:直接修改源码扩展 mysqlbinlog 工具的功能,增加 Flashback 的选项操作。

优点:复用了server 层中 binlog 解析代码,因此无须关心复杂的字段类型;能够结合使用到 mysqlbinlog 原有的一些过滤选项

缺点:需要对 mysql 的复制代码结构和细节需要有较为清晰深入的理解;改动的代码分布在 MySQL 的各个文件和函数中,MySQL 版本更新有可能需要重新对 patch 进行适配

二、patch 实现详解

1、binlog 基础知识详述

具体来说, MySQL 是以事件(event)为维度将数据变更按时间先后线性记录到 binlog 的,但 binlog 里不单单只有数据变更记录,还有其他诸如 描述该 binlog 文件格式的 event、rotate event、gtid event、start event,可以理解成涉及数据同步所发生的一切事情都以事件形式记录到 binlog,换句话说我们使用 binlog 可以在其他服务器上复现与该 binlog 对应的 server同样的数据变更,以达到和该对应 server 一直的数据状态( master-slave复制),又或者是某个指定的数据状态。目前网易游戏内部 MySQL DBaaS 服务所提供的精准时刻数据恢复功能也正是利用这个特性,先恢复一个全备份数据,再在该备份数据的基础上应用 binlog 到指点的时刻点。

一个完整的 binlog 文件是由一个 format description event 开头,一个 rotate event 结尾,中间由多个其他 event 组合而成,最常见的包括有

Previous gtid event:用于表示在这个 binlog 文件之前的全局事务号(GTID)列表,可以理解成这个 binlog 文件就不涉及 previous gtid 了

Gtid log event:用于表示以下数据变更对应的全局事务号(GTID)

Query_event:用于记录语句,比如 use database 等 statements,以及 drop table/database 等 DDL (Data Definition Language)。无论 binlog_format 设定成什么格式,这种类型的 event 都只记录语句,而不会记录数据镜像,因此 DDL 语句不能用于数据回滚。

Table map event:用于表示下述数据变更涉及到的表结构是如何定义,在RBR (Row Based Replication)格式的 binlog 中数据变更和数据表结构定义是作为两个事件( event )分开存放的;

⑤Rows_event : 在 RBR 中用于表示行数据的变更事件,里面存放的是一行或多行的数据镜像,包括以下三种类型

Update_Rows_event:更新数据(即 UPDATE 语句),内容包含有更新后的镜像(AI) 和更新前的镜像 (BI)

Write_Rows_event:写入数据(即 INSERT 语句),内容只包含更新后的镜像(AI)

Delete_Rows_event:删除数据(即 DELETE 语句),内容只包含更新前的镜像(BI)

⑥xid event:表示两阶段提交中的最后一步,即 commit

取决于 binlog_format的配置,SQL查询可能有以下记录方式:

STATEMENT:将 MySQL server 上执行的一个个 更新数据的 SQL statement 以文本的形式写到 binlog 中去,甚至还包括可能没有任何更新的语句也会记录在里面,比如一个删掉 test 表中一行不存在的语句(Rows changes: 0)。在这种格式下,所有的 DML 更新语句会以Query的事件类型(event type) 记录到 binlog 文件中,刚刚已经介绍过 query 类型的事件,是不会记录数据镜像的。因此,在 statement 格式下的 binlog,也无法进行 flashback 操作。

Row format: 以二进制形式将行变更写到 binlog 中去,每一行的变更可能包括修改前的镜像( Before Image )和修改后的镜像( After Image ),镜像是由一个个字段组成的,可以理解成 BI 存放的就是该行数据变更前的字段的数据,对应的AI 就是该行数据变更后的字段的数据。(这是我们修改的重点)。还要注意有个配置项叫binlog-row-image,用于配置是否记录所有的字段,它默认值是FULL,还有其他可选项minimal:只记录发生变更了的字段以及用于定位数据行的字段;noblob:记录除 BLOB 和 TEXT 类型外的所有字段

MIX: 即上述两者的混合,由 MySQL server 决定记录方式,此处不过多描述。

综上,我们本篇讨论的 flashback,有一个大前提—— binlog format 设置为 row format,且配置 binlog-row-image 是 FULL。只有在这种情况,MySQL 才会将整个行数据的所有内容存储在 binlog 中,才能实现反转。

使用 mysqlbinlog 对 binlog 文件进行解析的示例图:

2、EVENT 格式

每个 event 都是由 event header 和 event data 组成,以 update_rows_event 举例

3、mysqlbinlog 读取本地 binlog 并打印的主逻辑

4、那么 flashback 的逻辑应该怎么加呢?

按照前面的介绍,最多可能会有三层反转( update_row )
一、在单个 row_event 里面:对每一行数据进行 BI <—>AI;
二、在单个 row_event 里面:将每一行倒序
三、对于整个 binlog 文件:倒序所有的 row_event

接下来就是深入讲解代码实现逻辑的,为清晰表述思路,会有比较多的简化代码展现,可能会比较深入,需要仔细阅读才能理解。

不过没关系,上图我们给出了整一个 Flashback 的核心实现思想,记住这三个蓝色反转,会对下面的具体代码实现的理解有帮助。

4.1 如何翻转 BI 和 AI?

先重新回顾两个超常见的 event 类型并普及一个基本知识:

①table_map_event:table_map event 包含了所要了表名、库名等元数据信息;

②row_event:只包含真正的数据,表结构定义是没在这里的;

那么思路就变成:
BI <—> AI,反转前后镜像——>涉及到对行数据中 image 的长度计算——>则需要知道对应的 table define——>即需要对应的table map event

那么,这里参考美团的 MyFlash 文档介绍,同样地就可以引入一个概念——最小执行单元,如下图

4.2 最终思路如下

①构造最小执行单元:一个 table_map_event + 若干个 row_event

②反转打印最小执行单元中的单个 row_event:

Write_Rows_event: 因为只含有 AI,无需翻转 AI  和BI,只需改变 event_type_code 为delete_rows_event

Delete_Rows_event: 只含有 BI,无需翻转 AI 和 BI,只需改变event_type_codewrite_rows_event

Update_Rows_event: 含有 AI 和 BI,需要反转 AI 和 BI--> 反转过程涉及字段操作,需要知道该行所对应的table_define(td)--> 需要用到其对应的 table_map event

③逆序整个 binlog 的中的所有执行单元队列

4.3 构造最小的执行单元

①使用数组 events_in_stmt 来保存最小执行单元

②在 process_event() 中

每遇到一个 table_map_event,则意味着一个新的 events_in_stmt,则将 events_in_stmt 重置

③每遇到一个 row_event,调整 stmt_end 的标记:去掉原有最后一个 row_event 的 stmt_end 标记,将 events_in_stmt 数组中第一个 event 标记为 stmt_end,并将该 event 插入到 events_in_stmt 数组中

④直到遇到 stmt_end 标记的 row_event,这个标记意味着 events_in_stmt 作为最小执行单元已经保存所有的 rows_event,可以对 events_in_stmt 中的所有 event 进行倒序打印了:

4.4 反转打印单个 row_event

event 的打印是在 Log_event 类中实现的,因此 mysqlbinlog 只需在是在构造 log_event 的时候,将 log_event 的 is_flashback 标记为 TRUE 即可。

以下是简约代码展示

4.4.1 change_to_flashback() 做了什么?

①遍历行数据

对于 update_rows_event,交换 AI和 BI

将每一行数据存放到临时数组 rows_arr

②将行数据倒序覆盖回去 event

以下是该函数稍作简化后的代码展示

4.5 逆序所有执行单元打印

由于涉及到倒序打印,意味着大多数操作都不能马上真正打印,这就涉及到一系列临时保存使用的 buffer 数据结构了。

首先,单个 rows_log_event 的反转打印简要逻辑可以归纳如下

解释上面各种临时数据结构 buff:

tmp_buf:是  log_event::read_event() 读取的 event 原始内容,保存在这个临时 buffer 里

②print_event_info->body_cache: 存放打印出来的event body

output_buf: 因为 flashback 不能真正马上打印出来,调用 ev->print() 打印最终实际上即是存放在这个 buff 里面

然后,整个 binlog 所有 rows_event 的打印校验逻辑可言归纳如下

三、延伸介绍

思考一下还有没其他比 FLASHBACK 更快的回滚数据实现?能不能一条命令直接一把梭?

实际上是有的!在SQL:2011标准中引入的系统版本表(system-versioned-table)定义带给我们答案——直接将不同时刻的数据状态都存储记录下来,让我们简单看看其他 DBMS 在实现这个标准上做的事情。

1、MariaDB、Oracle

系统版本表是SQL:2011标准中首次引入的功能,它存储所有更改的历史数据,而不仅仅是当前时刻有效的数据。

举个例子,同一行数据一秒内被更改了 10 次,那么系统版本表就会保存 10 份不同时间的版本数据。就像电影《源代码》里的平行世界理论一样,你可以退回任意时间里,从而有效保障你的数据是安全的。也就是说,DBA 手抖或是程序 BUG 引起的数据丢失,在 MariaDB 10.3 里已然成为过去。
MariaDB 在 10.3.4 推出system-versioned-table的技术,实现这个功能。

参考介绍:

而 Oracle 则是在 12c 推出 Temporal Validity技术,所谓Temporal Validity技术,就是指表记录的可视性将由 PERIOD FOR 指定的时间维度字段的范围而定,只有落在时间字段范围之内的记录,相关 SQL 语句才可看见这些记录,而落在时间字段范围之外的记录,相关 SQL 语句则无法访问这些记录。而记录的时间范围则完全由应用程序来控制

参考介绍:

mariadb 和 oracle 使用的相关语法关键词都是period for,创建示例如下:

这样,我们在查询的时候就可以直接如下的命令一把梭查询到历史的数据,妈妈再也不会担心 DBA 删库跑路啦~

除了数据恢复之外,这个功能还可以用于数据变更审计、不同时间点的数据比较等需求。

2、TiDB

类似的,在一些 NewSQL 中,也有相关的功能实现。如 TiDB 的历史读功能,它提供了如下特性:

①保存多版本的数据,而不是真的去删除数据或更新数据:each update/removal creates a new version of the data object instead of updating/removing the data object in-place

②定期对过期数据进行垃圾回收以防止数据无限量增长:Garbage Collection (GC) runs periodically to remove the obsolete data versions

详细介绍:

四、延伸思考:

1、flashback 真的 flash 吗?

根据上面的 flashback 实现思路详解和其他 DB 的多版本数据的简介,MySQL 这种所谓的Flashback,其实并不 flash,它的实现实际上已经是对数据的二次操作(反转操作),而这样的一个实现思路,相比其他 DB 直接存储多版本数据快照的实现来说,实际上是可以认为对于 MySQL 自身没有实现存储多版本数据快照的功能的妥协与无奈。
当然,对于另外一种回滚的实现(先恢复一个全备份,然后再应用其对应的 binlog 到指定的时间点),已经快多了!

2、在操作 flashback 过程中,有什么隐患?

如果 flashback 操作不当,很有可能会导致数据没能够回到用户想要的“状态”,仍然以上面的学生表举例子:

如上图示,假设用户 A 在 10:14:00 发现数据有问题,希望回滚将该学生表回到 10:00:00 时候的状态(即该表中还没有 Lee 学生时的状态),在 10:14:15 生成的 flashback 文件,但恰好在 10:14:16 该 Lee学生被用户 B 修改了,性别被置为 F,而回滚 binlog 是在 10:14:17 的时候才执行,不难发现,此时执行第一条update回滚语句UPDATE STUDENT SET NAME='LEE', GENDER='M',AGE=23 WHERE NAME='LEE',GENDER='M', AGE=33;会因为 GENDER 不匹配,实际应用不成功,由此类推,第一条回滚不成功,那么第二条肯定也不会成功。

3、flashback 回滚的正确使用姿势

根据上面举的例子,flashback 正确使用姿势就是在执行回滚前,将整库置为只读,不允许有新的更新操作,以防止后续数据有任何的变更,然后再开始生成回滚 binlog,并应用回去 MySQL server。

这是因为 flashback 的操作实际上是对数据的二次操作,因此我们在使用这个工具进行回滚的时候,一定要确保数据不会再发生变更,数据“状态”能与我们在生成回滚 BINLOG 的时候一致。

五、资料引用

1、Feature Request

 

2、彭立勋,MySQL下实现闪回的设计思路

3、美团MyFlash闪回工具

4、python-mysql-replication

5、MyFLash

评论 0

0/1000
网易游学APP
为热爱赋能
扫描二维码下载APP