Skip to main content
  1. internet/

异地多活

·3344 words·7 mins·

背景 #

随着用户的日益增多,系统的质量问题越来越突出。

想象一下:用户正在使用软件,突然软件崩溃了、不能用了,这时候用户肯定要理(ma)解(niang)的。如果一年只崩溃一两次还好(当然,如飞机、火车运行所需要的软件是绝对不能出问题的),如果每隔几天就来这么一下,那么用户可能就要寻找替代品了。2B的产品更是如此(业内通常使用SLA来描述可靠性,也就是大佬们常说的4个9、5个9)。

提升服务质量的手段有很多,如:

  • 良好的代码风格、积极的code review、完善的自动化测试——在根上减少问题出现的可能性

  • 合理的监控、报警、预警——保证第一时间内得到通知甚至提前预知风险

  • 科学的熔断策略——减小一个低质量的服务造成全体系统崩溃的风险

  • 完善的链路追踪、日志系统——提高解决问题的速度

  • 善用灰度网关——减少重构系统带来的风险以及损失

  • 。。。

尽管目前的手段众多,但是如果一个地区发生了“黑天鹅”事件,如没有预警的停电、地震、海啸,又碰巧这就是我们的服务器所在地,那么上述手段也是无能为力。

所以我们就需要更强大的容灾方案——异地多活。

目标 #

实现两地三中心方案。

什么是两地三中心?就是在两个区域部署三套服务——一个区域一套,另外一个区域两套。大部分两地三中心是在同城双活的基础上,增加了异地灾备数据中心。而对我们来说,其实就是实现的多区域同步设计方案,只是在实施上是两地三中心。

为什么不是三地三中心?因为城市之间要通过光缆来传输数据,而这是一笔很大的开销。

功能列表:

  1. 用户“就近访问”
  2. 区域之间的数据同步
  3. 一个区域的服务器宕机后,流量自动打到其他区域

仅看这个功能列表,很多细节都很模糊(不是模糊,是根本就没有),我们先看设计方案,然后再把剩余的细节问题解决。

设计方案 #

两区域间单向的数据流 #

上图是区域之间数据的单向流动。

  1. 数据库的同步组件选择了阿里开源的canal,它会模拟从服务器来获取数据库的binlog
  2. canal支持tcp、kafka、rocketmq三种同步方式,我们选择kafka
  3. 发送端:主动发起同步的区域从kafka中获取到数据,然后发往被同步的区域
  4. 接收端:被同步的区域接收数据的服务即为接收端,接收到数据后会放到kafka中。这里kafka的作用是削峰与暂时的持久化。
  5. 回放端:从第四步中的kafka中获取数据,解析为sql,并执行,完成数据的回放

以上步骤解决了两个区域之间的单向同步

两区域间双向的数据流 #

image-20211004233516052

跟前一张图相比,只是进行了“镜像复制”,逻辑没有增加。

但是我们发现了数据回环——即从A区域的数据同步到B区域之后,又回到了A区域。如何打断数据回环

一般来说,我们以“就近原则”为准,能在B区域打断就不要在A区域打断,这样至少能减少数据传输。

我们能控制得只有接收端、回放端和发送端,并且需要在入库之前打上标记,入库拿到数据之后进行过滤。根据“就近原则”,我们在回放端标记数据,在发送端进行数据过滤。具体方案如下:

将数据信息记录到redis的hash中,key为`replay:{数据库名}:{表名}`, field和value规则如下:
1. 对于DDL, field为crc32(sql)+区域标识, value为serverID
2. 对于插入, field为操作类型标识+主键ID+区域标识, value为来源serverID
3. 对于删除, field为操作类型标识+主键ID+区域标识, value为来源serverID
4. 对于更新, field为操作类型标识+主键ID+crc32(after)+区域标识, value为来源serverID
  • 其中serverID为数据库实例的唯一标识,这里只来源实例。
  • after为更新后的列数据,在实现中是一个结构体。插入和删除都是幂等的,因此不需要记录列信息,更新操作需要判断是否为同一条语句只用主键是不行的,所以需要记录列信息。

发送端从kafka获取到数据后,先判断数据是否是回环数据,如果是则过滤,然后删除缓存。

数据流向图如下:

三区域间双向的数据流 #

逻辑与两区域相同,只是图更难画。

区域宕机处理 #

在多区域中,一个区域宕机会导致其他区域的数据不一致,这时候就要找到获得宕机区域数据最新的区域(实际上,更准确的表述应该是找到每个最新的数据库表,因为各个数据库表都是独立进行同步的),对数据缺失的其他区域进行补偿。那么如何找到这个数据最新的区域

先了解下canal的机制:在canal的配置中,我们以数据库名作为topic,对表名进行哈希取模后作为分区存入kafka中,那么对一个表的消费情况通过偏移量offset即可确知。

但是不同区域同一个topic的同一个分区下,同一个offset对应的数据可能是不同的,这和canal中配置的binlog文件和偏移量有关。因此,记录消费位点,我们不能以本区域的kafka偏移量为准,而应该以其他区域的kafka偏移量为准

这意味着发送端在发送数据时,需要将本条kakfa消息的位点告之接收端,接收端得到后,对其进行记录。

当北京区域宕机后:

  • 广州区域记录表XXX中接收到北京区域的偏移量为80
  • 上海区域记录表XXX中接收到北京区域的偏移量为100

那么我们就知道对于北京区域所同步的XXX表的数据而言,上海区域中表XXX的数据最新,因此我们需要将上海区域最新的这20条消息发给广州。

那怎样找到这20条消息呢?我们得到的位点是北京区域的,不是上海区域的。目前的解决方式是在接收端获取到数据时,将其写入到kafka中,这样就相当于进行了一份备份。对于这种备份,我们可以缩小kafka文件的保留时间来减少资源的消耗。

回想下整个过程,我们已经想好了区域宕机后的数据同步逻辑,但是如何得知区域宕机了呢?我们通过zookeeper进行监听。同时,zookeeper也是配置中心。

前端如何访问本区域 #

用户在访问时,网关会判断其所属区域,如果是本区域,则直接访问,如果不是,则告知其所属区域的地址,前端接收到该信息后,重新访问其所属区域的服务。

这里有个问题:一个区域是会崩溃的,如果用户访问的是崩溃的区域,那么就没办法访问了。因此前端需要记住三个区域的地址,如果一个区域访问不通,就访问另外的。

但这又产生了另外一个问题:区域A崩溃后,原属于区域A的用户的数据可能还没有完全同步到其他区域,这时数据是不一致的。所以我们在其同步完成前,是不允许其写入操作的,只能查看,即使看到的数据有些延迟,也不会差太多,并且很快就会同步完成。

因为断电等原因导致的区域崩溃,数据是肯定会丢失一些的,但是由于同步速度快于数据库的写入速度,因此我们能够最大程度上保护数据。

如何降低数据延迟 #

区域之间的数据同步延迟比较严重(相对来说),所以我们根据企业来源地来决定用户访问哪个区域的服务,并且能保证大部分用户能够得到准确的判断。

对于另外一些用户,比如去外地出差,我们暂时不做优化,还让他访问原区域的服务。因为这存在数据安全性问题。下面会讲。

数据安全性 #

在我们的设计中,三个区域都是可以写入的,并且需要互相同步,那么要如何保证三个区域之间的数据不冲突的?

  1. 对数据库的主键冲突问题:我们强制要求在数据表中使用UUID或者雪花ID作为主键
  2. 对于一个用户来说,因为他会一直访问一个区域的服务,因此对他来说不存在数据同步,但是对于一个企业来说,就不一样了,因为任何一个用户的变动都可能影响一个企业的数据。因此我们规定,对于一个企业下的用户,只能访问同一个区域下的服务,这是通过企业注册的地址来判断的。

崩溃后的区域恢复 #

区域崩溃后,其数据已经远远落后于其他区域。这时候如果还向之前那样,根据kafka的位点进行同步,对于数据的一致性是有很大不确定性的,比如崩溃的区域有些数据未同步到其他区域,这时候没有办法处理这些数据,因为用户可能已经又做了数据更改。

所以我们决定放弃不做区域恢复,重新进行区域的创建。

对接方案 #

哪些业务不可以做异地多活 #

对数据要求强一致性的业务,如涉及到金钱上的业务。对于这样的业务,再谨慎都是不为过的。

这里说的不做异地多活,是指对用户不做区域的切换,数据还是要同步的。

哪些业务可以做异地多活 #

以优先级由高到低进行罗列:

  1. 核心业务
  2. 能够给公司带来收益的业务
  3. 不要求数据强一致的业务
  4. 数据可恢复性强的业务:比如打卡