异地多活场景下,如何举行数据同步?

作者:leye乐鱼娱乐app发布时间:2022-01-08 02:57

本文摘要:文章泉源:https://dwz.cn/Xmghvn4n作者:田守枝在当今互联网行业,大多数人互联网从业者对单元化、异地多活这些词汇已经耳熟能详。而数据同步是异地多活的基础,所有具备数据存储能力的组件如:数据库、缓存、MQ等,数据都可以举行同步,形成一个庞大而庞大的数据同步拓扑。本文将先从观点上先容单元化、异地多活、就近会见等基本观点。 之后,将以数据库为例,解说在数据同步的情况下,如何解决数据回环、数据冲突、数据重复等典型问题。

leyu乐鱼全站app

文章泉源:https://dwz.cn/Xmghvn4n作者:田守枝在当今互联网行业,大多数人互联网从业者对"单元化"、"异地多活"这些词汇已经耳熟能详。而数据同步是异地多活的基础,所有具备数据存储能力的组件如:数据库、缓存、MQ等,数据都可以举行同步,形成一个庞大而庞大的数据同步拓扑。本文将先从观点上先容单元化、异地多活、就近会见等基本观点。

之后,将以数据库为例,解说在数据同步的情况下,如何解决数据回环、数据冲突、数据重复等典型问题。1 什么是单元化如果仅仅从"单元化”这个词汇的角度来说,我们可以明白为将数据划分到多个单元举行存储。

"单元"是一个抽象的观点,通常与数据中心(IDC)观点相关,一个单元可以包罗多个IDC,也可以只包罗一个IDC。本文假设一个单元只对应一个IDC。

思量一开始只有一个IDC的情况,所有用户的数据都市写入同一份底层存储中,如下图所示:这种架构是大多数据中小型互联网公司接纳的方案,存在以下几个问题: 1 差别地域的用户体验差别。一个IDC一定只能部署在一个地域,例如部署在北京,那么北京的用户会见将会获得快速响应;可是对于上海的用户,会见延迟一般就会大一点,上海到北京的一个RTT可能有20ms左右。

2 容灾问题。这里容灾不是单台机械故障,而是指机房断电,自然灾害,或者光纤被挖断等重大灾害。

一旦泛起这种问题,将无法正常为用户提供会见,甚至泛起数据丢失的情况。这并不是不行能,例如:2015年,支付宝杭州某数据中心的光缆就被挖断过;2018年9月,云栖大会上,蚂蚁金服就地把杭州两个数据中心的网线剪断。为相识决这些问题,我们可以将服务部署到多个差别的IDC中,差别IDC之间的数据相互举行同步。如下图:通过这种方式,我们可以解决单机房遇到的问题: 1 用户体验。

差别的用户可以选择离自己最近的机房举行会见 2 容灾问题。当一个机房挂了之后,我们可以将这个机房用户的流量调理到另外一个正常的机房,由于差别机房之间的数据是实时同步的,用户流量调理已往后,也可以正常会见数据 (故障发生那一刻的少部门数据可能会丢失)。需要注意的是,关于容灾,存在一个容灾级此外划分,例如:单机故障,机架(rack)故障,机房故障,都会级故障等。我们这里只讨论机房故障和都会故障。

机房容灾 : 上面的案例中,我们使用了2个IDC,可是2个IDC并不能具备机房容灾能力。至少需要3个IDC,例如,一些基于多数派协议的一致性组件,如zookeeper,redis、etcd、consul等,需要获得大部门节点的同意。例如我们部署了3个节点,在只有2个机房的情况下, 一定是一个机房部署2个节点,一个机房部署一个节点。当部署了2个节点的机房挂了之后,只剩下一个节点,无法形成多数派。

在3机房的情况下,每个机房部署一个节点,任意一个机房挂了,还剩2个节点,还是可以形成多数派。这也就是我们常说的"两地三中心”。

都会级容灾:在发生重大自然灾害的情况下,可能整个都会的机房都无法会见。一些组件,例如蚂蚁的ocean base,为了到达都会级容灾的能力,使用的是"三地五中心"的方案。这种情况下,3个都会划分拥有2、2、1个机房。

