异地多活之Set化系统架构设计

关键词

  1. 异地多活
  2. 流量分发
  3. 系统容灾

一、背景

1.1 为什么需要异地多活

随着公司业务的不断发展,业务上对系统的要求也会不断变化。一个不断向上发展的业务所对应的软件系统的发展周期一般会经历以下几个阶段:

  1. 创业初期:这个阶段用户较少,系统的单量也比较低,系统的建设的主要目标就是实现业务功能的完整性。简单说就是:业务需要什么,就快速实现什么,这个阶段属于业务的发展阶段和试错阶段,"快"就是最重要的事,也是屎山的罪魁祸首。
  2. 快速上升期:这个阶段业务得到快速发展,一般体现为用户量、业务单量、系统流量等迅速上升,达到创业初期所设计系统的性能瓶颈。这时候为了支持更高的系统吞吐能力,我们增加更多的服务器;为了提供更大的容量和更高的性能,我们引入了分库分表对数据进行切分治理(分治);提出CQRS架构、实现读写分离、支持配置化、开始做系统重构等等。在该阶段,我们在设计系统时开始更多地考虑系统的扩展能力,进行系统重构解决早期的屎山,做更多技术上的思考,在该阶段对于新的系统设计,我们会考虑其是否能支撑未来几年甚至十几年的业务发展。
  3. 业务稳定期:这个阶段业务发展变得更平稳,每天都有巨大的访问量和单量,因为我们在上一个阶段设计了支持水平扩展的系统,所以系统的性能不再是问题。但是由于某一天,某个核心服务(比如订单服务,LBS服务,认证服务等)出现故障;或者机房发生了断电、机房所在地发生了火灾、网络中断等,导致整个机房完全不可用,相关的核心服务完全无法提供服务,整个系统服务直接停机,工程师花费几小时甚至长达几天进行系统恢复。在这个时间段,从业务损失上看,资损一般少则千万,多则几亿。于是在该阶段,我们开始考虑系统的灾备,实现即便是一个数据中心某个核心服务不可用,甚至一个城市的机房完全被毁,我们可以迅速使用异地的正常机房接替故障机房继续提供服务,从而保证业务正常。这也是异地多活被提出的主要原因。除了这个原因,还有一个直接导因,假设我们的运气足够好,系统平稳运行了几十年,机房所在地未发生任何天灾人祸,即不需要考虑灾难,但是由于业务的持续上升,我们就不得不做更多的分库分表,到最后导致分库的所引起的数据库连接数已经超过了单机所能达到的物理连接瓶颈,或者单个机房已经不足以放下那么多的物理机进行使用,这时候系统的水平扩展能力便达到了极限(早期某里提出Set化的直接原因)。

1.2 什么是异地多活

在单机系统的时代,机器挂了,服务也就终止了,没有任何方式能够保证在这台机器断电或挂了的时候,它还能服务。

但如果有更多的机器,你会忽然发现,一台机器挂了,还有其他机器可以工作;一个机房里面的所有机器都挂了,还有其他机房的机器可以工作。

在机器多了以后,我们可以额外的追求更多的东西了,这就是服务的可用性(也叫做多活)。

异地多活是指在不同城市建立独立的数据中心,每个数据中心都可以承担业务,通过机器冗余的方式来实现在一个城市的机器都挂了的情况下,仍然有另外城市的机器可以工作。(核心目标)

1.3 各大公司如何实现异地多活

1.3.1 国内:某讯、某里

某讯先后遭遇了08年圣诞节深圳核心光纤被挖断,以及后来812天津港爆炸导致电力中断等外部故障,开始对核心业务进行异地容灾改造。

2013年夏天,因为杭州40°高温酷暑持续,全城电力供应极度紧张,而某里的服务器机房又是耗电大户,险被拉闸限电,随后启动异地容灾项目:

  • 2013年在杭州一个城市实现了同城两个SET双活
  • 2014年在杭州、上海两个相距有一定距离的城市实现了异地双活,当年双十一两个城市分别承担了50%的用户流量
  • 2015年在杭州、上海两地基础上,在距离超过1千公里外的华南部署了两个新的SET,成功实现三地四单元的异地多活

某金服也是从2013年已经完成了基本的SET化架构尝试,分为两个SET,帮助某宝顺利支撑当年双十一活动;后续建设比较完善的三地五中心异地多活架构;

