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