饿了么异地多活技术实现(四)- 实时双向复制工具(DRC)

饿了么异地多活技术实现(四)- 实时双向复制工具(DRC)

DRC介绍

饿了么的 Data Replicate Center(DRC)项目用于数据双向复制和数据订阅,使用场景如下图:

要点说明:

  • 跨机房的 Mysql 数据复制完全通过 DRC 来完成
  • 还有很多业务团队通过 DRC 来实现数据订阅

目前饿了么100%的跨机房数据复制,90%的数据订阅都通过DRC完成,每天有大量的数据流经DRC。复制延迟保持在1s以下,从来没有发生过数据丢失和错乱的情况。


DRC的设计目标包括:

  1. 实时双向数据复制,延时 < 1s ,并能够解决双向修改时的数据冲突
  2. 数据变更订阅,能够在DB数据发生变化时通知到相关订阅方
  3. 高度可靠和保持顺序,不能丢失数据,也不能因为错乱执行顺序导致数据错误

我们最终达到了这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

以上主要介绍了DRC 的一些实现细节,DRC 作为一个核心的中间件,还有很多其他的设计,例如集群管理,高可用性设计,性能优化等,但考虑到篇幅,就不在这里全部写出来了。有兴趣的朋友可以可留言和我联系,大家共同探讨。


作者介绍:

李双涛,饿了么框架工具部高级架构师,参与了整个饿了么多活的设计和实施过程。职业生涯中,有两次参与这种大规模的整站高可用改造,一次在 Cisco Webex,一次在饿了么,积累了一些高可用和异地多活方面的经验。

编辑于 2018-03-26

文章被以下专栏收录