解析 TiDB 在线数据同步工具 Syncer

摘要:详细介绍了如何将 MySQL 的数据迁移到 TiDB,并将 TiDB 做为 MySQL 的 Slave 进行数据同步。(做者:崔秋)mysql

TiDB 是一个彻底分布式的关系型数据库,从诞生的第一天起,咱们就想让它来兼容 MySQL 语法,但愿让原有的 MySQL 用户 (不论是单机的 MySQL,仍是多机的 MySQL Sharding)均可以在基本不修改代码的状况下,除了能够保留原有的 SQL 和 ACID 事务以外,还能够享受到分布式带来的高并发,高吞吐和 MPP 的高性能。git

对于用户来讲,简单易用是他们试用的最基本要求,得益于社区和 PingCAP 小伙伴们的努力,咱们提供基于 Binary 和 基于 Kubernetes 的两种不一样的一键部署方案来让用户能够在几分钟就能够部署起来一个分布式的 TiDB 集群,从而快速地进行体验。
固然,对于用户来讲,最好的体验方式就是从原有的 MySQL 数据库同步一份数据镜像到 TiDB 来进行对于对比测试,不只简单直观,并且也足够有说服力。实际上,咱们已经提供了一整套的工具来辅助用户在线作数据同步,具体的能够参考咱们以前的一篇文章:TiDB 做为 MySQL Slave 实现实时数据同步, 这里就再也不展开了。后来有不少社区的朋友特别想了解其中关键的 Syncer 组件的技术实现细节,因而就有了这篇文章。github

首先咱们看下 Syncer 的总体架构图, 对于 Syncer 的做用和定位有一个直观的印象。sql

图片描述
从总体的架构能够看到,Syncer 主要是经过把本身注册为一个 MySQL Slave 的方式,和 MySQL Master 进行通讯,而后不断读取 MySQL Binlog,进行 Binlog Event 解析,规则过滤和数据同步。从工程的复杂度上来看,相对来讲仍是很是简单的,相对麻烦的地方主要是 Binlog Event 解析和各类异常处理,也是容易掉坑的地方。数据库

为了完整地解释 Syncer 的在线同步实现,咱们须要有一些额外的内容须要了解。安全

MySQL Replication

咱们先看看 MySQL 原生的 Replication 复制方案,其实原理上也很简单:网络

  • MySQL Master 将数据变化记录到 Binlog (Binary Log),架构

  • MySQL Slave 的 I/O Thread 将 MySQL Master 的 Binlog 同步到本地保存为 Relay Log并发

  • MySQL Slave 的 SQL Thread 读取本地的 Relay Log,将数据变化同步到自身分布式

blob.png

MySQL Binlog

MySQL 的 Binlog 分为几种不一样的类型,咱们先来大概了解下,也看看具体的优缺点。

  • Row
    MySQL Master 将详细记录表的每一行数据变化的明细记录到 Binlog。

优势:完整地记录了行数据的变化信息,彻底不依赖于存储过程,函数和触发器等等,不会出现由于一些依赖上下文信息而致使的主从数据不一致的问题。
缺点:全部的增删改查操做都会完整地记录在 Binlog 中,会消耗更大的存储空间。

  • Statement
    MySQL Master 将每一条修改数据的 SQL 都会记录到 Binlog。

优势:相比 Row 模式,Statement 模式不须要记录每行数据变化,因此节省存储量和 IO,提升性能。
缺点:一些依赖于上下文信息的功能,好比 auto increment id,user define function, on update current_timestamp/now 等可能致使的数据不一致问题。

  • Mixed
    MySQL Master 至关于 Row 和 Statement 模式的融合。

优势:根据 SQL 语句,自动选择 Row 和 Statement 模式,在数据一致性,性能和存储空间方面能够作到很好的平衡。
缺点:两种不一样的模式混合在一块儿,解析处理起来会相对比较麻烦。

MySQL Binlog Event

了解了 MySQL Replication 和 MySQL Binlog 模式以后,终于进入到了最复杂的 MySQL Binlog Event 协议解析阶段了。