当整个都会发生灾难时,其他两个都会依然至少可以保证有3个机房依然是存活的,同样可以形成多数派。小结:如果仅仅是思量差别地域的用户数据就近写入距离最近的IDC,这是纯粹意义上的”单元化”。差别单元的之间数据实时举行同步,相互备份对方的数据,才气做到真正意义上"异地多活”。

实现单元化,技术层面我们要解决的事情许多,例如:流量调理,即如何让用户就近会见四周的IDC;数据互通,如何实现差别机房之间数据的相互同步。流量调理不在本文的讨论领域内,数据同步是本文解说的重点。2 如何实现数据同步需要同步的组件有许多,例如数据库,缓存等,这里以多个Mysql集群之间的数据同步为例举行解说,实际上缓存的同步思路也是类似。

2.1 基础知识为了相识如何对差别mysql的数据相互举行同步,我们先相识一下mysql主从复制的基本架构,如下图所示:通常一个mysql集群有一主多从组成。用户的数据都是写入主库Master,Master将数据写入到当地二进制日志binary log中。从库Slave启动一个IO线程(I/O Thread)从主从同步binlog,写入到当地的relay log中,同时slave还会启动一个SQL Thread,读取当地的relay log,写入到当地,从而实现数据同步。

基于这个配景知识,我们就可以思量自己编写一个组件,其作用类似与mysql slave,也是去主库上拉取binlog,只不外binlog不是生存到当地,而是将binlog转换成sql插入到目的mysql集群中,实现数据的同步。这并非是一件不行能完成的事,MySQL官网上已经提供好所有你自己编写一个mysql slave 同步binlog所需的相关配景知识,会见这个链接:https://dev.mysql.com/doc/internals/en/client-server-protocol.html,你将可以看到mysql 客户端与服务端的通信协议。下图红色框中展示了Mysql主从复制的相关协议: 固然,笔者的目的并不是希望读者真正的根据这里的先容实验编写一个mysql 的slave,只是想告诉读者,模拟mysql slave拉取binlog并非是一件很神奇的事,只要你的网络基础知识够扎实,完全可以做到。

然而,这是一个庞大而庞大的事情。以一人之力,要完成这个事情,需要占用你大量的时间。幸亏,现在已经有许多开源的组件,已经实现了根据这个协议可以模拟成一个mysql的slave,拉取binlog。例如:阿里巴巴开源的canal美团开源的pumalinkedin开源的databus ... 你可以使用这些组件来完成数据同步,而不必重复造轮子。

假设你接纳了上面某个开源组件举行同步,需要明确的是这个组件都要完成最基本的2件事:从源库拉取binlog并举行剖析,笔者把这部门功效称之为binlog syncer;将获取到的binlog转换成SQL插入目的库,这个功效称之为sql writer。为什么划分成两块独立的功效?因为binlog订阅剖析的实际应用场景并不仅仅是数据同步,如下图:如图所示,我们可以通过binlog来做许多事,如:实时更新搜索引擎,如es中的索引信息实时更新redis中的缓存发送到kafka供下游消费,由业务方自界说业务逻辑处置惩罚等... 因此,通常我们把binlog syncer单独作为一个模块,其只卖力剖析从数据库中拉取并剖析binlog,并在内存中缓存(或持久化存储)。另外,binlog syncer另外提一个sdk,业务方通过这个sdk从binlog syncer中获取剖析后的binlog信息,然后完成自己的特定业务逻辑处置惩罚。显然,在数据同步的场景下,我们可以基于这个sdk,编写一个组件专门用于将binlog转换为sql,插入目的库,实现数据同步,如下图所示:北京用户的数据不停写入离自己最近的机房的DB,通过binlog syncer订阅这个库binlog,然后下游的binlog writer将binlog转换成SQL,插入到目的库。

上海用户类似,只不外偏向相反,不再赘述。通过这种方式,我们可以实时的将两个库的数据同步到对端。固然事情并非这么简朴,我们有一些重要的事情需要思量。2.2 如何获取全量+增量数据? 通常,mysql不会生存所有的历史binlog。

