千万级日订单下,饿了么异地多活数据实施DRC的应用实践
今天,我主要分享饿了么多活的底层数据实施,和大家介绍在整个多活的设计和实施过程中,我们是怎么处理异地数据同步的,而这个数据同步组件在我们公司内部称之为 DRC。
饿了么异地多活背景
在讲 DRC 或者讲数据复制之前,先跟大家回顾一下异地多活的背景。
去年,我们在做多活调研的时候,整个公司所有的业务服务都是部署在北京机房,服务器大概有四千多台,灾备的机器是在云端,都是虚拟机,大概有三千多台。
当时,我们峰值的业务订单数量已经接近了千万级别,但是基本上北京机房(IDC)已经无法再扩容了,也就是说我们没有空余的机架,没有办法添加新的服务器了,必须要再建一个新的机房。
于是,我们在上海新建一个机房,在今年的 4 月份投入使用,所以在上海机房建成之后,异地多活项目能具备在生产环境上进行灰度。
异地多活的底层数据同步实施
这是异地多活的底层数据同步实施的一个简单的概要图,大家可以看到,我们有两个机房,一个是北京机房,一个是上海机房。
在这个时候,我们期望目标是北方所有的用户请求、用户流量全部进入北京机房,南方所有的用户请求、用户流量进入上海机房。
困难的地方是,这个用户有可能今天在北方,明天在南方,因为他在出差,还有就是存在一些区域在我们划分南北 shard 的时候,它是在边界上面的,这种情况会加剧同一个用户流量在南北机房来回漂移的发生。
还有个情况,当我们某个机房出现故障,如核心交换机坏掉导致整个机房服务不可用,我们希望可以把这个机房的所有流量快速切到另外的数据中心去,从而提高整个饿了么服务的高可用性。
以上所有的因素,都需要底层数据库的数据之间是打通的。而今天我所要分享的 DRC 项目就是饿了么异地 MySQL 数据库双向复制的组件服务,即上图中红色框标记的部分。
异地多活对底层数据的要求
我们在前期调研 DRC 实现的时候,主要总结了的三点,而在后续的设计和实施当中,基本上也是围绕这三点来去解决问题:
我们觉得是延迟要低,当时给自己定的目标是秒级的,我们希望在北京机房或上海机房写入的数据,需要在 1 秒钟之内同步到上海或者北京机房。整个延迟要小于 1 秒钟。
我们要确保数据的一致性,数据是不能丢也不能错的,如果出现数据的不一致性,可能会给上层的业务服务、甚至给产品带来灾难性的问题。
保证整个复制组件具备高吞吐处理能力,指的是它可以面对各种复杂的环境,比方说业务正在进行数据的批量操作、数据的维护、数据字典的变更情况。
这些会产生瞬间大量的变更数据,DRC 需要面对这种情况,需要具备高吞吐能力去扛住这些情况。
数据低延迟和一致性之间,我们认为主要从数据的并发复制这个策略上去解决,安全、可靠、高效的并发策略,才能保证数据是低延迟的复制,在大量数据需要复制时,DRC 并发处理才能快速在短时间内解决。
数据一致性,用户的流量可能被路由到两个机房的任何一个机房去,也就是说同样一条记录可能在两个机房中被同时更改,所以 DRC 需要做数据冲突处理,最终保持数据一致性,也就是数据不能出错。
如果出现冲突且 DRC 自身无法自动处理冲突,我们还提供了一套数据冲突订正平台,会要求业务方一道来制定数据订正规则。
高吞吐刚才已经介绍了,正常情况用户流量是平稳的,DRC 是能应对的,在 1 秒钟之内将数据快速复制到对端机房。
当 DBA 对数据库数据进行数据归档、大表 DDL 等操作时,这些操作会在短时间内快速产生大量的变更数据需要我们复制,这些数据可能远远超出了 DRC 的最大处理能力,最终会导致 DRC 复制出现延迟。
所以 DRC 与现有的 DBA 系统需要进行交互,提供一种弹性的数据归档机制,如当 DRC 出现大的复制延迟时,终止归档 JOB,控制每轮归档的数据规模。
如 DRC 识别属于大表 DDL 产生的 binlog events,过滤掉这些 events,避免这些数据被传输到其他机房,占用机房间带宽资源。
以上是我们在实施异地多活的数据层双向复制时对 DRC 项目提出的主要要求。
数据集群规模(多活改造前)
这是我们在做多活之前的北京数据中心的数据规模,这个数据中心当时有超过 250 套 MySQL 的集群,一千多台 MySQL 的实例,Redis 也超过四百个集群。
DRC 服务的目标对象就是这 250 套 MySQL 集群,因为在正在建设的第二个数据中心里未来也会有对应的 250 套 MySQL 集群,我们需要把两个机房业务对等的集群进行数据打通。
多活下 MySQL 的用途分类
我们按照业务的用途,给它划分了多种 DB 服务类型。为什么要总结这个呢?因为有一些类型,我们是不需要复制的,所以要甄别出来,首先第一个多活 DB,我们认为它的服务需要做多活的。
比方说支付、订单、下单,一个机房挂了,用户流量切到另外新的机房,这些业务服务在新的机房是工作的。
我们把这些多活服务依赖的 DB 称为多活 DB,我们优先让业务把 DB 改造成多活 DB,DRC 对多活 DB 进行数据双向复制,保障数据一致性。
多活 DB 的优势刚才已经讲了,如果机房出现故障、核心交换机出问题,整个机房垮了,运维人员登不进机房机器,那么我们可以在云端就把用户流量切到其它的机房。
有些业务对数据有强一致性要求,后面我会讲到其实 DRC 是没有办法做到数据的强一致性要求的,它是有数据冲突发生的,需要引入数据订正措施。
业务如果对数据有强一致性要求,比方说用户注册,要求用户登录名全局唯一(DB字段上可能加了唯一约束),两个机房可能会在同一时间接收了相同用户登录名的注册请求。
这种情况下,DRC 是无法自身解决掉这个冲突,而且业务方对这个结果也是无法接受的,这种 DB 我们会把它归纳到 GlobalDB 里面,它的特性是什么呢?
它的特性是单机房可写,多机房可读,因为你要保证数据的强一致性的话,必须让所有机房的请求处理结果,最终写到固定的一个机房中。
这种 DB 的上层业务服务,在机房挂掉之后是有损的。比方说机房挂了,用户注册功能可能就不能使用了。
最后一个非多活 DB,它是很少的,主要集中于一些后端的管理平台,这种项目本身基本上不是多活的,所以这种 DB 我们不动它,还是采用原生的主备方式。
DRC 总体架构设计
这是 DRC 复制组件的总体架构设计。我们有一个组件叫 Replicator,它会从 MySQL 集群的 Master 上把 binlog 日志记录抽取出来,解析 binlog 记录并转换成我们自定义的数据,存放到一个超大的 event buffer 里面,event buffer 支持 TB 级别的容量。
在目标机房里,我们会部署一个 Applier 服务,这个服务启一个 TCP 长连接到 Replicator 服务,Replicator 会不断的推送数据到 Applier,Applier 通过 JDBC 最终把数据写入到目标数据库。
我们会通过一个 Console 控制节点来进行配置管理、部署管理以及进行各个组件的 HA 协调工作。
DRC Replicator Server
这是 DRC Replicator Server 组件比较细的结构描述,主要是包含了一个 MetaDB 模块,MetaDB 主要用来解决历史的 Binlog 的解析问题。
我们成功解析 Binlog 记录之后,会把它转换成我们自己定义的一种数据结构,这种结构相对于原生的结构,Size 更小,MySQL binlog event 的定义在 Size 角度上考虑事实上已经很极致了。
但是可以结合我们自己的特性,我们会把不需要的 event 全部过滤掉(如table_map_event),把可以忽略的数据全部忽略掉。我们比对的结果是需要复制的 event 数据只有原始数据 Size 的 70%。
DRC Applier Server
往目标的 MySQL 集群复制写的时候,由 DRC Applier Server 负责,它会建一个长连接到 Replicator 上去,Replicator PUSH 数据给 Applier。
Applier 把数据拿到之后做事务的还原,最后通过 JDBC 把事务重新写到目标 DB 里面,写的过程当中,我们应用了并发的策略。
并发策略在提供复制吞吐能力,降低复制延迟上起到决定的作用,还有幂等也是非常重要的,后面有很多运维操作,还有一些 Failover 回退操作,会导致发生数据被重复处理的情况,幂等操作保障重复处理数据不会发生问题。
DRC 防止循环复制
在做复制的时候,大家肯定会碰到解决循环复制的问题。我们在考虑这个问题的时候,查了很多资料,也问了很多一些做过类似项目的前辈,当时我们认为有两大类办法。
第一大类办法一开始否决了,因为我们对 MySQL 的内核原码不熟悉,而且时间上也来不及,虽然我们知道通过 MySQL 的内核解决回路复制是最佳的、最优的。
靠 DRC 自身解决这个问题,也有两种办法:
一种办法是我们在 Apply 数据到目标 DB 的时候把 binlog 关闭掉。
另外一种办法就是写目标 DB 的时候在事物中额外增加 checkpoint 表的数据,用于记录源 DB的server_id。
后来我们比较了一下,第一个办法是比较简单,实现容易,但是因为 Binlog 记录没有产生,导致不支持级联复制,也对后续的运维带来麻烦。
所以我们最后选择的是第二个办法,通过把事务往目标 DB 复制的时候,在事务中 hack 一条 checkpoint 的数据来标识事务产生的原始 server,DRC 在解析 MySQL binlog 记录时就能正确分辨出数据的真正来源。
DRC 数据一致性保障
在刚开始研发、设计的时候,数据一致性保障是我们很头疼的问题。并不是在一开始就把所有的点都想全了,是在做的过程当中出现了问题,一步步解决的,回顾一下,我们大概从三个方面去保证数据的一致性:
首先,因为数据库是多活的,我们必须从数据中心层面尽可能把数据冲突发生的概率降到最低,避免冲突,怎么避免呢?就是合理的流量切分,你可以按照用户的维度,按照地域的维度,对流量进行拆分。
刚才我们讲的,北方用户的所有数据在北京机房,这些北方用户的下单、支付等的所有操作数据都是在北方机房产生的,所以用户在同一个机房中发生的数据变更操作绝对是安全的。
我们最怕的是同一个数据同时或者是在相近的时间里同时在两个机房被修改,我们怕的是这个问题,因为这种情况就会引发数据冲突。所以我们通过合理的流量切分,保证绝大部分时候数据是不会冲突的。
第二个我们认为你要保障数据一致性,首先你要确保数据不丢,一旦发生可能数据丢失的情况,我们会做一个比较保险的策略,就是把数据复制的时间位置回退,即使重复处理数据,也避免丢数据的可能。
但是这个时候会带来数据重复处理的问题,所以数据的幂等操作特别重要。
这些都是我们避免数据发生冲突的方法,那冲突实际上是不可避免的,冲突发生后,我们怎么解决?
最终采用的办法是在数据库表上隐含地加一个时间字段(数据最后更新时间),这个字段对业务是透明的,主要用来辅助 DRC 复制。
一旦数据发生冲突,DRC 复制组件可以通过这个时间来判断两个机房或者三个机房中的哪条数据是最后被更新的,最新优先的原则,谁最后的修改时间是最新的,就以它为准。
DRC 数据复制低延迟保障
刚才我们讲的是数据的一致性,还有一个点非常重要,就是数据复制的低延迟保障。我们现在延迟包括用户高峰时间也是小于 1 秒的,只有在凌晨之后,各种归档、批量数据处理、DDL 变更等操作会导致 DRC 延迟出现毛刺和抖动。
如果你的延迟很高的话,第一在做流量切换时,因为运维优先保障产品服务的可用性,在不得以的情况会不考虑你的复制延迟,不会等数据复制追平之后再切流量,所以你的数据冲突的概率就变的很大。
为了保证复制低延迟,我们认为主要策略、或者你在实施时主要的做法还是并发,因为你只有用高效的安全的并发复制策略,服务才有足够的吞吐处理能力,而不至于你的复制通道因为遇到“海量”数据而导致数据积压,从而加剧了复制延迟的产生。
我们一开始采用的基于表级别的并发,但是表级别的并发在很多情况下,并发策略没办法被有效的利用。
比方说有的业务线的数据库可能 90% 的数据集中在一张表或者是几个表里面,而大部分表数据量很小,那基于表的并发策略就并发不起来了。我们现在跑的是基于行级别的并发,这种并发它更能容忍和适应很多场景。
DRC & MySQL Master切换
这个是 DRC 复制组件与 MySQL 集群的关系关联图,一旦 MySQL 集群里面的 Master 发生了主备切换,原来的 Master 挂了,DRC 怎么处理?
目前的解决方案是 DBA 系统的 MHA 工具会通知 DRC 控制中心,DRC 的控制中心会找到对应的复制链路,然后把复制链路从老的 Master 切到新的 Master。
但是关键点是 MHA 在通知之前先把老的 Master 设置为不可写,阻断 DRC 可能往老的 Master 继续写数据。
DRC 线上运行状况(规模)
这个是我们 DRC 上线之后的运行状况。现在大概有将近 400 多条复制链路。这个复制链路是指单向的链路。我们提供的消息订阅大概有 17 个业务方接入,每天产生超过 1 亿条的消息。
DRC 线上运行状况(性能)
这是 DRC 线上运行的一个性能监控快照,我们可以看到,它是上午 11 点多到 12 点多的一个小时的性能,你会发现其实有一个 DB 是有毛刺的,有一个复制链路有毛刺,复制延迟最高达到 4s,但是大部分的复制链路的延迟大概也是在 1 秒或 1 秒以下。
作者:陈永庭
本文根据陈永庭老师在〖DAMS 2017中国数据资产管理峰会〗现场演讲内容整理而成。
选自DBAplus社群微信公众号。
陈永庭
饿了么框架工具部高级架构师
主要负责 MySQL 异地双向数据复制,支撑饿了么异地多活项目。曾就职于 WebEx、Cisco、腾讯等公司。
精彩文章推荐: