100Wqps异地多活,得物是怎么架构的?
文章持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
100Wqps异地多活,得物是怎么架构的?
说在前面
在40岁老架构师尼恩的数千读者群中,一直在指导大家简历和职业升级,前几天,指导了一个华为老伙伴的简历,小伙伴的优势在异地多活,但是在简历指导的过程中,尼恩发现: 异地多活的概念、异地多活的架构、非常重要,但是小伙伴却对整个异地多活的体系,不是太清晰。
异地多活的概念有很杂乱,像什么同城双活、两地三中心、三地五中心等等
这里 ,尼恩 站在 得物 异地多活架构(得物架构师 YINJIHUAN)的基础上, 给大家对异地多活,做一个起底式的 、系统化、穿透式的介绍。
并且,把此文的异地多活架构,和尼恩其他的架构文章一起,组成一个架构知识系统,帮助大家实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
以上的架构系列文章,非常重要,建议大家多多看看。
以上文章的PDF版本,都可以在《技术自由圈》公众号找尼恩来获取。
言归正传。
本文目录
- 说在前面
- 本文目录
- 1.什么是异地多活
- 架构设计的3高原则
- 常见的多活方案
- 常见方案1:同城双活
- 常见方案2:两地三中心
- 常见方案2:三地五中心
- 异地多活3大挑战
- 1、数据同步延迟挑战
- 2、单元化解耦挑战
- 3、流量的路由挑战
- 2.得物APP的异地多活改造
- 2.1得物APP异地多活基础改造
- 改造之前的单机房架构
- 得物APP机房改造
- 得物APP单元化改造
- 得物APP流量调度
- 2.2 RPC框架的异地多活改造
- 2.2.1 定义RPC路由类型
- 2.2.2 业务RPC改造
- 2.2.4 遇到的问题
- 2.3 数据库的异地多活
- 2.3.1 DB-Proxy代理中间件
- 2.3.2 分布式ID
- 2.3.3 使用OTTER进行数据同步
- 2.3.4 业务改造
- 2.3.5 遇到的问题
- 2.4 Redis 的异地多活
- 2.4.1 业务改造
- 2.4.2 遇到的问题
- 2.5 RocketMQ异地多活
- 2.5.1 定义消费类型
- 2.5.2 业务改造
- 2.5.3 遇到的问题
- 2.1得物APP异地多活基础改造
- 3.得物异地多活的半单元化
- 3.1 整体方向
- 3.2 服务类型
- 3.2.1 中心服务
- 3.2.2 单元服务
- 3.2.3 中心单元服务
- 4.异地多活切流方案
- 5.得物异地多活的总结
1. 什么是异地多活
异地多活的概念很多,像什么同城双活、两地三中心、三地五中心等等概念。
要想理解异地多活,需要从架构设计的3高原则说起。
架构设计的3高原则
现如今,开发一个软件系统,对其要求越来越高,如果你了解一些「架构设计」的要求,就知道一个好的软件架构应该遵循以下 3 个原则:
- 高性能
- 高并发
- 高可用
高性能意味着系统拥有更大流量的处理能力,更低的响应延迟。
例如 1 秒可处理 10W 并发请求,接口响应时间 5 ms 等等。
高并发表示系统在迭代新功能时,能以最小的代价去扩展,系统遇到流量压力时,可以在不改动代码的前提下,去扩容系统。
高可用通常用 2 个指标来衡量:
- 平均故障间隔 MTBF(Mean Time Between Failure):表示两次故障的间隔时间,也就是系统「正常运行」的平均时间,这个时间越长,说明系统稳定性越高
- 故障恢复时间 MTTR(Mean Time To Repair):表示系统发生故障后「恢复的时间」,这个值越小,故障对用户的影响越小
可用性与这两者的关系:
可用性(Availability)= MTBF / (MTBF + MTTR) * 100%
这个公式得出的结果是一个「比例」,通常我们会用「N 个 9」来描述一个系统的可用性。
从这张图你可以看到,要想达到 4 个 9 以上的可用性,一年的不可以时间为 52分钟,平均每天故障时间必须控制在 10 秒以内。
系统发生故障其实是不可避免的,尤其是规模越大的系统,发生问题的概率也越大。
这些故障一般体现在 3 个方面:
- 硬件故障:CPU、内存、磁盘、网卡、交换机、路由器
- 软件问题:代码 Bug、版本迭代
- 不可抗力:地震、水灾、火灾、战争
这些风险随时都有可能发生。所以,在面对故障时,我们的系统能否以「最快」的速度恢复,就成为了可用性的关键。
常见的多活方案
4 个 9 高可用的核心方案就是异地多活
异地多活指分布在异地的多个站点同时对外提供服务的业务场景。
异地多活是高可用架构设计的一种,与传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。
常见的多活方案有同城双活、两地三中心、三地五中心等多种技术方案,
常见方案1:同城双活
同城双活是在同城或相近区域内建立两个机房。同城双机房距离比较近,通信线路质量较好,比较容易实现数据的同步复制 ,保证高度的数据完整性和数据零丢失。
同城两个机房各承担一部分流量,一般入口流量完全随机,内部RPC调用尽量通过就近路由闭环在同机房,相当于两个机房镜像部署了两个独立集群,数据仍然是单点写到主机房数据库,然后实时同步到另外一个机房。
下图展示了同城双活简单部署架构,当然一般真实部署和考虑问题要远远比下图复杂。
服务调用基本在同机房内完成闭环,数据仍然是单点写到主机房数据储存,然后实时同步复制到同城备份机房。
当机房A出现问题时候运维人员只需要通过GSLB或者其他方案手动更改路由方式将流量路由到B机房。
同城双活可有效用于防范火灾、建筑物破坏、供电故障、计算机系统及人为破坏引起的机房灾难。
同城双活中的核心组件GSLB的原理,可以参见 尼恩的高并三部曲 之三《Java高并发核心编程 卷3 加强版》PDF。
常见方案2:两地三中心
所谓两地三中心是指 同城双中心 + 异地灾备中心。
异地灾备中心是指在异地的城市建立一个备份的灾备中心,用于双中心的数据备份,数据和服务平时都是冷的,
当双中心所在城市或者地区出现异常而都无法对外提供服务的时候,异地灾备中心可以用备份数据进行业务的恢复。
两地三中心方案特点
优势
- 服务同城双活,数据同城灾备,同城不丢失数据情况下跨机房级别容灾。
- 架构方案较为简单,核心是解决底层数据双活,由于双机房距离近,通信质量好,底层储存例如mysql可以采用同步复制,有效保证双机房数据一致性。
- 灾备中心能防范同城双中心同时出现故障时候利用备份数据进行业务的恢复。
劣势
- 数据库写数据存在跨机房调用,在复杂业务以及链路下频繁跨机房调用增加响应时间,影响系统性能和用户体验。
- 服务规模足够大(例如单体应用超过万台机器),所有机器链接一个主数据库实例会引起连接不足问题。
- 出问题不敢轻易将流量切往异地数据备份中心,异地的备份数据中心是冷的,平时没有流量进入,因此出问题需要较长时间对异地灾备机房进行验证。
同城双活和两地三中心建设方案建设复杂度都不高,两地三中心相比同城双活有效解决了异地数据灾备问题,但是依然不能解决同城双活存在的多处缺点,想要解决这两种架构存在的弊端就要引入更复杂的解决方案去解决这些问题。
常见方案2:三地五中心
三地五中心和两地三中心 的架构差不太多,这里不做展开,
有兴趣的小伙伴,可以来尼恩的疯狂创客圈 高并发社群交流。
异地多活3大挑战
1、数据同步延迟挑战
(1)应用要走向异地,首先要面对的便是物理距离带来的延时。
如果某个应用请求需要在异地多个单元对同一行记录进行修改,为满足异地单元间数据库数据的一致性和完整性,需要付出高昂的时间成本。
(2)解决异地高延时即要做到单元内数据读写封闭,不能出现不同单元对同一行数据进行修改,所以我们需要找到一个维度去划分单元。
(3)某个单元内访问其他单元数据需要能正确路由到对应的单元,例如A用户给B用户转账,A用户和B用户数据不在一个单元内,对B用户的操作能路由到相应的单元。
(4)面临的数据同步挑战,对于单元封闭的数据需全部同步到对应单元,对于读写分离类型的,我们要把中心的数据同步到单元。
2、单元化解耦挑战
所谓单元(下面我们用RZone代替),是指一个能完成所有业务操作的自包含集合,在这个集合中包含了所有业务所需的所有服务,以及分配给这个单元的数据。
单元化架构就是把单元作为系统部署的基本单位,在全站所有机房中部署数个单元,每个机房里的单元数目不定,任意一个单元都部署了系统所需的所有的应用。
单元化架构下,服务仍然是分层的,不同的是每一层中的任意一个节点都属于且仅属于某一个单元,上层调用下层时,仅会选择本单元内的节点。
选择什么维度来进行流量切分,要从业务本身入手去分析。
例如电商业务和金融的业务,最重要的流程即下单、支付、交易流程,通过对用户id进行数据切分拆分是最好的选择,买家的相关操作都会在买家所在的本单元内完成。
对于商家相关操作则无法进行单元化,需要按照下面介绍的非单元化模式去部署。
当然用户操作业务并非完全能避免跨单元甚至是跨机房调用,例如两个买家A和B转账业务,A和B所属数据单元不一致的时候,对B进行操作就需要跨单元去完成,后面我们会介绍跨单元调用服务路由问题。
3、流量的路由挑战
- 流量调度,系统部署过去后流量怎么跟着怎么过去。
- 流量自闭环。由于距离的原因,跨地域的物理延时是没法避免的,流量过去之后怎么保证所有的操作都在本地完成,如果做不到那怎么将这种延时影响降到最低。
- 容灾切流。当某个机房出现故障时,如何快速把流量无损地切至其他机房。这里并不是说简单把流量切过去就完事,由于数据在多区域同步,流量切过去之后能否保证数据的一致性?
2. 得物APP的异地多活改造
2.1得物APP异地多活基础改造
改造之前的单机房架构
了解改造点之前我们先来看下目前单机房的现状是什么样子,才能更好的帮助大家去理解为什么要做这些改造。
如上图所示,客户端的请求进来会先到SLB(负载均衡),然后到我们内部的网关,通过网关再分发到具体的业务服务。
业务服务会依赖Redis, Mysql, MQ, Nacos等中间件。
改造之后的目标
既然做异地多活,那么必然是在不同地区有不同的机房,比如中心机房,单元机房。
所以我们要实现的效果如下图所示:
得物APP机房改造
得物多活改造一期目前有两个机房,分别是机房A和机房B。
A机房我们定义为中心机房,也就是多活上线之前正在使用的机房。
另一个B机房,在描述的时候可能会说成单元机房,那指的就是B机房。
得物APP单元化改造
得物多活进行了业务的单元改造,他们的业务比较单一,就是电商业务,所以:一个机房就是一个单元,或者说,一个单元就是一个机房,在这个单元内能够完成业务的闭环。
比如说用户进入APP,浏览商品,选择商品确认订单,下单,支付,查看订单信息,这整个流程都在一个单元中能够完成,并且数据也是存储在这个单元里面。
这块对他们的难度不大。
得物APP流量调度
用户的请求,从客户端发出,这个用户的请求该到哪个机房,这是得物APP要改造的第一个点。
没做多活之前,域名会解析到一个机房内,做了多活后,域名会随机解析到不同的机房中。
如果按照这种随机的方式是肯定有问题的,对于服务的调用是无所谓的,因为没有状态。
但是服务内部依赖的存储是有状态的呀。
得物APP是电商业务,用户在中心机房下了一个单,然后跳转到订单详情,这个时候请求到了单元机房,底层数据同步有延迟,一访问报个错:订单不存在。 用户当场就懵了,钱都付了,订单没了。
所以针对同一个用户,尽可能在一个机房内完成业务闭环。
为了解决流量调度的问题,得物APP基于OpenResty二次开发出了DLB流量网关,DLB会对接多活控制中心,
DLB流量网关能够知道当前访问的用户是属于哪个机房,如果用户不属于当前机房,DLB会直接将请求路由到该用户所属机房内的DLB。
如果每次都随机到固定的机房,再通过DLB去校正,必然会存在跨机房请求,耗时加长。
所以在这块得物APP也是结合客户端做了一些优化,在DLB校正请求后,得物APP会将用户对应的机房IP直接通过Header响应给客户端。
这样下次请求的时候,客户端就可以直接通过这个IP访问。
如果用户当前访问的机房挂了,客户端需要降级成之前的域名访问方式,通过DNS解析到存活的机房。
2.2 RPC框架的异地多活改造
当用户的请求达到了单元机房内,理论上后续所有的操作都是在单元机房完成。
这就要求RPC请求落在就近的机房,那么,怎么知道单元机房的服务信息
所以得物APP的注册中心(Nacos)要做双向同步,这样才能拿到所有机房的服务信息。
不同的机房的Nacos,服务的注册信息采用双向复制,进行同步。
前面也提到了,用户的请求尽量在一个机房内完成闭环,当然,只是尽量,没有说全部。
这是因为有的业务场景不适合划分单元,比如库存扣减。
所以在得物APP的划分里面,有一个机房是中心机房,那些不做多活的业务只会部署在中心机房里面,那么库存扣减的时候就需要跨机房调用。
对于单元服务会存在多个机房的服务信息,如果不进行控制,则会出现调用其他机房的情况,所以RPC框架要进行改造。
2.2.1 定义RPC路由类型
- 默认路由
请求到中心机房,会优先调用中心机房内的服务,如果中心机房无此服务,则调用单元机房的服务,如果单元机房没有此服务则直接报错。
- 单元路由
请求到单元机房,那么说明此用户的流量规则是在单元机房,接下来所有的RPC调用都只会调用单元机房内的服务,没有服务则报错。
- 中心路由
请求到单元机房,那么直接调用中心机房的服务,中心机房没有服务则报错。请求到中心机房,那么就本机房调用。
2.2.2 业务RPC改造
业务方需要对自己的接口(Java interface)进行标记是什么类型,是单元路由,还是中心路由,通过@HARoute加在接口上面。
标记完成后,在Dubbo接口进行注册的时候,会把路由类型放入到这个接口的元数据里面。
在Nacos后台可以查看Dubbo接口的路由类型,这些数据,也是RPC路由异地多活改造的核心参数。
后面通过RPC调用接口内部所有的方法都会按照标记类型进行路由。
比如,单元路由的RPC,RPC在路由的时候会根据这个值判断用户所在的机房。
路由逻辑如下:
RPC 接口复制一份,命名为UnitApi,带上路由参数。在新接口的实现里面调用老接口,新旧接口共存。
2.2.4 遇到的问题
1 其他场景切单元接口
除了RPC直接调用的接口,还有一大部分是通过Dubbo泛化过来的,这块在上线后也需要将流量切到UnitApi,等老接口没有请求量之后才能下线。
2 接口分类整改
接口进行分类,之前没有多活的约束,一个Java interface中的方法可能各种各样,所以需要进行rpc
接口的分类整改
3 业务层面调整
业务层面调整,比如之前查询订单只需要一个订单号,但是现在需要路由参数,所以接入这个接口的上游都需要调整。
2.3 数据库的异地多活
请求顺利的到达了服务层,接下来要跟数据库打交道了。
数据库得物APP定义了不同的类型,定义如下:
1 单元化
此库为单元库,会同时在两个机房部署,每个机房都有完整的数据,数据采用双向同步。
2 中心化
此库为中心库,只会在中心机房部署。
3 中心单元化
此库为中心单元库,会同时在两个机房部署,中心可以读写,其他机房只能读。
中心写数据后单向复制到另一个机房。
2.3.1 DB-Proxy代理中间件
异地多活之前,得物内部的各大服务, 都是客户端形式的Sharding中间件,客户端模式访问分库分表,
要命的是,每个业务方的版本还不一致。
在多活切流的过程中需要对数据库禁写来保证业务数据的准确性,如果没有统一的中间件,这将是一件很麻烦的事情。
所以得物APP调整为 proxy模式,去掉 client模式的分库分表访问。
得物APP 通过对ShardingSphere进行深度定制,二次开发数据库代理proxy中间件 ,彩虹桥。
有了proxy组件之后,各业务方替换之前的Sharding Client方式。
2.3.2 分布式ID
单元化的库,数据层面会做双向同步复制操作。如果直接用表的自增ID则会出现下面的冲突问题:
得物APP采用了一种一劳永逸的方式,接入全局唯一的分布式ID来避免主键的冲突。
所以,分布式ID绝对是 分库分表的核心 技术要点,如果做到 高并发、高性能、防止倾斜,绝对是一大核心的技术难题,
这里,强烈建议大家去看看尼恩 分析了百度ID、推特snowflake ID,shardingjdbc ID三大ID源码之后,定义的异步高并发、防止倾斜、防止时间回拨的高并发ID,一定带给大家N多的启发。
2.3.3 使用OTTER进行数据同步
OTTER是阿里巴巴公司为了解决杭州/美国机房数据间同步研发的一个开源软件。
OTTER基于数据库增量日志解析,准实时同步到本机房或异地机房的mysql/oracle数据库,是一个分布式数据库同步系统。
工作原理图:
原理描述:
- 基于Canal开源产品,获取数据库增量日志数据。
- 典型管理系统架构,manager(web管理)+node(工作节点)
a. manager运行时推送同步配置到node节点
b. node节点将同步状态反馈到manager上 - 基于zookeeper,解决分布式状态调度的,允许多node节点之间协同工作
2.3.4 业务改造
在Dao层对表进行操作的时候,会通过ThreadLocal设置当前方法的ShardingKey,然后通过Mybatis拦截器机制,将ShardingKey通过Hint的方式放入SQL中,带给彩虹桥。
彩虹桥会判断当前的ShardingKey是否属于当前机房,如果不是直接禁写报错。
这里跟大家简单的说明下为什么切流过程中要禁写,这个其实跟JVM的垃圾回收有点相似。如果不对操作禁写,那么就会不断的产生数据,而得物APP切流,一定要保证当前机房的数据全部同步过去了之后才开始生效流量规则,否则用户切到另一个机房,数据没同步完,就会产生业务问题。除了彩虹桥会禁写,RPC框架内部也会根据流量规则进行阻断。
2.3.5 遇到的问题
1 单元接口中不能访问中心数据库
如果接口标记成了单元接口,那么只能操作单元库。
在以前没有做多活改造的时候,基本上没有什么中心和单元的概念,所有的表也都是放在一起的。
多活改造后,得物APP会根据业务场景对数据库进行划分。
划分后,中心库只会被中心机房的程序使用,在单元机房是不允许连接中心库。
所以单元接口里面如果涉及到对中心库的操作,必定会报错。
这块需要调整成走中心的RPC接口。
2 中心接口不能访问单元数据库
跟上面同样的问题,如果接口是中心的,也不能在接口里面操作单元库。中心接口的请求都会强制走到中心机房,如果里面有涉及到另一个机房的操作,也必须走RPC接口进行正确的路由,
因为你中心机房不能操作另一个机房的数据库。
3 批量查询调整
比如批量根据订单号进行查询,但是这些订单号不是同一个买家。
如果随便用一个订单的买家作为路由参数,那么其他一些订单其实是属于另一个单元的,这样就有可能存在查询到旧数据的问题。
这样批量查询的场景,只能针对同一个买家可用,如果是不同的买家需要分批调用。
2.4 Redis 的异地多活
Redis在业务中用的比较多,在多活的改造中也有很多地方需要调整。
对于Redis首先得物APP明确几个定义:
不做双向同步
Redis不会和数据库一样做双向同步,也就是中心机房一个Redis集群,单元机房一个Redis集群。
每个机房的集群中只存在一部分用户的缓存数据,不是全量的。
Redis类型
Redis分为中心和单元,中心只会在中心机房部署,单元会在中心和单元两个机房部署。
2.4.1 业务改造
1 Redis多数据源支持
多活改造前,每个应用都有一个单独的Redis集群,
多活改造后,由于应用没有进行单元化和中心的拆分,所以一个应用中会存在需要连接两个Redis的情况。
一个中心Redis,一个单元Redis。
基础架构组提供的专用Redis Client包,需要支持多数据源的创建,
基础包中并且定义通用的配置格式,业务方只需要在自己 的配置里面指定集群和连接模式即可完成接入。
spring.redis.sources.carts.mode=unit
spring.redis.sources.carts.cluster-name=cartsCuster
具体的Redis实例信息会在配置中心统一维护,不需要业务方关心,
在做机房扩容的时候,业务方是不需要调整的
2 数据一致性
缓存和缓存之间,不进行同步,没有数据一致性问题
缓存和DB之间,使用binlog 进行同步
这里得物APP的方案是采用订阅数据库的binlog来进行缓存的失效操作,可以订阅本机房的binlog,也可以订阅其他机房的binlog来实现所有机房的缓存失效。
使用 binlog 进行同步的实操,非常重要, 具体请参见尼恩的 100wQps 三级缓存组件实操,建议大家一定认真看看。
2.4.2 遇到的问题
1 序列化协议兼容
在接入新的Redis Client包后,测试环境出现了老数据的兼容问题。
有个别应用自己定制了序列化方式,导致Redis按新的方式装配后没有用到自定义的协议,这块也是进行了改造,支持多数据源的协议自定义。
2 分布式锁的使用
目前项目中的分布式锁是基于Redis实现,当Redis有多个数据源之后,分布式锁也需要进行适配。
在使用的地方要区分场景,默认都是用的中心Redis来加锁。
但是单元接口里面的操作都是买家场景,所以这部分需要调整为单元Redis锁对象进行加锁,这样能够提高性能。其他的一些场景有涉及到全局资源的锁定,那就用中心Redis锁对象进行加锁。
2.5 RocketMQ异地多活
所以MQ跟数据库一样,也要做同步,将消息同步到另一个机房的MQ中,至于另一个机房的消费者要不要消费,这就要让业务场景去决定。
2.5.1 定义消费类型
1 中心订阅
中心订阅指的是消息无论是在中心机房发出的还是单元机房发出的,都只会在中心机房进行消费。
如果是单元机房发出的,会将单元的消息复制一份到中心进行消费。
2 普通订阅
普通订阅就是默认的行为,指的是就近消费。在中心机房发送的消息就由中心机房的消费者进行消费,在单元机房发送的消息就由单元机房的消费进行消费。
3 单元订阅
单元订阅指的是消息会根据ShardingKey进行消息的过滤,无论你在哪个机房发送消息,消息都会复制到另一个机房,此时两个机房都有该消息。通过ShardingKey判断当前消息应该被哪个机房消费,符合的才会进行消费,不符合的框架层面会自动ACK。
4 全单元订阅
全单元订阅指的是消息无论在哪个机房发出,都会在所有的机房进行消费。
2.5.2 业务改造
1 消息发送方调整
消息发送方,需要结合业务场景进行区分。如果是买家场景的业务消息,在发消息的时候需要将"多活路由Key"放入消息中,具体怎么消费由消费方决定。
如果消费方是单元消费的话那么必须依赖发送方的"多活路由Key",否则无法知道当前消息应该在哪个机房消费。
2 消息消费方指定消费模式
前面提到了中心订阅,单元订阅,普通订阅,全单元订阅多种模式,到底要怎么选就是要结合业务场景来定的,定好后在配置MQ信息的时候指定即可。
比如中心订阅就适合你整个服务都是中心的,其他机房都没部署,这个时候肯定适合中心订阅。
比如你要对缓存进行清除,就比较适合全单元订阅,一旦数据有变更,所有机房的缓存都清除掉。
2.5.3 遇到的问题
1 消息幂等消费
就算不做多活,消息消费场景,肯定是要做幂等处理的,因为消息本身就有重试机制。
单独拎出来说是在切流的过程中,属于切流这部分用户的消息会被复制到另一个机房重新进行消费,
解释下为什么切流过程中会有消息消费失败以及需要复制到另一个机房去处理,如下图所示:
用户在当前机房进行业务操作后,会产生消息。由于是单元订阅,所以会在当前机房进行消费。
消费过程中,发生了切流操作,消费逻辑里面对数据库进行读写,但是单元表的操作都携带了ShardingKey,彩虹桥会判断ShardingKey是否符合当前的规则,发现不符合直接禁写报错。
这批切流用户的消息就全部消费失败。
等到流量切到另一个机房后,如果不进行消息的重新投递,那么这部分消息就丢失了,这就是为什么要复制到另一个机房进行消息的重新投递。
2 切流场景的消息顺序问题
上面讲到了在切流过程中,会将消息复制到另一个机房进行重新消费,然后是基于时间点去回放的,如果你的业务消息本身就是普通的Topic,
在消息回放的时候如果同一个场景的消息有多条,这个顺序并不一定是按照之前的顺序来消费,所以这里涉及到一个消费顺序的问题。
如果你之前的业务场景本身就是用的顺序消息,那么是没问题的,如果之前不是顺序消息,这里就有可能有问题,我举个例子说明下:
解决方案有下面几种:
- Topic换成顺序消息,以用户进行分区,这样就能保证每个用户的消息严格按照发送顺序进行消费
- 对消息做幂等,已消费过就不再消费。但是这里跟普通的消息不同,会有N条消息,如果对msgId进行存储,这样就可以判断是否消费过,但是这样存储压力太大,当然也可以只存储最近N条来减小存储压力。
- 消息幂等的优化方式,让消息发送方每发送一次,都带一个version,version必须是递增。消费方消费消息后把当前version存储起来,消费之前判断消息的version是否大于存储的version,满足条件才进行消费,这样既避免了存储的压力也能满足业务的需求。
3. 得物异地多活的半单元化
得物异地多活的没有做全单元化,而是半单元化
3.1 整体方向
首先要根据整个多活的一个整体目标和方向去梳理,
比如得物APP的整体方向就是买家交易的核心链路必须实现单元化改造。那么这整个链路所有依赖的上下游都需要改造。
用户浏览商品,进入确认订单,下单,支付,查询订单信息。这个核心链路其实涉及到了很多的业务域,比如:商品,出价,订单,支付,商家等等。
在这些已经明确了的业务域下面,可能还有一些其他的业务域在支撑着,所以要把整体的链路都梳理出来,一起改造。
当然也不是所有的都必须做单元化,还是得看业务场景,比如库存,肯定是在交易核心链路上,但是不需要改造,必须走中心。
3.2 服务类型
3.2.1 中心服务
中心服务只会在中心机房部署,并且数据库也一定是中心库。
可以对整个应用进行打标成中心,这样外部访问这个服务的接口时都会被路由到中心机房。
3.2.2 单元服务
单元服务会在中心机房和单元机房同时部署,并且数据库也一定是单元库。
单元服务是买家维度的业务,比如确认订单,下单。
买家维度的业务,在接口定义上,第一个参数必须是"多活路由Key",因为要进行路由。
用户的请求已经根据规则进行分流到不同的机房,只会操作对应机房里面的数据库。
3.2.3 中心单元服务
中心单元服务也就是说这个服务里面既有中心的接口也有单元的接口,并且数据库也是有两套。
所以这种服务其实也是要在两个机房同时部署的,只不过是单元机房只会有单元接口过来的流量,中心接口是没有流量的。
一些底层的支撑业务,比如商品,商家这些就属于中心单元服务。
支撑维度的业务是没有"多活路由Key"的,商品是通用的,并不属于某一个买家。
而支撑类型的业务底层的数据库是中心单元库,也就是中心写单元读,写请求是在中心进行,比如商品的创建,修改等。
操作后会同步到另一个机房的数据库里面。这样的好处就是可以减少得物APP在核心链路中的耗时,如果商品不做单元化部署,那么浏览商品或者下单的时候查询商品信息都必须走中心机房进行读取。
而现在则会就近路由进行接口的调用,请求到中心机房就调中心机房的服务,请求到单元机房就调单元机房的服务,单元机房也是有数据库的,不需要跨机房。
从长远考虑,还是需要进行拆分,把中心的业务和单元的业务拆开,这样会比较清晰。
4. 异地多活切流方案
所谓切流,就是在⼀个数据中心发生故障或灾难的情况下,将流量切换到其他数据中心,其他数据中心可以正常运行并对关键业务或全部业务进行接管,实现用户的故障无感知。
前面得物APP也提到了再切流过程中,会禁写,会复制MQ的消息到另一个机房重新消费。
接下来给大家介绍下得物APP的切流方案,能够帮助大家更深刻的理解整个多活的异常场景下处理流程。
- 下发禁写规则
当需要切流的时候,操作人员会通过双活控制中心的后台进行操作。
切流之前需要先进行已有流量的清理,需要下发禁写规则。
禁写规则会下发到中心和单元两个机房对应的配置中心里面,通过配置中心去通知需要监听的程序。
- 彩虹桥执行禁写逻辑
彩虹桥会用到禁写规则,当禁写规则在配置中心修改后,彩虹桥能立马感知到,然后会根据SQL中携带的shardingkey进行规则的判断,看当前shardingkey是否属于这个机房,如果不属于则进行拦截。
- 反馈禁写生效结果
当配置变更后会推送到彩虹桥,配置中心会感知到配置推送的结果,然后将生效的结果反馈给双活控制中心。
- 推送禁写生效时间给Otter
双活控制中心收到所有的反馈后,会将全部生效的时间点通过MQ消息告诉Otter。
- Otter进行数据同步
Otter收到消息会根据时间点进行数据同步。
- Otter同步完成反馈同步结果
生效时间点之前的数据全部同步完成后会通过MQ消息反馈给双活控制中心。
- 下发最新流量规则
双活中心收到Otter的同步完成的反馈消息后,会下发流量规则,流量规则会下发到DLB,RPC,彩虹桥。
后续用户的请求就会直接被路由到正确的机房。
5. 得物异地多活的总结
多活是一个高可用的容灾手段,但实现的成本和对技术团队的要求非常高。但是异地多活改造的范围实在是太大了。
本篇主要讲的是中间件层面和业务层面的一些改造点和过程,同时还有其他的一些点都没有提到。
比如:机房网络的建设,发布系统支持多机房,监控系统支持多机房的整个链路监控,数据巡检的监控等等。
没有100%的可用性,异地多活只是在极端场景下对业务的一些取舍罢了,优先保证核心功能。
在实现多活的时候,得物APP应该结合业务场景去进行设计,所以,也不是所有系统,所有功能都要满足多活的条件。
得物异地多活的方案很多很多, 大家有什么具体的问题,也可以来尼恩的高并发社群(50+)里边交流。
后续,尼恩会给大家结合各大互联网的行业案例,分析出更多,更加劲爆的异地多活,大家可以找尼恩来一次性获取这些方案的PDF。
当然,如果大家遇到这类高可用的面试难题,也可以找尼恩求助。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》