异地多活:MySQL实时双向(多向)复制实践 - MySQL

标签: | 发表时间:2020-07-04 19:50 | 作者:
出处:https://dbaplus.cn

 

一、前言

携程内部MySQL部署采用多机房部署,机房A部署一主一从,机房B部署一从,作为DR(Disaster Recovery)切换使用。当前部署下,机房B部署的应用需要跨机房进行写操作;当机房A出现故障时,DBA需要手动对数据库进行DR切换。

 

为了做到真正的数据异地多活,实现MySQL同机房就近读写,机房故障时无需进行数据库DR操作,只进行流量切换,就需要引入数据实时双向(多向)复制组件。

 

 

二、DRC 介绍

 

DRC(Data Replicate Center)是携程框架架构研发部推出的用于数据双向或多向复制的数据库中间件,在公司G2(高品质Great Service、全球化Globalization)战略的背景下,服务于异地多活项目,赋予了业务全球化的部署能力。

 

三、DRC 架构设计

 

DRC采用服务端集中化设计,配合另一数据库访问中间件DAL(Data Access Layer)的本地读写功能,实现数据就近访问。

 

 

模块介绍 

 

1)Replicator Container

 

Replicator Container 实现对 Replicator 实例的管理,一个 Replicator 实例表示对一个MySQL集群的复制单元,Instance将自己伪装为MySQL的Slave,实现Binlog的拉取和本地存储。

 

2)Applier Container

 

Applier Container实现对Applier 实例的管理,一个Applier 实例连接到一个Replicator 实例,实现对Replicator 实例本地存储Binlog的拉取,进而解析出SQL语句并应用到目标MySQL,从而实现数据的复制。

 

3)Cluster Manager

 

Cluster Manager负责集群高可用切换,包括由于MySQL主从切换导致的Replicator 实例和Applier 实例重启,以及Replicator 实例与Applier 实例自身主从切换引起的新实例启动通知。

 

4)Console

 

Console提供UI操作、外部系统交互API以及监控告警。

 

四、DRC 详细设计

 

1、接入DB规范 

 

DRC的核心指标包括复制延迟和数据一致性。

 

为了实现数据复制的低延迟,Applier能够快速应用SQL,就需要每个表至少包含主键或者唯一键,加速执行效率;同时在保证数据准确的前提下,SQL应该尽量并行复制,需要MySQL开启从5.7.22版本引入的Writeset功能。

 

为了保证数据复制的准确性,在主备切换时Replicator仍能准确定位Binlog位点,需要MySQL开启GTID;当数据复制发生冲突时,为了具备自动解决冲突的能力,需要表包含时间戳列,并精确到毫秒。

 

这就需要接入DRC的MySQL数据库满足:

 

  • 5.7.22及以上版本;

  • Master上开启Writeset并行复制;

  • MySQL开启GTID;

  • 每个表包含时间戳列,精确到毫秒;

  • 每个表至少包含主键或者唯一键。

 

DRC的复制依赖GTID(Global Transaction ID),这里先简单介绍一下GTID的概念。MySQL 5.6.5版本新增了一种基于GTID的复制方式,强化了数据库的主备一致性,故障恢复以及容错能力,取代传统的基于file和position主从复制,使得在MySQL主备切换时,仍能准确定位到Binlog位点。

 

GTID的格式形如:source_id:transaction_id,其中source_id表示MySQL服务器的uuid,transaction_id是在事务提交的时候系统顺序分配的一个序列号。

 

2、Binlog 复制 

 

单向复制链路包含拉取Binlog并持久化到本地磁盘的Replicator,和请求Binlog且并行应用到目标MySQL的Applier。整个链路涉及的I/O操作包括网络传输和磁盘读写。

 

 

1)低复制延迟

 

为了降低复制延迟,就要求复制链路中每一环都尽可能高效。网络层通信模型使用异步I/O;系统层尽可能使用操作系统提供的Zero Copy和Page Cache;应用层提高数据处理并行度以及降低系统不可用时间。

 

监控显示生产环境业务双向复制延迟999线 < 1s。下面就介绍一下DRC在降低复制延迟方面所做的性能优化工作。

 

 

① 网络层

 

Replicator采用GTID复制方式,实现了MySQL复制协议,伪装成源MySQL的Slave拉取Binlog。网络层通信组件采用携程开源组件XPipe,实现网络交互异步化。

 

XPipe:https://github.com/ctripcorp/x-pipe

 

② 系统层

 

接收Binlog时,从数据流中解析出不同类型的Event,直接保存在堆外内存。每个Event需要经过一组过滤器,进而决定是否需要落盘持久化。对于Heartbeat类型的Event需要过滤丢弃;针对某些不需要进行数据同步的库和表,需要丢弃相应Event,减少存储量和传输量;对于需要持久化的Event,直接将堆外内存中的数据写入文件Page Cache并定时刷入磁盘,减少数据复制和IO操作,降低处理耗时,提升Replicator拉取效率。

 

发送Binlog时,当Applier进度落后Replicator,需要从磁盘读取,这时只解析gtid_event事件,其他需要发送的事件直接从磁盘读取到堆外内存进行发送,减少数据复制。

 

③ 应用层

 

Applier借鉴原生MySQL基于Writeset的并行复制,内嵌了基于水位的并行算法,高效的将SQL应用到目标数据库。

 

除去正常复制之外,为了降低系统的不可用时间,就需要系统在异常情况下,尽快恢复正常功能。比如断网恢复时,为了避免一端使用老连接,就需要对连接进行空闲检测;为了应对断网导致数据堆积出现流量突增,就需要对流量进行控制。

 

④ 空闲检测

 

Replicator与MySQL、Applier和Replicator通过Netty进行数据传输,当网络出现故障,可能一端仍然使用老连接进行通信,会导致数据复制出现中断。

 

针对网络故障,Replicator对MySQL添加了读空闲检测,启动时设置MySQL空闲时间隔10s发送一次heartbeat_event,如果30s没有收到MySQL任何事件,则认为MySQL出现问题,发起重连。

 

Replicator对Applier设置了写空闲检测,当没有Event需要发送给Applier时,间隔10s发送一次heartbeat_event,如果发送失败,则认为Applier出现问题,断开连接。

 

Applier对Replicator设置了读空闲检测,如果30s没有收到Replicator任何事件,则认为Replicator出现问题,发起重连。

 

⑤ 流量控制

 

设计上Replicator Container使用物理机,其中会运行若干Replicator实例,Applier Container使用虚拟机,这样会造成发送和消费的速率不匹配。尤其当Applier由于某种原因出现故障后,在Replicator端堆积大量未消费的Event,重启后如果堆积的Event全部发送过来,可能会直接打垮Applier,这样就需要在Replicator实例上对Applier进行限流。

 

Replicator发送端使用Netty提供的WRITE_BUFFER_WATER_MARK高低水位的变化来控制流控的开关,进而动态调整发送速率,整形平滑流量。

 

2)数据一致性

 

为了保证数据的一致,就需要满足:

 

  • 数据拉取时保证时序;

  • 数据拉取不能遗漏,SQL应用时不重,或者即使重复,要保证幂等操作,保证At Least Once;

  • 数据冲突时,能正确处理,保证数据最终一致。

 

下面就看下DRC是如何保证以上3个要求。

 

① 时序保证

 

本地磁盘保存Binlog采用原生的存储协议,Replicator顺序处理接收到每一个Event事件。存储协议兼容MySQL原生的mysqlbinlog命令,其中根据DRC自身的需要,保存了自定义的一些辅助事件,比如DDL事件,表结构事件。消费时顺序发送Binlog文件中的事件给Applier。

 

 

② At Least Once

 

为了实现At Least Once,需要解决3个子问题:

 

  • Replicator或者Applier重启时,如何保证请求的GTID set准确体现目前的消费偏移?

  • 双向(多向)复制如何解决循环复制?

  • Applier由于异常重复拉取时,如何保证幂等?

 

下面逐一介绍每个子问题的解决方案。

 

断点重续

 

当Replicator重启时,会从本地磁盘中恢复已经拉取过的GTID set:

 

  • 定位重启前使用的最后一个Binlog文件;

  • 解析出previous_gtids_event;

  • 遍历该文件的所有gtid_event,与previous_gtids_event解析出的GTID set取并集。

 

恢复过程中,会校验文件的正确性,对于没有以xid_event结束的事务,Replicator会对文件进行截断,对应的gtid事务会重新请求。

 

当Applier重启时,Cluster Manager会从目标数据库中查询出当前已经执行过的GTID set发送给Applier,Applier带着该参数向Replicator发送Binlog拉取请求。Replicator收到请求中的GTID set,从本地磁盘中定位出第一个需要发送的Event所在的Binlog文件,依次遍历该文件中的每一个Event,针对gtid_event事件取出其中的gtid,判断该gtid对应的事务是否包含在GTID set中,如果包含其中,则表示Applier已经消费过,无需发送,否则通过堆外内存直接将Event发送给Applier。

 

循环复制

 

单向复制时,经过DRC复制到对端的SQL在执行后,同样会落到MySQL的Binlog中,这样在双向(多向)复制结构中,对端的Replicator Instance在拉取到该条Binlog后如果继续复制,就会出现循环复制的问题。

 

针对循环复制,业内可选的解决方案是在Binlog事务开头插入一条写操作,标识出该条事务是DRC复制过来,而不是真实业务写入,这样对端Replicator发现一个事务开头包含DRC特殊标记时,就不会继续复制该事务。

 

分析MySQL自身主从复制,Slave在收到Master同步过来的Binlog时,通过set gtid_next将该事务的GTID设置为同步过来的gtid_event中的GTID,这样就实现了主从GTID set的一致性。

 

如果将Replicator拉取Binlog类比为Slave的I/O线程,磁盘文件类比为Relay log,Applier类比为Slave的SQL线程,那么Applier是可以采用同样的方式,使用set gtid_next设置经过DRC复制到对端事务的GTID,这样源和目标数据库的GTID set会保持一致,更重要的是可以标识出该事务是经DRC复制过来的。这也是DRC最终采用的破解循环复制的方案。

 

如下双向复制结构,Replicator Instance1只会同步源MySQL集群uuidSet1中的服务器产生事务,Replicator Instance2只会同步目标MySQL集群uuidSet2中的服务器产生事务。如果业务在源MySQL集群写入一条数据,Replicator Instance1从gtid_event中的GTID解析出uuid属于uuidSet1,那么会持久化到磁盘并发送给Applier Instance1,Applier Instance1接收到事务中包含的所有Event后,执行set gtid_next=GTID,然后通过JDBC将SQL写入目标MySQL,完成单向复制;Replicator Instance2接收到gtid_event后,同样解析出GTID,但是uuid并不属于uuidSet2,这样该条事务就会被过滤,从而避免的循环复制。

 

 

幂等

 

Applier如果重复接收到相同GTID的事务,由于MySQL会记录已经执行的GTID set,如果该GTID已经被执行,则会自动忽略,这样即使Applier重复应用同一条事务,也不会对业务产生影响。

 

小结

 

从上面可以看到,在保证数据一致性时,GTID不论是在Replicator和Applier重启后Binlog位点定位,标识Binlog来源避免循环复制,还是Applier重复应用时幂等实现,都起到了至关重要的作用。

 

③ 冲突解决

 

设计上,首先要避免冲突的出现:

 

  • 接入Set化的业务在流量入口处就会根据uid进行分流,同一个用户的流量进入同一个机房;数据接入层中间件DAL同样会采用local-2-local的路由策略。这样同一条记录在2个机房同时被修改的情况很少发生;

  • 对于使用自增ID的业务,通过不同机房设置不同的自增ID规则,或者采用分布式全局ID生成方案,避免双向复制后数据冲突。

 

如果数据确实出现了冲突,2个机房对同一条数据进行的修改,这时需要根据冲突处理策略进行处理:

 

  • Applier根据默认的冲突处理策略进行处理,接入DRC的表都有一个精确到毫秒自动更新的时间戳,冲突时时间戳靠后的会被采用,进而实现数据的一致;

  • 冲突的SQL会被监控记录,连同数据库中的原始数据同时提供给用户,进而自助决定是否需要进行覆盖。

 

3、DDL 支持 

 

DDL操作会引起表结构的变更,在复制链路中Applier需要表结构信息解析对应时刻的Binlog Event,当Applier消费速率落后Replicator的发送速率时,就需要历史版本的表结构信息才能够正确解析Binlog Event。这就引入了表结构设计第一个问题:历史版本如何存储?

 