某云也为其客户提供了异地多活解决方案;现阶段客户需要承担的工作仍然比较多,和某里内部业务建设阶段类似,包括:业务梳理、多活分析、应用改造和实施演练。

1.3.2 国外:Amazon、Google

AWS体系化的容灾架构方案,供不同需求的客户选用:

对以上4种容灾方案,AWS都给出了具体的解决方案、架构图、成本估算、具体执行步骤和一系列自动化脚本。

企业可结合自己对应用系统RTO和PRO的要求以及成本预算,选择最适合自己的灾备方案。

此外,对企业混合云场景,AWS也提供低成本的混合云容灾方案

Google为其公有云用户提供了 Google Cloud 灾难恢复指南,核心思路和AWS一致,即根据RTO/RPO要求的不同,制定不同的Disaster recovery模式。

RPO/RTO:

  1. RTO (Recovery Time Objective,复原时间目标)是指灾难发生后,从IT系统当机导致业务停顿之时开始,到IT系统恢复至可以支持 各部门运作、恢复运营之时,此两点之间的时间段称为RTO。比如说灾难发生后半天内便需要恢复,RTO值就是十二小时。
  2. RPO (Recovery Point Objective,复原点目标)是指从系统和应用数据而言,要实现能够恢复至可以支持各部门业务运作,恢复得来 的数据所对应时的间点。如果现时企业每天凌晨零时进行备份一次,当服务恢复后,系统内储存的只会是最近灾难发生前那个凌晨零时的资料。

在现实场景中,对于RTP/RPO要求较低(图左)的业务一般是日志数据、长时间归档的数据。对于RTP/RPO要求较高(图右)的业务,用现实生活中的银行转账举例,对于银行的账户交易业务,银行的要求就是不丢失任何数据,即便是某一个数据中心完全故障,业务也支持可用。

二、架构演进

2.1 单数据中心(AZ)系统运行架构

现在流行的常规微服务系统架构大致如图所示,不同物理地域的用户通过Web端、App端、小程序等接入层访问系统,请求首先达到网关路由,网关再讲请求转发至指定的业务应用,请求进入业务应用后,各个服务互相在业务层内部进行交互,并在交互过程中进行数据库查询操作。需要注意的是,因为访问数据库是很常见的操作,出于性能考虑,业务层的业务服务与持久化层一般都会在同一个机房(即数据中心,简称AZ),只是运行在不同的物理机上,甚至可能物理机都是同一个,只是不同的进程实例。

如果Service A是整个业务的核心服务,比如订单服务,认证服务等,某个时刻该AZ的Service A发生故障,则会导致整个业务完全不可用。同理,如果该AZ发生断电、火灾等则整个系统会完全崩溃,甚至数据可能完全无法恢复。本质原因是服务部署在单数据中心,且该数据中心不存在备份,即单机房部署便成了一个单点问题。

2.2 多数据中心系统运行架构

这里读者可能会想,我们可以再建设一个数据中心,部署完全一套相同的系统服务,甚至可以根据机房地域将用户请求进行分流,从而减小网络传输延迟,提高系统响应速度。当一个机房出现故障时,直接将流量切换到另一个机房,让另一个正常机房提供服务,如上图所示。看上去没啥问题,但是这里存在一个棘手的问题,大部分服务都需要与数据库这类有状态的服务进行交互,如果北京的用户只访问北京AZ,上海的只访问上海AZ,那也就意味着北京用户的数据也只会存在于北京AZ,同理上海用户数据只会存在于上海AZ,那么当北京AZ发生故障,即便是流量完全切换至上海AZ,由于上海并不存有任何北京AZ的任何用户的数据,对于北京的用户来说,上海AZ并不能作为其正确的副本继续提供服务(简单说:北京用户在北京刚下单完毕,订单数据存在于北京AZ,这时候北京AZ发生故障,流量切换至上海AZ,由于上海AZ不存在任何该北京用户的订单数据,用户再次查看订单会发现不存在任何订单数据,这显然是不可接受的)

2.3 多数据中心系统运行架构V2

