以滴滴打车、美团外卖举例来说:
打车业务体量巨大,尤其在早晚高峰期。全年订单量已越10亿。
外卖业务体量庞大,目前单量突破1700w/天,对应如此庞大的单个大型分布式集群,会面临一下问题:
1、容灾问题
2、资源扩展性问题
3、大集群拆分问
容灾问题
核心服务(比如订单服务)挂掉,会影影响全网所有的用户,导致整个业务不可用;
数据库主库集中在一个IDC,主机房挂掉,会影响全网所有用户,整个业务无法快速切换和恢复
资源扩展问题
单IDC的资源(机器、网络带宽等)已经没法满足,扩展IDC时,存在跨机房访问延时问题(增加异地机房,时延问题严重)
数据库主库单点,连接数有限,不能支持应用程序的持续发展;
大集群拆分问题
核心问题:分布式集群规模扩大后,会响应的带来资源扩展、大集群拆分以及容灾问题
所有处于对业务扩展性以及容灾需求的考虑,我们需要一套从底层架构彻底解决问题的方案,业界主流解决方案:单元化架构方案
二、SET单元化架构方案
(一)同城 "双活" 架构介绍
同城双活是在同城或相近区域内建立两个机房。同城双机房距离比较近,通信线路质量较好,比较容易实现数据的同步复制 ,保证高度的数据完整性和数据零丢失。同城两个机房各承担一部分流量,一般入口流量完全随机,内部RPC调用尽量通过就近路由闭环在同机房,相当于两个机房镜像部署了两个独立集群,数据仍然是单点写到主机房数据库,然后实时同步到另外一个机房。下图展示了同城双活简单部署架构,当然一般真实部署和考虑问题要远远比下图复杂。
1、服务路由 zk集群:
每个机房都部署一个zk集群,机房之间zk数据进行实时双向同步,每个机房都拥有所有机房zk注册数据。 路由方案:条件路由 > 就近路由 > 跨机房路由,尽量避免跨机房调用。 订阅方案:consumer订阅所有机房服务,provider只向该机房zk集群进行注册。
2、数据双活 MySQL:
采用MHA部署方案,主从半同步方案保证数据一致性。读写分离、读就近路由到机房内数据节点、写路由到master节点所在机房。 Redis: Redis cluster模式主从同步,就近读、写路由主节点机房。采用原生主从同步跨机房写性能较低,也可以依靠CRDT理论构建多节点双向同步,实现机房就近读写,但是整体实现较为复杂。
3、同城双活方案评估
优势:
(1)服务同城双活,数据同城灾备,同城不丢失数据情况下跨机房级别容灾。
(2)架构方案较为简单,核心是解决底层数据双活,由于双机房距离近,通信质量好,底层储存例如mysql可以采用同步复制,有效保证双机房数据一致性。
劣势:
(1)数据库写数据存在跨机房调用,在复杂业务以及链路下频繁跨机房调用增加响应时间,影响系统性能和用户体验。
(2)保证同城市地区容灾,当服务所在的城市或者地区网络整体故障、发生不可抗拒的自然灾害时候有服务故障以及丢失数据风险。对于核心金融业务至少要有跨地区级别的灾备能力。
(3)服务规模足够大(例如单体应用超过万台机器),所有机器链接一个主数据库实例会引起连接不足问题。
(二)两地三中心架构介绍
所谓两地三中心是指 同城双中心 + 异地灾备中心。异地灾备中心是指在异地的城市建立一个备份的灾备中心,用于双中心的数据备份,数据和服务平时都是冷的,当双中心所在城市或者地区出现异常而都无法对外提供服务的时候,异地灾备中心可以用备份数据进行业务的恢复。
两地三中心方案评估:
优势:
(1)服务同城双活,数据同城灾备,同城不丢失数据情况下跨机房级别容灾。
(2)架构方案较为简单,核心是解决底层数据双活,由于双机房距离近,通信质量好,底层储存例如mysql可以采用同步复制,有效保证双机房数据一致性。
(3)灾备中心能防范同城双中心同时出现故障时候利用备份数据进行业务的恢复。
劣势:
(1)数据库写数据存在跨机房调用,在复杂业务以及链路下频繁跨机房调用增加响应时间,影响系统性能和用户体验。
(2)服务规模足够大(例如单体应用超过万台机器),所有机器链接一个主数据库实例会引起连接不足问题。
(3)出问题不敢轻易将流量切往异地数据备份中心,异地的备份数据中心是冷的,平时没有流量进入,因此出问题需要较长时间对异地灾备机房进行验证。
同城双活和两地三中心建设方案建设复杂度都不高,两地三中心相比同城双活有效解决了异地数据灾备问题,但是依然不能解决同城双活存在的多处缺点,想要解决这两种架构存在的弊端就要引入更复杂的解决方案去解决这些问题。
三、异地多活与Set化部署
(一)异地多活
异地多活指分布在异地的多个站点同时对外提供服务的业务场景。异地多活是高可用架构设计的一种,与传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。
1、异地多活挑战
(1)应用要走向异地,首先要面对的便是物理距离带来的延时。如果某个应用请求需要在异地多个单元对同一行记录进行修改,为满足异地单元间数据库数据的一致性和完整性,需要付出高昂的时间成本。
(2)解决异地高延时即要做到单元内数据读写封闭,不能出现不同单元对同一行数据进行修改,所以我们需要找到一个维度去划分单元。
(3)某个单元内访问其他单元数据需要能正确路由到对应的单元,例如A用户给B用户转账,A用户和B用户数据不在一个单元内,对B用户的操作能路由到相应的单元。
(4)面临的数据同步挑战,对于单元封闭的数据需全部同步到对应单元,对于读写分离类型的,我们要把中心的数据同步到单元。
2、单元化
所谓单元(下面我们用RZone代替),是指一个能完成所有业务操作的自包含集合,在这个集合中包含了所有业务所需的所有服务,以及分配给这个单元的数据。
单元化架构就是把单元作为系统部署的基本单位,在全站所有机房中部署数个单元,每个机房里的单元数目不定,任意一个单元都部署了系统所需的所有的应用。单元化架构下,服务仍然是分层的,不同的是每一层中的任意一个节点都属于且仅属于某一个单元,上层调用下层时,仅会选择本单元内的节点。
选择什么维度来进行流量切分,要从业务本身入手去分析。例如电商业务和金融的业务,最重要的流程即下单、支付、交易流程,通过对用户id进行数据切分拆分是最好的选择,买家的相关操作都会在买家所在的本单元内完成。对于商家相关操作则无法进行单元化,需要按照下面介绍的非单元化模式去部署。当然用户操作业务并非完全能避免跨单元甚至是跨机房调用,例如两个买家A和B转账业务,A和B所属数据单元不一致的时候,对B进行操作就需要跨单元去完成,后面我们会介绍跨单元调用服务路由问题。
3、非单元化应用和数据
对于无法单元化的业务和应用,会存在下面两种可能性:
(1)延时不铭感但是对数据一致性非常铭感,这类应用只能按照同城双活方式部署。其他应用调用该类应用的时候会存在跨地区调用可能性,要能容忍延时,这类应用我们称为MZone应用。
(2)对数据调用延时铭感但是可以容忍数据短时间不一致,这类应用和数据可以保持一个机房一份全量数据,机房之间以增量的方式实时同步,这类应用我们暂时称为QZone。
加上两种以上非单元化应用我们的机房部署可能是下面这样,每个机房有两个RZone,MZone保持类似两地三中心部署方式,异地机房调用MZone服务需要跨地区、跨机房调用。而QZone每个机房都保持一份完整数据,机房之间通过数据链路实时相互同步。
4、请求路由
(1)Api入口网关
为了保证用户请求能正确进入自己所属单元,每一个机房都会部署流量入口网关集群。当用户请求到达进入机房内最先进入到流量网关,流量网关能感知全局的流量分片情况,计算用户所处流量单元并将流量转发到对应的单元,这样就可以将用户请求路由到对应的单元内。
采用GateWayr转发方式可以确定用户单元从而将用户流量路由到正确位置,但是HTTP转发也会造成一定性能损耗。为了减少HTTP流量转发量,可以在在用户请求返回的时候在COOKIE上带上该用户的路由标识信息。当用户下次在请求的时候请求的时候可以提前获取到路由标识直接请求到对应的单元,这种方式可以大幅度减少HTTP流量转发。
(2)服务路由
虽然应用已经进行了单元化,但是依然无法避免跨单元调用,例如A用户给B用户转账,如果A和B所处单元不同,对B用户操作需要跨单元去调用,这个时候需要能将请求路由到B用户数据所在的单元。异地多活情况下RPC、MQ、DB等等中间件都需要提供路由能力,将请求能正确路由到对应的单元。下面以RPC路由为例说明异地多活下中间件是如何进行路由的,对于其他中间件(数据库中间件、缓存中间、消息中间件等)也是一样方法。
public interface ManualInterventionFacade { @ZoneRoute(zoneType= ZoneType.RZone,uidClass = UidParseClass.class) ManualRecommendResponse getManualRecommendCommodity(ManualRecommendRequest request); }
上面展示了多活下的RPC接口定义方法,需要注明该RPC类型,如果是RZone服务必须要提供解析uid方法。下图展示了RPC注册中心路由寻址过程,和同城双活有一定的差异性。
5、数据同步
(1)QZone类型数据:这种数据只需要保证最终一致性,对于短暂不一致无影响,但是对延时非常铭感,例如一些算法、风控、配置等数据。这类数据基本上都是每个机房部署一套QZone,然后机房之间相互同步。
(2)MZone数据:这类数据对一致性非常铭感,不能出现不一致,只能采用同城双活部署方式,业务需要能容忍异地调用延时。
(3)RZone数据:这类数据每个Zone都有自己的主节点,如果数据不在该单元内需要路由到对应的节点去写。这类数据部署情况像下面这样
6、方案评估
优势:
容灾能力大幅度提高,服务异地多活,数据异地多活。
理论上系统服务可以水平扩展,异地多机房突破大幅度提升整体容量,理论上不会有性能担忧。
将用户流量切分到多个机房和地区去,有效能减少机房和地区级别的故障影响范围。
劣势
架构非常复杂,部署和运维成本很高,需要对公司依赖的中间件、储存做多方面能力改造。
对业务系统有一定的侵入性,由于单元化影响服务调用或者写入数据要路由到对应的单元,业务系统需要设置路由标识(例如uid)。
无法完全避免跨单元、跨地区调用服务,例如上面的转账业务。我们要做的是尽力避免跨地区的服务调用。
(二)Set化部署
Set化部署实际就是在异地多活的基础上衍生出来的。
解决容灾问题: UnitA一套业务的核心组件,比如网购,从加入购物车到下单,经过A、B、C、D等步骤,全部部署到UnitA的一个机房中,UnitB和UnitC是UnitA的备份,如果UnitA的服务或MQ发生故障,就会路由到UnitB或UnitC中 非核心的业务组件部署到center中 解决扩展问题: UnitA可以是旅游,UnitB可以是外卖,将来还可以扩展
相关概念
流量路由:按照特殊的key(通常为userid)进行路由,判断某次请求该路由到中心集群还是单元化集群
中心集群:为进行单元化改造的服务(通常不在核心交易链路)成为中心集群,跟当前架构保存一致
单元化集群: 每个单元化集群只负责本单元内的流量处理,以及实现流量拆分及故障隔离。每个单元化集群前期只存储本单元产生的交易数据,后续会做双向数据同步,实现容灾切换需求
中间件(RPC、KV、MQ等)
RPC:对于SET服务,调用封闭在SET内;对于非SET服务,沿用现有路由逻辑
KV:支持分SET的数据产生和查询
MQ:支持分SET的消息生产和消费
数据同步:全局数据(数据量小且变化不大,比如商家的菜品数据)部署在中心集群,其他单元化集群同步全局数据到本单元化内。未来演变为异地多活架构时,各单元化集群数据需要进行双向同步来实现容灾需要
SET化路由策略及其能力
异地容灾: 通过SET化架构的流量调度能力,将SET分别部署到不停地区的数据中心,实现跨地区容灾支持
高效本地化服务:利用前端位置信息采集和域名解析策略,将流量路由由最近的SET,提供最高效的本地化服务 比如O2O场景天然具有本地生产,本地消费的特点,更加需要SET化支持
集装箱式扩展 SET的封装性支持更灵活的部署扩展性,比如SET一键创建/下线,SET一键发布等(比如docker)
(三)RabbitMQ-SET化架构实现
SET化消息中间件架构实现(RabbitMQ双活)
使用RabbitMQ异步消息通信插件 Federation(节点和节点、集群和集群之间通信) 安装与配置:
安装插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
备注:当你再一个cluster钟使用了federation插件,所有在集群中的 nodes都需要安装federation插件
使用RabbitMQ通信插件Rederation:
Federation插件是一个在不需要cluster进行数据同步的(选择一个cluster中的节点和另一个cluster节点同步),而brokers之间传输消息的高新性能插件。
Federation插件可以在brokers或者cluster之间传输消息,链接的双方可以使用不同的users和virtual hosts、或者双方的rabbitmq和erlang版本不一致,federation插件使用AMQP协议通信,可以接受不连续的传输。
SET化配置规则:
1、Federation Exchanges,可以看成Downstream(82节点)从Upstream(81节点)主动拉取消息,并不是拉取所有消息,必须是在Downstream上已经明确定义Bindings关系的Exchange,也就是有实际的物理Queue来接收消息,才会从Upstream拉取消息到Downstream。使用AMQP协议实施代理间通信,Downstream会将绑定关系组合在一起,绑定/解绑命令将发送到Upstream交换机。
2、经过配置后,Upstream节点已经可以把消息直接通过Federation Exchanges路由给我们的Downstream节点,然后进行消费。
也就是说可以实现消息的转发,接下来也可以在Upstream添加具体的队列去进行消费Federation Exchanges里的消息,我们一条消息分别发送到2个RabbitMQ集群并且消费,这样我们可以实现SET化的关键要素,就是集群间的消息同步了。
3、可以根据自己的业务规则去规划不同的集群去监听不同的消息队列,从而达到SET化的手段,保障了性能、可靠性、数据一致性。
MQ组件实现思路和架构设计方案
MQ组件需要实现功能点
支持消息高性能序列化转换、异步化发送消息
支持消息生产实例与消费实例的链接池化、缓存化,提升性能
支持可靠性投递消息,保障消息的100%不丢失
支持消费端的幂等操作,避免消费端重复消费的问题
支持迅速消息发送模式,在一些日志收集、统计分析等需求下可以保证高性能,超高吞吐量(可忽略100%投递)
支持延迟消息模式,消息可以延迟发送,指定延迟时间,用于某些延迟检查、服务限流场景
支持事务消息,且100%保障可靠性投递,在金融行业单笔大金额操作是会有此类需求
支持顺序消息,保证消费送达消费端的前后顺序,例如下订单,再送积分、优惠券等复合性操作
支持消息补偿,重试,以及快速定位异常/失败消息
支持集群消息负载均衡,保障消息落到具体SET集群的负责均衡
支持消息路由策略,指定某些消息路由到指定的SET集群