在解析 MySQL Binlog Eevent 以前,咱们首先看下 MySQL Slave 在协议上是怎么和 MySQL Master 进行交互的。

Binlog dump

首先,咱们须要伪造一个 Slave,向 MySQL Master 注册,这样 Master 才会发送 Binlog Event。注册很简单,就是向 Master 发送 COM_REGISTER_SLAVE 命令,带上 Slave 相关信息。这里须要注意,由于在 MySQL 的 replication topology 中,都须要使用一个惟一的 server id 来区别标示不一样的 Server 实例,因此这里咱们伪造的 slave 也须要一个惟一的 server id。

Binlog Event

对于一个 Binlog Event 来讲,它分为三个部分,header,post-header 以及 payload。
MySQL 的 Binlog Event 有不少版本,咱们只关心 v4 版本的,也就是从 MySQL 5.1.x 以后支持的版本,太老的版本应该基本上没什么人用了。

Binlog Event 的 header 格式以下:

blob.png

header 的长度固定为 19,event type 用来标识这个 event 的类型,event size 则是该 event 包括 header 的总体长度,而 log pos 则是下一个 event 所在的位置。

这个 header 对于全部的 event 都是通用的,接下来咱们看看具体的 event。

FORMAT_DESCRIPTION_EVENT

在 v4 版本的 Binlog 文件中,第一个 event 就是 FORMAT_DESCRIPTION_EVENT,格式为:

blob.png

咱们须要关注的就是 event type header length 这个字段,它保存了不一样 event 的 post-header 长度,一般咱们都不须要关注这个值,可是在解析后面很是重要的ROWS_EVENT 的时候,就须要它来判断 TableID 的长度了, 这个后续在说明。

ROTATE_EVENT

而 Binlog 文件的结尾,一般(只要 Master 不当机)就是 ROTATE_EVENT,格式以下:

blob.png

它里面其实就是标明下一个 event 所在的 binlog filename 和 position。这里须要注意,当 Slave 发送 Binlog dump 以后,Master 首先会发送一个 ROTATE_EVENT,用来告知 Slave下一个 event 所在位置,而后才跟着 FORMAT_DESCRIPTION_EVENT。

其实咱们能够看到,Binlog Event 的格式很简单,文档都有着详细的说明。一般来讲,咱们仅仅须要关注几种特定类型的 event,因此只须要写出这几种 event 的解析代码就能够了,剩下的彻底能够跳过。

TABLE_MAP_EVENT

上面咱们提到 Syncer 使用 Row 模式的 Binlog,关于增删改的操做,对应于最核心的ROWS_EVENT ,它记录了每一行数据的变化状况。而如何解析相关的数据,是很是复杂的。在详细说明 ROWS_EVENT 以前,咱们先来看看 TABLE_MAP_EVENT,该 event 记录的是某个 table 一些相关信息,格式以下:

blob.png

table id 须要根据 post_header_len 来判断字节长度,而 post_header_len 就是存放到 FORMAT_DESCRIPTION_EVENT 里面的。这里须要注意,虽然咱们能够用 table id 来表明一个特定的 table,可是由于 Alter Table 或者 Rotate Binlog Event 等缘由,Master 会改变某个 table 的 table id,因此咱们在外部不能使用这个 table id 来索引某个 table。

TABLE_MAP_EVENT 最须要关注的就是里面的 column meta 信息,后续咱们解析 ROWS_EVENT 的时候会根据这个来处理不一样数据类型的数据。column def 则定义了每一个列的类型。

ROWS_EVENT

ROWS_EVENT 包含了 insert,update 以及 delete 三种 event,而且有 v0,v1 以及 v2 三个版本。
ROWS_EVENT 的格式很复杂,以下:

blob.png

ROWS_EVENT 的 table id 跟 TABLE_MAP_EVENT 同样,虽然 table id 可能变化,可是 ROWS_EVENT 和 TABLE_MAP_EVENT 的 table id 是能保证一致的,因此咱们也是经过这个来找到对应的 TABLE_MAP_EVENT。
为了节省空间,ROWS_EVENT 里面对于各列状态都是采用 bitmap 的方式来处理的。

