DRC介绍
饿了么的 Data Replicate Center(DRC)项目用于数据双向复制和数据订阅,使用场景如下图:
要点说明:
-
跨机房的 Mysql 数据复制完全通过 DRC 来完成
-
还有很多业务团队通过 DRC 来实现数据订阅
目前饿了么100%的跨机房数据复制,90%的数据订阅都通过DRC完成,每天有大量的数据流经DRC。复制延迟保持在1s以下,从来没有发生过数据丢失和错乱的情况。
DRC的设计目标包括:
-
实时双向数据复制,延时 < 1s ,并能够解决双向修改时的数据冲突
-
数据变更订阅,能够在DB数据发生变化时通知到相关订阅方
-
高度可靠和保持顺序,不能丢失数据,也不能因为错乱执行顺序导致数据错误
我们最终达到了这3个目标,下面围绕着如何设计以满足以上目标介绍一下。
DRC的总体设计
DRC采用了多组件的集群化设计,整体结构如下图:
要点说明:
-
DRC有两个核心服务,Replicator 和 Apply,他们以集群化的方式部署,一个负责从源头数据库拉取变更,一个负责应用变更到目标数据库。
-
Master Replicator目前实现了 Mysql Binary Log Dump 协议,从Source Mysql 中取得 DataChangeEvent。
-
对于任何唯一的数据源,数据修改事件(DataChangeEvent)需要能够映射为唯一的单调递增的SCN(System Change Number),如果不能执行这样的映射,则DRC的一致性就不能得到保证。
-
Replicator接收到 DataChangeEvent 数据后使用一个Heap外的环状内存结构(MMAP)保存,减少GC负荷,为了保证低延迟的复制,不写入本地文件。
-
Replicator Slave是一种稍微特殊的 Replicator,从 Master Replicator 拉取 DataChangeEvent,并保存到本地EventBuffer中。
-
Replicator中保存当前 SCN Number,有一个 Master 和任意多个 Slave ,当 Masters失效后,Slave Replicator 可以选举新的 Master
-
Client SDK 从 Replicator 中拉取 DataChange Event序列,拉取时需要提供当前位置(SCN)和过滤条件,过滤条件支持基于库名,或者表名的过滤。
-
Replicator 中不保存任何 Client SDK 相关的状态信息(SCN),该信息由 Client SDK 调用方维护。
-
Data Apply负责把ChangeEvent按照需要的格式应用到数据库中,需要保持事务一致性,按照SCN的顺序执行。
其中 SCN 和 DataChangeEvent 是整个DRC的两个核心概念,需要详细说明一下:
SCN是一个结构体,能够单调有续,并全局唯一,绑定到唯一的事件上,每当 DRC 从 Mysql 拿到一个新的 Data Change Event,就会分配一个新的 SCN 与之对应。
每种数据源对应于一种 SCN 的结构,对于 Mysql 数据源的SCN结构体代码如下:
SCN要求单调有续,并全局唯一,所以SCN 的产生逻辑大部分情况下需要绑定到数据源的唯一逻辑上,例如 Mysql 的 SCN 实现就嵌入了 mysql 的 serverid,logIndex,logPostion,这样能保证对于一个唯一的 mysql server 来说,scn 是单调有序并唯一的,我们还加上了一个 changeId 字段,这样,如果数据抽取切换到另外一个 mysql 上了,changeId 只需要 +1,就可以保证产生的SCN依然有序,关于 Mysql Bin Log 结构的详细信息
整个SCN的产生逻辑如下图:
有了唯一的 SCN 之后,整个系统就有了保证一致性的最基础保障。SCN是在 Replicator 端生成的,并贯穿了整个 DRC 系统的各个组件,所有的组件都用相同的SCN来标志event,也用SCN来记录当前复制的位点。
下面简单介绍一下各个组件的要点,以方便之后的介绍。
Replicator: replicator 负责变更事件的抽取,SCN的生成,以及维护了一个Event Buffer 来存储取到的 event,结构如下图:
要点说明:
-
replicator 有一个 master 多个 slave,只有 master 会连接 DB,其他的 slave 只连接 master replicator。
-
SCN在各个Replicator中是一致的,当 master 失效后会选举出新的 master(zk),并从该 master 的当前位置开始复制,这样就避免了非常复杂的在多个 replicator 间保持一致的问题。
-
Replicator 中不保存客户端状态,Client SDK Pull 数据的时候需要指定开始 SCN 位置,所以可以随时切换到任何一个Replicator拉取数据。
-
如果客户端提交的SCN超过当前Replicator的最老数据,Replicator 会回源到源头的数据库拉取。
-
Replicator 在维护了一个 RingBuffer,用于保存 ChangeEvent,这个buffer我们叫做 EventBuffer,EventBuffer 是 DRC 提高性能的一个非常关键的环节,之后回详细说明。
Apply :Apply 部署在目标端,负责把读取到的数据写入目标数据库,或者把变更消息发送到指定的消息队列中,目前 DRC 支持 Kafka / RabbitMQ / MaxQ 等多种消息队列。
要点说明:
-
Apply 以 Channel 的形式在 DataApply Server 上组织复制单元
-
Channel 是一张表或者一组表
-
Channel 内部逻辑通过串联的 Filter 实现
-
Change 上实现了诸多的业务逻辑,例如幂等,冲突检查,并行化性能优化等等。
-
以上就是 DRC 的一个整体介绍,下面说一下 DRC 的实现中一些具体的技术点。
DRC 实现
-
数据一致保证
如何保证数据一致:一致性是 DRC 的最基本要求,DRC 通过一系列的方法保证双向复制中两边的数据一致。这其中有三个问题要说一下:
-
问题一:如何防止循环复制?
双向复制要解决的一个重要问题是循环复制,要能够识别出一个改变动作是来自产研,还是来自DRC工具本身。来自产研的数据变更需要复制出去,而来自DRC的数据复制不需要复制。
DRC防止冲突的方案是在由DRC产生的事务中加入DRC标记。如下图的事务2,Apply在写入时,会在事务的开头处加上一个 insert 或者是 update 语句,其中包含了 DRC 的信息。当Replicator 发现一个事物包涵该特殊标记时,就不会再复制出去。
这种方式虽然避免了循环复制,带给目标端数据库带来了一些性能开销,我们会在之后的版本中通过修改 Mysql Binlog 机制来更为高效的阻断循环复制。
-
问题二:万一发生中断或者是故障,如果保证数据正确?
中断恢复:从中断恢复,主要靠的是SCN,因为SCN有序,并保存在可靠存储中,任何节点的失效,都可以通过SCN位点来恢复,SCN可以被保存在本地文件,ZK和数据库中,达到性能和可靠性的平衡。
幂等:另外,大部分的DB操作在DRC下是幂等的,从任意一点开始,重复执行一次,还是会得到相同的结果。DRC能够处理各种重复执行带来的异常,并且保证最终数据始终一致。这里主要靠的是详细分析各种重复执行可能带来的异常,对能够跳过的异常就直接跳过,而不停止复制。
-
问题三:万一一笔数据在两边都修改了,如何解决冲突?
避免冲突:首先我们通过全局定义的规则避免数据冲突,仔细设计的数据规则,让每笔数据都有自己的归属机房,两个机房同时修改一笔数据的情况很少出现。两个机房产生的数据在 ID 上是错开的,各种和业务相关的ID 也通过设计避免了重复,这样数据复制到一起后,不会发生冲突。对于有唯一键索引的数据,我们也进行了改造,加上了用于区别机房的数据字段。
冲突解决:即使如此,有时候冲突还是不可避免的,比如发生机房切换,或者是业务方的代码有Bug等,所以我们还提供2层冲突解决方案,万一发生同一笔数据,在两个机房同时修改,则引入冲突处理:
-
基本的冲突处理,通过时间戳完成,两边机房的都有实时同步的毫秒级别的NTP服务,每笔数据上都打上了变更时间戳。在发生冲突的时候,最后发生的修改会胜出,最终两个机房的数据都会被同步成最后的数据。
-
如果时间戳不能满足需要,还可以通过调用业务提供的冲突解决方案解决,冲突解决时,为业务方提供了原始数据和最新数据,由业务逻辑来决定哪个数据才是最终正确的数据。
-
但是我们不会对数据进行合并,因为合并带来的问题比较多,事实上看,基于时间戳已经解决了99%的数据冲突问题。
-
性能优化
DRC 具有非常高的吞吐量,主要归功于Replicator的本地EventBuffer,几个月的Event数据都会被缓冲到 EventBuffer 中,EventBuffer 是一个跳表结构,如下图:
其中跳表的索引是SCN,通过SCN能够快速的找到对应Event,之后按照顺序输出其后的 Event,每次输出一批Event,磁盘读取和网络传输都很高效。
EventBuffer 落地在磁盘上,通过内存映射Map到内存中。Java Heap 中只保存比较少的索引数据,大量的Event数据维持在堆外,避免大内存带来的GC开销。大部分 EventBuffer 的大小维持在512G空间,能够支持数月到数个星期的事件。
EventBuffer 中只保存了二进制的数据,数据的结构被保存在另外一个独立的存储中,我们称为MetaData Store。MetaData Store 保存了每个表的历史数据格式的快照,每次发生表结构变化,都会创建一个新的快照,结构如下:
要点说明:
-
Meta History 记录了数据结构以及对应的 SCN
-
通过 Meta History,可以回放任意时间点的数据
-
通过 Meta Histroy 的翻译,可以把数据翻译成业务需要的格式,或者组装成对应的SQL