原因在于,对于一条记载,可能我们会更新多次,这依然是一条记载,可是针对每一次更新操作,都市发生一条binlog记载,这样就会存在大量的binlog,很快会将磁盘占满。因此DBA通常会通过一些设置项,来定时清理binlog,只保留最近一段时间内的binlog。例如,官方版的mysql提供了expire_logs_days设置项,可以设置生存binlog的天数,笔者这里设置为0,表现默认不清空,如果将这个值设置大于0,则只会生存指定的天数。

另外一些mysql 的分支,如percona server,还可以指定保留binlog文件的个数。我们可以通过show binary logs来检察当前mysql存在几多个binlog文件,如下图:通常,如果binlog如果从来没被清理过,那么binlog文件名字后缀通常是000001,如果不是这个值,则说明可能已经被清理过。固然,这也不是绝对,例如执行"reset master”下令,可以将所有的binlog清空,然后从000001重新开始计数。

Whatever! 我们知道了,binlog可能不会一直保留,所以直接同步binlog,可能只能获取到部门数据。因此,通常的计谋是,由DBA先dump一份源库的完整数据快照,增量部门,再通过binlog订阅剖析举行同步。2.2 如何解决重复插入思量以下情况下,源库中的一条记载没有唯一索引。

对于这个记载的binlog,通过sql writer将binlog转换成sql插入目的库时,抛出了异常,此时我们并不知道知道是否插入乐成了,则需要举行重试。如果之前已经是插入目的库乐成,只是目的库响应时网络超时(socket timeout)了,导致的异常,这个时候重试插入,就会存在多条记载,造成数据纷歧致。因此,通常,在数据同步时,通常会限制记载必须有要有主键或者唯一索引。

2.3 如何解决唯一索引冲突 由于双方的库都存在数据插入,如果都使用了同一个唯一索引,那么在同步到对端时,将会发生唯一索引冲突。对于这种情况,通常建议是使用一个全局唯一的漫衍式ID生成器来生成唯一索引,保证不会发生冲突。另外,如果真的发生冲突了,同步组件应该将冲突的记载生存下来,以便之后的问题排查。

2.4 对于DDL语句如那边理如果数据库表中已经有大量数据,例如千万级别、或者上亿,这个时候对于这个表的DDL变换,将会变得很是慢,可能会需要几分钟甚至更长时间,而DDL操作是会锁表的,这一定会对业务造成极大的影响。因此,同步组件通常会对DDL语句举行过滤,不举行同步。

DBA在差别的数据库集群上,通过一些在线DDL工具(如gh-ost),举行表结构变换。2.5 如何解决数据回环问题数据回环问题,是数据同步历程中,最重要的问题。我们针对INSERT、UPDATE、DELETE三个操作来划分举行说明:INSERT操作假设在A库插入数据,A库发生binlog,之后同步到B库,B库同样也会发生binlog。

由于是双向同步,这条记载,又会被重新同步回A库。由于A库应存在这条记载了,发生冲突。

UPDATE操作先思量针对A库某条记载R只有一次更新的情况,将R更新成R1,之后R1这个binlog会被同步到B库,B库又将R1同步会A库。对于这种情况下,A库将不会发生binlog。因为A库记载当前是R1,B库同步回来的还是R1,意味着值没有变。