首先咱们须要获得 columns present bitmap 的数据,这个值用来表示当前列的一些状态,若是没有设置,也就是某列对应的 bit 为 0,代表该 ROWS_EVENT 里面没有该列的数据,外部直接使用 null 代替就成了。

而后就是 null bitmap,这个用来代表一行实际的数据里面有哪些列是 null 的,这里最坑爹的是 null bitmap 的计算方式并非 (num of columns+7)/8,也就是 MySQL 计算 bitmap 最通用的方式,而是经过 columns present bitmap 的 bits set 个数来计算的,这个坑真的很大。为何要这么设计呢,可能最主要的缘由就在于 MySQL 5.6 以后 Binlog Row Image 的格式增长了 minimal 和 noblob,尤为是 minimal,update 的时候只会记录相应更改字段的数据,好比我一行有 16 列,那么用 2 个 byte 就能搞定 null bitmap 了,可是若是这时候只有第一列更新了数据,其实咱们只须要使用 1 个 byte 就能记录了,由于后面的铁定全为 0,就不须要额外空间存放了。bits set 其实也很好理解,就是一个 byte 按照二进制展现的时候 1 的个数,譬如 1 的 bits set 就是1,而 3 的 bits set 就是 2,而 255 的 bits set 就是 8 了。

获得了 present bitmap 以及 null bitmap 以后,咱们就能实际解析这行对应的列数据了,对于每一列,首先判断是否 present bitmap 标记了,若是为 0,则跳过用 null 表示,而后在看是否在 null bitmap 里面标记了,若是为 1,代表值为 null,最后咱们就开始解析真正有数据的列了。

可是,由于咱们获得的是一行数据的二进制流,咱们怎么知道一列数据如何解析?这里,就要靠 TABLE_MAP_EVENT 里面的 column def 以及 meta 了。
column def 定义了该列的数据类型,对于一些特定的类型,譬如 MYSQL_TYPE_LONG, MYSQL_TYPE_TINY 等,长度都是固定的,因此咱们能够直接读取对应的长度数据获得实际的值。可是对于一些类型,则没有这么简单了。这时候就须要经过 meta 来辅助计算了。

譬如对于 MYSQL_TYPE_BLOB 类型,meta 为 1 代表是 tiny blob,第一个字节就是 blob 的长度,2 代表的是 short blob,前两个字节为 blob 的长度等,而对于 MYSQL_TYPE_VARCHAR 类型,meta 则存储的是 string 长度。固然这里面还有最复杂的 MYSQL_TYPE_NEWDECIMAL, MYSQL_TYPE_TIME2 等类型,关于不一样类型的 column 解析仍是比较复杂的,能够单独开一章专门来介绍,由于篇幅关系这里就不展开介绍了,具体的能够参考官方文档。

搞定了这些,咱们终于能够完整的解析一个 ROWS_EVENT 了:)

XID_EVENT
在事务提交时,不论是 Statement 仍是 Row 模式的 Binlog,都会在末尾添加一个 XID_EVENT 事件表明事务的结束,里面包含事务的 ID 信息。

QUERY_EVENT
QUERY_EVENT 主要用于记录具体执行的 SQL 语句,MySQL 全部的 DDL 操做都记录在这个 event 里面。

Syncer

介绍完了 MySQL Replication 和 MySQL Binlog Event 以后,理解 Syncer 就变的比较容易了,上面已经介绍过基本的架构和功能了,在 Syncer 中, 解析和同步 MySQL Binlog,咱们使用的是咱们首席架构师唐刘的 go-mysql 做为核心 lib,这个 lib 已经在 github 和 bilibili 线上使用了,因此是很是安全可靠的。因此这部分咱们就跳过介绍了,感兴趣的话,能够看下 github 开源的代码。这里面主要介绍几个核心问题:

MySQL Binlog 模式的选择

在 Syncer 的设计中,首先考虑的是可靠性问题,即便 Syncer 异常退出也能够直接重启起来,也不会对线上数据一致性产生影响。为了实现这个目标,咱们必须处理数据同步的可重入问题。
对于 Mixed 模式来讲,一个 insert 操做,在 Binlog 中记录的是 insert SQL,若是 Syncer 异常退出的话,由于 Savepoint 尚未来得及更新,会致使重启以后继续以前的 insert SQL,就会致使主键冲突问题,固然能够对 SQL 进行改写,将 insert 改为 replace,可是这里面就涉及到了 SQL 的解析和转换问题,处理起来就有点麻烦了。另一点就是,最新版本的 MySQL 5.7 已经把 Row 模式做为默认的 Binlog 格式了。因此,在 Syncer 的实现中,咱们很天然地选择 Row 模式做为 Binlog 的数据同步模式。

Savepoint 的选取

对于 Syncer 自己来讲,咱们更多的是考虑让它尽量的简单和高效,因此每次 Syncer 重启都要尽量从上次同步的 Binlog Pos 的地方作相似断点续传的同步。如何选取 Savepoint 就是一个须要考虑的问题了。
对于一个 DML 操做来讲(以 Insert SQL 操做举例来看),基本的 Binlog Event 大概是下面的样子:

blob.png

咱们从 MySQL Binlog Event 中能够看到,每一个 Event 均可以获取下一个 Event 开始的 MySQL Binlog Pos 位置,因此只要获取这个 Pos 信息保存下来就能够了。可是咱们须要考虑的是,TABLE_MAP_EVENT 这个 event 是不能被 save 的,由于对于 WRITE_ROWS_EVENT 来讲,没有 TABLE_MAP_EVENT 基本上没有办法进行数据解析,因此为何不少人抱怨 MySQL Binlog 协议不灵活,主要缘由就在这里,由于不论是 TABLE_MAP_EVENT 仍是 WRITE_ROWS_EVENT 里面都没有 Schema 相关的信息的,这个信息只能在某个地方保留起来,好比 MySQL Slave,也就是 MySQL Binlog 是没有办法自解析的。

固然,对于 DDL 操做就比较简单了,DDL 自己就是一个 QUERY_EVENT。

因此,Syncer 处于性能和安全性的考虑,咱们会按期和遇到 DDL 的时候进行 Save。你们可能也注意到了,Savepoint 目前是存储在本地的,也就是存在必定程度的单点问题,暂时还在咱们的 TODO 里面。

断点数据同步

在上面咱们已经抛出过这个问题了,对于 Row 模式的 MySQL Binlog 来讲,实现这点相对来讲也是比较容易的。举例来讲,对于一个包含 3 行 insert row 的 Txn 来讲,event 大概是这样的:

blob.png

因此在 Syncer 里面作的事情就比较容易了,就是把每一个 WRITE_ROWS_EVENT 结合 TABLE_MAP_EVENT,去生成一个 replace into 的 SQL,为何这里不用 insert 呢?主要是 replace into 是可重入的,重复执行屡次,也不会对数据一致性产生破坏。
另一个比较麻烦的问题就是 DDL 的操做,TiDB 的 DDL 实现是彻底无阻塞的,因此根据 TiDB Lease 的大小不一样,会执行比较长的时间,因此 DDL 操做是一个代价很高的操做,在 Syncer 的处理中经过获取 DDL 返回的标准 MySQL 错误来判断 DDL 是否须要重复执行。

固然,在数据同步的过程当中,咱们也作了不少其余的工做,包括并发 sync 支持,MySQL 网络重连,基于 DB/Table 的规则定制等等,感兴趣的能够直接看咱们 tidb-tools/syncer 的开源实现,这里就不展开介绍了。

欢迎对 Syncer 这个小项目感兴趣的小伙伴们在 Github 上面和咱们讨论交流,固然更欢迎各类 PR:)

相关文章
相关标签/搜索