这种解决方案也有一定意义,即运行架构变成上图,针对核心服务,我们在异地机房进行同样部署,针对有状态的服务,比如数据库、缓存、MQ等仍然采用单点部署(简化说明道理,图例只说明数据库),即北京、上海AZ都连接的北京的同一个数据库,数据均存在北京AZ。假如北京AZ的Service A发生故障,流量则切换至上海AZ,由于数据库未改变,整个业务功能仍然是完整的,但是这种情况下服务是有损的,因为针对Service A的请求,均是从上海AZ访问到北京AZ,这是跨物理地域的网络请求,会引起网络请求耗时加长。但是这种解决方案确实能解决针对无状态的核心服务发生故障的问题,大部分对稳定性、可用性要求不高的系统,该运行架构已经可以满足需求。但是该架构仍然无法解决数据库发生故障或者机房完全被毁引起数据库被毁的问题,发生上述故障时,整个系统仍然会完全不可用。因为数据库这种有状态服务仍然是单点,不存在能完整替换的备份。

2.4 Set化架构

这里读者可能会说,将不同AZ的数据库数据进行同步,甚至可以针对核心数据的更新操作变更为保证多个AZ同时更新完毕才算更新完毕(突然想到分布式算法的CR算法),这样在进行AZ切换时,保证备份的AZ上有相同的数据,这样就可以进行安全的切换,其运行架构变成上图。其实,Set化解决方案本质就是按照这个思路进行实现。从理论上看,如果我们能够实现这种架构,那么就可以实现,即便是北京AZ完全不可用,我们也可能采用上海AZ迅速提供服务。

三、存在的问题

从上述运行架构上可以看出,北京/上海AZ在业务功能上完全是独立的、且完整的,任何一个AZ理论上都可以对外提供完整的服务。但是会存在以下问题,如何保证北京AZ和上海AZ就是独立的,换言之,北京AZ如果需要读取上海AZ的数据时怎么处理。对于统计请求,由于数据库存了两个地方的数据,如何过滤其他AZ的数据;全国统计如何实现。数据库如何解决数据循环同步。

3.1 跨AZ请求

在解答这个问题补充一下Set化架构的一些原则

  • 切分原则

    • 每个SET包含所有核心业务应用和数据,在其它SET发生故障时,核心功能不受影响,有效避免全局故障。所以Set化要求在任何业务操作中不运行出现跨Set请求的情况发生。
    • SET切分和业务场景强相关,需要在规划阶段充分评估论证,确保能实现SET切分目标
    • 在实现容灾目标同时,应SET改造范围最小化,尽可能降低后续业务改造成本
  • 常见切分维度:通常根据核心用户特征信息,如用户ID,用户位置城市ID等;前文便采用的用户位置城市ID进行切分。

理论上确实会有跨AZ请求的可能性,但从实际的业务场景的角度出发,任何数据都是存在关联关系的。比如,在北京AZ的用户A的任何订单,操作日志,信息等都是用户A的,在北京AZ的用户A的任何请求中,不会有查看处于上海AZ的用户B的数据的场景,而数据的切分采用的用户位置的城市ID,即该用户的所有数据一定会被路由到同一个Set中,不可能出现跨Set的请求。所以Set化要求在任何业务操作中不运行出现跨Set请求的情况发生。即:北京/上海AZ在数据上互相同步,互为主备,但是北京AZ存有上海AZ的数据,上海也存有北京AZ的数据。但是北京AZ永远不会有请求去读取上海AZ这部分数据,同步上海AZ也不会读取北京AZ的数据。各自冗余对方AZ的数据,用于灾备发生时Set切换时使用。

3.2 如何解决数据循环同步

这个问题是最开始出现的问题,如果我们binlog的方式实现两个数据库进行同步,比如北京AZ将订单A的同步给上海AZ,上海AZ插入后该订单又生成了新的Binlog,这条Binlog又会同步至北京AZ,如果不做处理,北京AZ会认为这条Binlog是上海AZ的新数据,北京AZ需要进行存储。解决这个问题的方案也比较简单。在存储数据时,记录数据的来源,相当于对Binlog进行了打标,标记上来源Set,并且记录下来,再向其他AZ同步Bloglog时,只同步带有本AZ的Set化标签数据,或者在收到Binlog时只操作不是本Set标签的Binlog。

3.3 统计请求如何过滤其他Set的数据