在一个更新操作并没有改变某条记载值的情况下,mysql是不会发生binlog,相当于同步终止。下图演示了当更新的值没有变时,mysql实际上不会做任何操作:上图演示了,数据中原本有一条记载(1,"tianshouzhi”),之后执行一个update语句,将id=1的记载的name值再次更新为”tianshouzhi”,意味着值并没有变换。

这个时候,我们看到mysql 返回的影响的记载函数为0,也就是说,并不会发生真是的更新操作。然而,这并不意味UPDATE 操作没有问题,事实上,其比INSERT越发危险。思量A库的记载R被一连更新了2次,第一次更新成R1,第二次被更新成R2;这两条记载变换信息都被同步到B库,B也发生了R1和R2。

由于B的数据也在往A同步,B的R1会被先同步到A,而A现在的值是R2,由于值纷歧样,将会被更新成R1,并发生新的binlog;此时B的R2再同步会A,发现A的值是R1,又更新成R2,也发生binlog。由于B同步回A的操作,让A又发生了新的binlog,A又要同步到B,如此重复,陷入无限循环中。

DELETE操作 同样存在先后顺序问题。例如先插入一条记载,再删除。B在A删除后,又将插入的数据同步回A,接着再将A的删除操作也同步回A,每次都市发生binlog,陷入无限回环。

关于数据回环问题,笔者有着血的教训,曾经因为笔者的误操作,将一个库的数据同步到了自身,最终也导致无限循环,原因分析与上述提到的UPDATE、DELETE操作类似,读者可自行思考。针对上述数据同步到历程中可能会存在的数据回环问题,最终会导致数据无限循环,因此我们必须要解决这个问题。

由于存在多种解决方案,我们将在稍后统一举行解说。2.6 数据同步架构设计 现在,让我们先把思路先从解决数据同步的详细细节问题转回来,从更高的层面解说数据同步的架构应该如何设计。稍后的内容中,我们将解说种种制止数据回环的种种解决方案。

前面的架构中,只涉及到2个DB的数据同步,如果有多个DB数据需要相互同步的情况下,架构将会变得很是庞大。例如:这个图演示的是四个DB之间数据需要相互同步,这种拓扑结构很是庞大。为相识决这种问题,我们可以将数据写入到一个数据中转站,例如MQ中举行生存,如下:我们在差别的机房各部署一套MQ集群,这个机房的binlog syncer将需要同步的DB binlog数据写入MQ对应的Topic中。

对端机房如果需要同步这个数据,只需要通过binlog writer订阅这个topic,消费topic中的binlog数据,插入到目的库中即可。一些MQ支持consumer group的观点,差别的consumer group的消费位置offset相互隔离,从而到达一份数据,同时供多个消费者举行订阅的能力。固然,一些binlog订阅剖析组件,可能实现了类似于MQ的功效,此时,则不需要独立部署MQ。

那么MQ应该选择什么呢?别问,问就是Kafka,详细原因问厮大。3 数据据回环问题解决方案 数据回环问题有多种解决方案,通过清除法,一一举行解说。

3.1 同步操作不生成binlog 在mysql中,我们可以设置session变量,来控制当前会话上的更新操作,不发生binlog。这样当往目的库插入数据时,由于不发生binlog,也就不会被同步会源库了。为了演示这个效果,笔者清空了本机上的所有binlog(执行reset master),现在如下图所示:忽略这两个binlog event,binlog文件花样最开始就是这两个event。

接着,笔者执行set sql_log_bin=0,然后插入一条语句,最后可以看到简直没有发生新的binlog事件: 通过这种方式,貌似可以解决数据回环问题。目的库不发生binlog,就不会被同步会源库。可是,谜底是否认的。我们是往目的库的master插入数据,如果不发生binlog,目的库的slave也无法同步数据,主从数据纷歧致。

所以,需要清除这种方案。提示:如果恢复set sql_log_bin=1,插入语句是会发生binlog,读者可以自行模拟。3.2 控制binlog同步偏向 既然不发生binlog不能解决问题。

那么换一种思路,可以发生binlog。当把一个binlog转换成sql时,插入某个库之前,我们先判断这条记载是不是原本就是这个库发生的,如果是,那么就扬弃,也可以制止回环问题。

现在问题就变为,如何给binlog加个标志,表现其实谁人mysql集群发生的。这也有几种方案,下面一一讲述。3.2.1 ROW模式下的SQL mysql主从同步,binlog复制一般有3种模式。STATEMENT,ROW,MIXED。

默认情况下,STATEMENT模式只记载SQL语句,ROW模式只记载字段变换前后的值,MIXED模式是二者混淆。binlog同步一般使用的都是ROW模式,高版本Mysql主从同步默认也是ROW模式。我们想接纳的方案是,在执行的SQL之前加上一段特殊标志,表现这个SQL的泉源。

例如/*IDC1:DB1*/insert into users(name) values("tianbowen")其中/*IDC1:DB1*/是一个注释,表现这个SQL原始在是IDC1的DB1中发生的。之后,在同步的时候,剖析出SQL中的IDC信息,就能判断出是不是自己发生的数据。

然而,ROW模式下,默认只记载变换前后的值,不记载SQL。所以,我们要通过一个开关,让Mysql在ROW模式下也记载INSERT、UPDATE、DELETE的SQL语句。

详细做法是,在mysql的设置文件中,添加以下设置:binlog_rows_query_log_events =1这个设置可以让mysql在binlog中发生ROWS_QUERY_LOG_EVENT类型的binlog事件,其记载的就是执行的SQL。通过这种方式,我们就记载下的一个binlog最初是由哪一个集群发生的,之后在同步的时候,sql writer判断目的机房和当前binlog中包罗的机房相同,则扬弃这条数据,从而制止回环。

这种思路,功效上没问题,可是在实践中,确很是贫苦。首先,让业务对执行的每条sql都加上一个这样的标识,险些不行能。另外,如果忘记加了,就不知道数据的泉源了。

如果接纳这种方案,可以思量在数据库会见层中间件层面添加支持在sql之前增加/*..*/的功效,统一对业务屏蔽。纵然这样,也不完美,不能保证所有的sql都通过中间件来来写入,例如DBA的一些日常运维操作,或者手工通过mysql下令行来操作数据库时,肯定会存在没有添加机房信息的情况。总的来说,这个方案不是那么完美。3.2.2 通过附加表 这种方案现在许多知名互联网公司在使用。

大致思路是,在db中都加一张分外的表,例如叫direction,记载一个binlog发生的源集群的信息。例如CREATE TABLE `direction` ( `idc` varchar(255) not null, `db_cluster` varchar(255) not null,) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4idc字段用于记载某条记载原始发生的IDC,db_cluster用于记载原始发生的数据库集群(注意这里要使用集群的名称,不能是server_id,因为可能会发生主从切换)。假设用户在IDC1的库A插入的一条记载(也可以在事务中插入多条记载,单条记载,纵然不开启事务,mysql默认也会开启事务):BEGIN;insert into users(name) values("tianshouzhi”);COMMIT;那么A库数据binlog通过sql writer同步到目的库B时,sql writer可以提前对事务中的信息可以举行一些修改,,如下所示:BEGIN;#往目的库同步时,首先分外插入一条记载,表现这个事务中的数据都是A发生的。

insert into direction(idc,db_cluster) values("IDC1”,"DB_A”)#插入原来的记载信息insert into users(name) values("tianshouzhi”);COMMIT;之后B库的数据往A同步时,就可以凭据binlog中的第一条记载的信息,判断这个记载原本就是A发生的,举行扬弃,通过这种方式来制止回环。这种方案已经已经由许多的公司的实际验证。3.2.3 通过GTIDMysql 5.6引入了GTID(全局事务id)的观点,极大的简化的DBA的运维。

在数据同步的场景下,GTID依然也可以发挥极大的威力。GTID 由2个部门组成:server_uuid:transaction_id其中server_uuid是mysql随机生成的,全局唯一。transaction_id事务id,默认情况下每次插入一个事务,transaction_id自增1。

注意,这里并不会对GTID举行全面的先容,仅说明其在数据同步的场景下,如何制止回环、数据重复插入的问题。GTID提供了一个会话级变量gtid_next,指示如何发生下一个GTID。可能的取值如下:AUTOMATIC: 自动生成下一个GTID,实现上是分配一个当前实例上尚未执行过的序号最小的GTID。

ANONYMOUS: 设置后执行事务不会发生GTID,显式指定的GTID。默认情况下,是AUTOMATIC,也就是自动生成的,例如我们执行sql:insert into users(name) values("tianbowen”); 发生的binlog信息如下:20BBA7C4-FAB0-4C2A-B1BB-D84ABFC5DB39可以看到,GTID会在每个事务(Query->...->Xid)之前,设置这个事务下一次要使用到的GTID。

从源库订阅binlog的时候,由于这个GTID也可以被剖析到,之后在往目的库同步数据的时候,我们可以显示的的指定这个GTID,不让目的自动生成。也就是说,往目的库,同步数据时,酿成了2条SQL: 发生的binlog信息如下:可以看到,GTID会在每个事务(Query->...->Xid)之前,设置这个事务下一次要使用到的GTID。

从源库订阅binlog的时候,由于这个GTID也可以被剖析到,之后在往目的库同步数据的时候,我们可以显示的的指定这个GTID,不让目的自动生成。也就是说,往目的库,同步数据时,酿成了2条SQL:SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1’insert into users(name) values("tianbowen")由于我们显示指定了GTID,目的库就会使用这个GTID当做当前事务ID,不会自动生成。

同样,这个操作也会在目的库发生binlog信息,需要同步回源库。再往源库同步时,我们根据相同的方式,先设置GTID,在执行剖析binlog后获得的SQL,还是上面的内容SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1'insert into users(name) values("tianbowen")由于这个GTID在源库中已经存在了,插入记载将会被忽略,演示如下:mysql> SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1';Query OK, 0 rows affected (0.00 sec)mysql> insert into users(name) values("tianbowen");Query OK, 0 rows affected (0.01 sec) #注意这里,影响的记载行数为0注意这里,对于一条insert语句,其影响的记载函数居然为0,也就会插入并没有发生记载,也就不会发生binlog,制止了循环问题。

如何做到的呢?mysql会记载自己执行过的所有GTID,当判断一个GTID已经执行过,就会忽略。通过如下sql检察:mysql> show global variables like "gtid_executed";+---------------+------------------------------------------+| Variable_name | Value |+---------------+------------------------------------------+| gtid_executed | 09530823-4f7d-11e9-b569-00163e121964:1-5 |+---------------+------------------------------------------+上述value部门,冒号":"前面的是server_uuid,冒号后面的1-5,是一个规模,表现已经执行过1,2,3,4,5这个几个transaction_id。这里就能解释了,在GTID模式的情况下,为什么前面的插入语句影响的记载函数为0了。显然,GTID除了可以资助我们制止数据回环问题,还可以资助我们解决数据重复插入的问题,对于一条没有主键或者唯一索引的记载,纵然重复插入也没有,只要GTID已经执行过,之后的重复插入都市忽略。

固然,我们还可以做得越发细致,不需要每次都往目的库设置GTID_NEXT,这究竟是一次网络通信。sql writer在往目的库插入数据之前,先判断目的库的server_uuid是不是和当前binlog事务信息携带的server_uuid相同,如果相同,则可以直接抛弃。

检察目的库的gtid,可以通过以下sql执行:mysql> show variables like "server_uuid";+---------------+--------------------------------------+| Variable_name | Value |+---------------+--------------------------------------+| server_uuid | 09530823-4f7d-11e9-b569-00163e121964 |+---------------+--------------------------------------+ GTID应该算是一个终极的数据回环解决方案,mysql原生自带,比添加一个辅助表的方式更轻量,开销也更低。需要注意的是,这倒并不是一定说GTID的方案就比辅助表好,因为辅助表可以添加机房等分外信息。

在一些场景下,如果下游需要知道这条记载原始发生的机房,还是需要使用辅助表。4 开源组件先容canal/otter前面深入解说了单元化场景下数据同步的基础知识。读者可能比力感兴趣的是,哪些开源组件在这些方面做的比力好。

笔者建议的首选,是canal/otter组合。canal的作用就是类似于前面所述的binlog syncer,拉取剖析binlog。

otter是canal的客户端,专门用于举行数据同步,类似于前文所解说的sql writer。而且,canal的最新版本已经实现了GTID。

我现在是在职Java开发,如果你现在正在相识Java技术,想要学好Java,盼望成为一名Java开发工程师,在入门学习Java的历程当中缺乏基础的入门视频教程,你可以关注并私信我:01。我这里有一套最新的Java基础JavaSE的精讲视频教程,这套视频教程是我在年头的时候,凭据市场技术栈需求录制的,很是的系统完整。


本文关键词:乐鱼官网推荐,异地,多活,场景,下,如何,举行,数据,同步,文章

本文来源:leyu乐鱼全站app-www.bicester-country-club.com