互联网高可用相关技术

异地多活场景下的数据同步

1 什么是单元化

如果仅仅从"单元化”这个词汇的角度来说,我们可以理解为将数据划分到多个单元进行存储。"单元"是一个抽象的概念,通常与数据中心(IDC)概念相关,一个单元可以包含多个IDC,也可以只包含一个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。例如:

  • 阿里巴巴开源的canal

  • 美团开源的puma

  • linkedin开源的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。

        Whatever! 我们知道了,binlog可能不会一直保留,所以直接同步binlog,可能只能获取到部分数据。因此,通常的策略是,由DBA先dump一份源库的完整数据快照,增量部分,再通过binlog订阅解析进行同步。

2.3 如何解决重复插入

考虑以下情况下,源库中的一条记录没有唯一索引。对于这个记录的binlog,通过sql writer将binlog转换成sql插入目标库时,抛出了异常,此时我们并不知道知道是否插入成功了,则需要进行重试。如果之前已经是插入目标库成功,只是目标库响应时网络超时(socket timeout)了,导致的异常,这个时候重试插入,就会存在多条记录,造成数据不一致。

因此,通常,在数据同步时,通常会限制记录必须有要有主键或者唯一索引。

2.4 如何解决唯一索引冲突

 由于两边的库都存在数据插入,如果都使用了同一个唯一索引,那么在同步到对端时,将会产生唯一索引冲突。对于这种情况,通常建议是使用一个全局唯一的分布式ID生成器来生成唯一索引,保证不会产生冲突。

另外,如果真的产生冲突了,同步组件应该将冲突的记录保存下来,以便之后的问题排查。

2.5 对于DDL语句如何处理

如果数据库表中已经有大量数据,例如千万级别、或者上亿,这个时候对于这个表的DDL变更,将会变得非常慢,可能会需要几分钟甚至更长时间,而DDL操作是会锁表的,这必然会对业务造成极大的影响。

因此,同步组件通常会对DDL语句进行过滤,不进行同步。DBA在不同的数据库集群上,通过一些在线DDL工具(如gh-ost),进行表结构变更。

2.6 如何解决数据回环问题

 

 2.7 数据同步架构设计

        现在,让我们先把思路先从解决数据同步的具体细节问题转回来,从更高的层面讲解数据同步的架构应该如何设计。稍后的内容中,我们将讲解各种避免数据回环的各种解决方案。

        前面的架构中,只涉及到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 数据据回环问题解决方案

        数据回环问题有多种解决方案,通过排除法,一一进行讲解。

/*IDC1:DB1*/insert into users(name) values("tianbowen")
binlog_rows_query_log_events =1

 

 3.2.2 通过附加表

        这种方案目前很多知名互联网公司在使用。大致思路是,在db中都加一张额外的表,例如叫direction,记录一个binlog产生的源集群的信息。例如

CREATE TABLE `direction` (

  `idc` varchar(255) not null,

  `db_cluster` varchar(255) not null,

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

idc字段用于记录某条记录原始产生的IDC,db_cluster用于记录原始产生的数据库集群(注意这里要使用集群的名称,不能是server_id,因为可能会发生主从切换)。
在一个事务中先往附加表中插入一条消息

 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 通过GTID

Mysql 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信息如下:

可以看到,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在源库中已经存在了,插入记录将会被忽略

 

4 开源组件介绍canal/otter

前面深入讲解了单元化场景下数据同步的基础知识。读者可能比较感兴趣的是,哪些开源组件在这些方面做的比较好。笔者建议的首选,是canal/otter组合。

canal的作用就是类似于前面所述的binlog syncer,拉取解析binlog。otter是canal的客户端,专门用于进行数据同步,类似于前文所讲解的sql writer。并且,canal的最新版本已经实现了GTID。

 

如何设计秒杀系统(流量削峰)?

 

今天,我就来介绍一下流量削峰的一些操作思路:排队、答题、分层过滤。

排队

用消息队列来缓冲瞬时流量的方案

但是,如果流量峰值持续一段时间达到了消息队列的处理上限,例如本机的消息积压达到了存储空间的上限,消息队列同样也会被压垮,这样虽然保护了下游的系统,但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时,即使是有水库恐怕也无济于事。

除了消息队列,类似的排队方式还有很多,例如:

  1. 利用线程池加锁等待也是一种常用的排队方式;
  2. 先进先出、先进后出等常用的内存排队算法的实现方式;
  3. 把请求序列化到文件中,然后再顺序地读文件(例如基于MySQL binlog的同步机制)来恢复请求等方式。

 

答题

你是否还记得,最早期的秒杀只是纯粹地刷新页面和点击购买按钮,它是后来才增加了答题功能的。那么,为什么要增加答题功能呢?

这主要是为了增加购买的复杂度,从而达到两个目的。

第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊。2011年秒杀非常火的时候,秒杀器也比较猖獗,因而没有达到全民参与和营销的目的,所以系统增加了答题来限制秒杀器。增加答题后,下单的时间基本控制在2s后,秒杀器的下单比例也大大下降。答题页面如下图所示。
在这里插入图片描述
图2 答题页面

第二个目的其实就是延缓请求,起到对请求流量进行削峰的作用,从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长,从以前的1s之内延长到2s~10s。这样一来,请求峰值基于时间分片了。这个时间的分片对服务端处理并发非常重要,会大大减轻压力。而且,由于请求具有先后顺序,靠后的请求到来时自然也就没有库存了,因此根本到不了最后的下单步骤,所以真正的并发写就非常有限了。这种设计思路目前用得非常普遍,如当年支付宝的“咻一咻”、微信的“摇一摇”都是类似的方式。

这里,我重点说一下秒杀答题的设计思路。
在这里插入图片描述
图3 秒杀答题

如上图所示,整个秒杀答题的逻辑主要分为3部分。

  1. 题库生成模块,这个部分主要就是生成一个个问题和答案,其实题目和答案本身并不需要很复杂,重要的是能够防止由机器来算出结果,即防止秒杀器来答题。
  2. 题库的推送模块,用于在秒杀答题前,把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的,目的也是防止答题作弊。
  3. 题目的图片生成模块,用于把题目生成为图片格式,并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题,它要求只有人才能理解题目本身的含义。这里还要注意一点,由于答题时网络比较拥挤,我们应该把题目的图片提前推送到CDN上并且要进行预热,不然的话当用户真正请求题目时,图片可能加载比较慢,从而影响答题的体验。

其实真正答题的逻辑比较简单,很好理解:当用户提交的答案和题目对应的答案做比较,如果通过了就继续进行下一步的下单逻辑,否则就失败。我们可以把问题和答案用下面这样的key来进行MD5加密:

  • 问题key:userId+itemId+question_Id+time+PK
  • 答案key:userId+itemId+answer+PK

验证的逻辑如下图所示:
在这里插入图片描述
图4 答题的验证逻辑

注意,这里面的验证逻辑,除了验证问题的答案以外,还包括用户本身身份的验证,例如是否已经登录、用户的Cookie是否完整、用户是否重复频繁提交等。

除了做正确性验证,我们还可以对提交答案的时间做些限制,例如从开始答题到接受答案要超过1s,因为小于1s是人为操作的可能性很小,这样也能防止机器答题的情况。

分层过滤

前面介绍的排队和答题要么是少发请求,要么对发出来的请求进行缓冲,而针对秒杀场景还有一种方法,就是对请求进行分层过滤,从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的,如下图所示。
在这里插入图片描述
图5 分层过滤

假如请求分别经过CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层,那么:

  • 大部分数据和流量在用户浏览器或者CDN上获取,这一层可以拦截大部分数据的读取;
  • 经过第二层(即前台系统)时数据(包括强一致性的数据)尽量得走Cache,过滤一些无效的请求;
  • 再到第三层后台系统,主要做数据的二次检验,对系统做好保护和限流,这样数据量和请求就进一步减少;
  • 最后在数据层完成数据的强一致性校验。

这样就像漏斗一样,尽量把数据量和请求量一层一层地过滤和减少了。
分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验。

分层校验的基本原则是:

  1. 将动态请求的读数据缓存(Cache)在Web端,过滤掉无效的数据读;
  2. 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
  3. 对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
  4. 对写请求做限流保护,将超出系统承载能力的请求过滤掉;
  5. 对写数据进行强一致性校验,只保留最后有效的数据。

分层校验的目的是:

在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;

在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。

总结一下

今天,我介绍了如何在网站面临大流量冲击时进行请求的削峰,并主要介绍了削峰的3种处理方式:

一个是通过队列来缓冲请求,即控制请求的发出;

一个是通过答题来延长请求发出的时间,在请求发出后承接请求时进行控制,最后再对不符合条件的请求进行过滤;

最后一种是对请求进行分层过滤。

其中,队列缓冲方式更加通用,它适用于内部上下游系统之间调用请求不平缓的场景,由于内部系统的服务质量要求不能随意丢弃请求,所以使用消息队列能起到很好的削峰和缓冲作用。

而答题更适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度,因为越到后面无效请求也会越多,所以配合后面介绍的分层拦截的方式,可以更进一步减少无效请求对系统资源的消耗。

分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?其实不一定,你可以放一些请求过去,然后在真正减的时候再做强一致性保证,这样既过滤一些请求又解决了强一致性读的瓶颈。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值