为了存储表结构,势必首先要获得表结构,如果从源MySQL直接抓取表结构,由于Binlog是异步发送,就导致抓取到DDL的Binlog时刻,与MySQL上表结构未必能够一一对应,从而引起Applier解析出现问题,进而导致数据不一致。这就引入表结构设计第二个问题:表结构从何处抓取?

 

业界通用的解决方案是基于独立的第3方数据库进行表结构单独存储管理。数据库本身就是存储工具,Snapshot表和DDL表分别保存表结构快照和DDL变更记录,这样任意时刻的表结构等于Snapshot及其后DDL变更集合,则第一个表结构存储问题顺其自然得以解决;独立数据库镜像一份源数据库的库表结构,每次从Binlog接收到DDL Event后,将解析出的DDL语句直接应用到镜像数据库,随即抓取相应表结构即可,这样就解决了第二个表结构从何处抓取的问题。

 

 

独立数据库解决方案的缺点是引入外部依赖,降低了系统的可用性,提高了运维成本。

 

1)表结构存储和计算

 

针对DDL功能中问题一:

 

从数据库中查询Snapshot和DDL记录的好处是时间顺序容易确定,能够简单准确的恢复表结构。那么是否有其他存储介质,在保存表结构快照和DDL操作的同时,能够保证时序呢?有,保存Binlog的文件就具有这种特性,DRC采用了这种基于Binlog的表结构文件存储方案。

 

针对DDL功能中问题二:

 

镜像数据库是为了实时计算出DDL变更后最新的表结构信息,在存储不使用独立部署的数据库后,DRC引入嵌入式轻量数据库,降低外部依赖和系统运维成本。

 

这样整体的设计方案如下图所示:

 

 

Binlog文件头会保存自定义表结构快照事件,当从接收的Event事件检测到DDL后,保存为自定义的DDL事件。这样当Applier连接上Replicator后,总是会根据GTID set定位到需要的第一个历史版本表结构所在的文件,从而实时恢复表结构历史,用于后续Binlog Event的解析。

 

我们将数据库最小依赖打成独立的Jar包服务,每个Replicator实例启动时,会一并启动一个独立的嵌入式数据库,在恢复GTID set的同时,根据表结构快照事件和DDL事件重建嵌入式数据库中表结构。

 

2)DDL 入口

 

携程内部发布DDL是通过gh-ost进行变更,gh-ost会在影子表中执行DDL操作,等影子表中数据同步完成后,业务低峰期进行原表和影子表的切换。

 

针对gh-ost,需要追踪gh-ost变更过程中内部形如_xxx_gho的表的DDL所有操作,最终执行切换时检测出rename操作,保存对应表结构最新信息发送给Applier即可。

 

同时针对数据库直接进行的DDL操作,直接检测出DDL类型的Event即可。

 

3)DDL 异常处理

 

对于接入DRC的数据库,当在进行DDL变更时,可能会出现两边数据库变更不同步,单侧进行了DDL变更,另一侧未进行变更。针对新增列这种场景,Applier在保证数据一致的前提下,对新增列的值进行比较,如果Binlog中解析出的值和该列的默认值一致,则会剔除该列,继续数据复制。这样在另一侧补上DDL变更后,两侧的数据最终仍然一致。

 

4)监控告警

 

DRC核心指标包括复制延迟和数据一致性。除此之外我们还提供BU、应用和IDC维度的监控:

 

  • 流量和TPS监控告警;

  • BU、应用和IDC维度的监控告警;

  • DDL变更监控;

  • 表结构一致性监控告警;

  • 数据冲突监控;

  • GTID set GAP监控。

 

五、总结

 

本次分享围绕DRC的核心指标复制延迟和数据一致性,介绍了复制过程中对性能的优化以及各种场景如何保证数据的一致性。针对DDL,分别支持gh-ost和直接DDL操作,实现在线表结构变更不影响数据复制。

 

后续DRC的工作会集中在高可用、海外支持上以及外围设施的建设上,为携程的国际化战略提供数据层面的支撑。

 

相关 [mysql 实时 复制] 推荐:

异地多活:MySQL实时双向(多向)复制实践 - MySQL

- -
携程内部MySQL部署采用多机房部署,机房A部署一主一从,机房B部署一从,作为DR(Disaster Recovery)切换使用. 当前部署下,机房B部署的应用需要跨机房进行写操作;当机房A出现故障时,DBA需要手动对数据库进行DR切换. 为了做到真正的数据异地多活,实现MySQL同机房就近读写,机房故障时无需进行数据库DR操作,只进行流量切换,就需要引入数据实时双向(多向)复制组件.

MySQL添加Hadoop数据实时复制功能

- - searchdatabase
  MySQL复制操作可以将数据从一个MySQL服务器(主)复制到其他的一个或多个MySQL服务器(从). 试想一下,如果从服务器不再局限为一个MySQL服务器,而是其他任何数据库服务器或平台,并且复制事件要求实时进行,是否可以实现呢?.   MySQL团队最新推出的 MySQL Applier for Hadoop(以下简称Hadoop Applier)旨在解决这一问题.

mysql主从复制

- - SQL - 编程语言 - ITeye博客
从库的配置,mysql5.5不支持配置文件的配置了,问了数据库的人,用命令行指定. 修改从库的配置 #default-storage-engine = InnoDB #修改 default-storage-engine = blackhole server-id = 11215004 #新增 replicate-do-db = test log-bin = mysql-bin #新增 binlog_format = row.

MySQL主从复制配置

- - 天空极速
在实际企业应用环境当中,单台MySQL数据库是不足以满足日后业务需求的. 譬如服务器发生故障,没有备份服务器来提供服务的话,业务就得停止. 使用MySQL主从复制的好处有:. 1、采用主从服务器这种架构,稳定性得以提升. 如果主服务器发生故障,我们可以使用从服务器来提供服务;. 2、在主从服务器上分开处理用户的请求,可以提升数据处理效率;.

MySQL 5.6 测试之 Replication(主从复制)

- - MySQL 中文网 -
MySQL 5.6测试之Replication. MySQL 5.6版本相比以前新增了很多令人激动的特性,简要介绍见: 转:MySQL 5.6新特性. 性能方面已经做过测试了,详细请见: MySQL 5.6 vs MariaDB 5.5 vs Percona(5.5 & 5.6) 之TPCC性能测试.

MySQL半同步复制(Semisynchronous Replication)

- - IT技术博客大学习
MySQL5.5引入了半同步复制(Semi-synchronous Replication),以下是对于半同步复制的认知和理解:. 半同步启动需要主从两端都需要加载安装各自对应的semi模块,从库端支持半同步功能的数量至少一台;主库端当一个事务成功提交后,并不及时反馈给前端用户,该线程会被临时block,等待由从库端返回确认该条事务也同时成功写入到relay log中的receipt(回执确认),这时主库线程才返回给当前session告知操作完成,半同步复制并不关心在从库一端该事务是否都被执行并被提交完成.

[玩转MySQL Replication] 复制拓扑

- - CSDN博客数据库推荐文章
     朴实简单的才是真、那些高端洋气的复制拓扑纯属自虐.      实施复制大概会有 4 个原则:.      ① 一个主库可以有多个备库.      ② 一个备库只能有一个主库.      ③ 每个备库 Server ID全局唯一.      ④ log_slave_updates 有薪火相传之效用.

MySQL主从复制与读写分离

- - 数据库 - ITeye博客
MySQL主从复制与读写分离. MySQL主从复制(Master-Slave)与读写分离(MySQL-Proxy)实践. Mysql作为目前世界上使用最广泛的免费数据库,相信所有从事系统运维的工程师都一定接触过. 但在实际的生产环境中,由单台Mysql作为独立的数据库是完全不能满足实际需求的,无论是在安全性,高可用性以及高并发等各个方面.

MySQL数据库复制概论

- - Float_Luuu的博客
每当我们讨论一项(新的)领域技术的时候,最好的方式通常是首先抛出一些问题,这些问题大致分为三类:诶. 这项技术又是什么玩意(What)?这项技术为什么会存在. 我们已经有那么多解决方案(Method)了,我们问什么要用它(Why). 如果这项技术那么好且我们正好有场景可以用到这项技术,且能使我们的系统得到很乐观的优化,那么我们怎么用呢(How).