这个问题也可以用3.2回答,在执行count等命令默认加上where条件:Set-tag=本Set即可。但是现在大部分实际情况并不是按照这种方式实现,这里在明晰一个实际的场景:针对统计型需求,基本上所有统计都是面向于查看,分析类需求,即非事务型、非实时读、非强一致性型场景,这类需求我们完全可以采用CQRS架构,将读写进行分离,Set化隔离的数据可以简单理解为针对强一致、事务型、交易型的底层数据,因为Set要求操作单元化、要求强一致、数据与数据根据外键进行强关联(可以Set切分的本质原因),所以针对所有的写入和更新操作都能在单个Set进行完整的闭环操作,单个Set的更新操作是有状态的。但是针对非事务型的读取请求,面向的是无状态数据,这类数据完全可以通过数据同步的方式,将其同步至某一个专门用于非实时查询的Set中即可。所以Set化实际上也常常会和读写分离绑定落地,所以实际的运行架构如下。Set主要针对核心业务进行高可用支持,其主要主要支持交易型业务、写入型业务、针对分析型,非实时读可以采用CQRS或者Set-tag=本Set进行解决。

3.4 Set切换可以做到完全无损吗

大道同归,读者应该了解过分布式算法,如果还没了解,欢迎阅读分布式算法之MIT6.824系列总结,纵观所有分布式算法,没有一种算法可以做到,即又要扩展性(高性能),又要容错(高可用),还要数据的强一致性。在分布式场景下,Set化架构也一样,做不到"既要""又要""还要"。

对于Set化架构一般存在两种选择

  1. 保证RPO/RTO完全实时,具体做法也有两种:

    1. 对于更新操作必须满足至少一半以上AZ的完全更新完毕才算做更新完毕(类比Raft算法),该做法会影响每一个更新操作,IO操作扩大为AZ数量倍,且跨多个物理地域,极大的影响性能,但银行的交易选择了这种方式,因为银行面向的业务和钱相关,几乎所有场景都要求绝对的强一致性,且不能接受任何一笔数据被丢失,为了这个强一致性和RPO/RTO完全实时选择牺牲性能,让用户等待网络的延时,这也是为什么每次转账慢等待数秒的一个主要原因。
    2. 还有一种方式,Set切换时禁止写入请求,等待切换的AZ数据完全同步至备份AZ,待同步完成后将流量进行切换,从开始到切换结束TP999一般在10s以内,即会造成大概10s的服务写入故障,这种方式仅支持对故障进行了预感知,比如某地方断电,机房靠备用电源、机房温度持续上升无法降温、或者故障演练等手动进行切换;如果机房发生瞬间故障,故障期间的数据仍然无法及时同步至备份AZ,这部分数据丢失只能采用最终一致性。
  2. 支持RPO/RTO近实时,这种相对比较简单,尽最大可能进行数据同步,但不保证在切换时完全进行了同步,切换时可能部分数据未同步,甚至无法同步(火灾烧毁等),针对这部分数据采用最终一致性进行处理即可。

四、思考

全文面向系统的灾备在对Set化进行介绍,除了灾备,Set化还有一个很重要的意义在于,提供一套系统无限平滑扩展、流量隔离的解决方案,有兴趣的读者可以进行了解,只要系统容量不够就增加Set进行提供服务,流量通过Set标签进行染色并分发。而Set的标签生成逻辑是可以进行配置的。当需要流量隔离时就创建一个Set,对一部分流量进行染色即实现流量分发,用于业务进行流量和业务线隔离。

这里读者需要注意,并不是一个AZ就只有一个Set,正常情况时1个AZ中存在多个Set,甚至可能运行在同一个物理机上,只是不同的进程和端口上。到这里,笔者的感触是Set化本质还是分治思想,因为产生Set化的直接导因是因为分库分表达到上限,单机已经不能再连接更多的数据库,所以才不得不将数据拆分到不同的物理单元,各Set能力完全相同,但又完全独立, 只是数据完全进行隔离。为了支持容灾,又将多个Set作为主备已支持容灾能力。

附录

参考引用

  1. 高性能系统设计之分库分表及平滑扩/缩容
  2. 体系化的容灾架构方案
  3. 混合云容灾方案
  4. Google Cloud 灾难恢复指南
  5. Disaster recovery
  6. GB/T 20988------2007
相关推荐
Java程序之猿1 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构
AskHarries2 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion3 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder4 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
小蜗牛慢慢爬行4 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
凌虚4 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心5 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
思忖小下5 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
.生产的驴6 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven