记录些Spring+题集(30)

100Wqps异地多活架构

1. 什么是异地多活

异地多活的概念很多,像什么同城双活、两地三中心、三地五中心等概念。要想理解异地多活,需要从架构设计的3高原则说起。

架构设计的3高原则

一个好的软件架构应该遵循以下 3 个原则:

  1. 高性能

  2. 高并发

  3. 高可用

高性能意味着系统拥有更大流量的处理能力,更低的响应延迟。例如 秒可处理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 个方面:

  1. 硬件故障:CPU、内存、磁盘、网卡、交换机、路由器

  2. 软件问题:代码 Bug、版本迭代

  3. 不可抗力:地震、水灾、火灾、战争

这些风险随时都有可能发生。所以,在面对故障时,我们的系统能否以「最快」的速度恢复,就成为了可用性的关键。

常见的多活方案

4 个 9  高可用的核心方案就是异地多活。异地多活指分布在异地的多个站点同时对外提供服务的业务场景。异地多活是高可用架构设计的一种,与传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。

常见的多活方案有同城双活、两地三中心、三地五中心等多种技术方案。

常见方案1:同城双活

同城双活是在同城或相近区域内建立两个机房。同城双机房距离比较近,通信线路质量较好,比较容易实现数据的同步复制 ,保证高度的数据完整性和数据零丢失。

同城两个机房各承担一部分流量,一般入口流量完全随机,内部RPC调用尽量通过就近路由闭环在同机房,相当于两个机房镜像部署了两个独立集群,数据仍然是单点写到主机房数据库,然后实时同步到另外一个机房。

下图展示了同城双活简单部署架构,当然一般真实部署和考虑问题要远远比下图复杂。

图片

服务调用基本在同机房内完成闭环,数据仍然是单点写到主机房数据储存,然后实时同步复制到同城备份机房。当机房A出现问题时候运维人员只需要通过GSLB或者其他方案手动更改路由方式将流量路由到B机房。

同城双活可有效用于防范火灾、建筑物破坏、供电故障、计算机系统及人为破坏引起的机房灾难。

常见方案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绝对是 分库分表的核心 技术要点,如果做到 高并发、高性能、防止倾斜,绝对是一大核心的技术难题。

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 进行同步的实操,非常重要。

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,在消息回放的时候如果同一个场景的消息有多条,这个顺序并不一定是按照之前的顺序来消费,所以这里涉及到一个消费顺序的问题。

如果你之前的业务场景本身就是用的顺序消息,那么是没问题的,如果之前不是顺序消息,这里就有可能有问题,我举个例子说明下:

解决方案有下面几种:

  1. Topic换成顺序消息,以用户进行分区,这样就能保证每个用户的消息严格按照发送顺序进行消费。

  2. 对消息做幂等,已消费过就不再消费。但是这里跟普通的消息不同,会有N条消息,如果对msgId进行存储,这样就可以判断是否消费过,但是这样存储压力太大,当然也可以只存储最近N条来减小存储压力。

  3. 消息幂等的优化方式,让消息发送方每发送一次,都带一个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应该结合业务场景去进行设计,所以,也不是所有系统,所有功能都要满足多活的条件。

蓝绿发布、金丝雀发布、滚动发布、AB测试的原理和实操

蓝绿发布、金丝雀发布、滚动发布、A/B测试 核心原理

蓝绿发布(Blue-green Deployments) 核心原理

蓝绿发布的目的是减少发布时的中断时间能够快速撤回发布

It’s basically a technique for releasing your application in a predictable manner with an goal of reducing any downtime associated with a release. It’s a quick way to prime your app before releasing, and also quickly roll back if you find issues.

蓝绿发布中,一共有两套系统:一套是正在提供服务系统,标记为“绿色”;另一套是准备发布的系统,标记为“蓝色”。

图片

两套系统都是功能完善的,并且正在运行的系统,只是系统版本和对外服务情况不同。

最初,没有任何系统,没有蓝绿之分。然后,第一套系统开发完成,直接上线,这个过程只有一个系统,也没有蓝绿之分。后来,开发了新版本,要用新版本替换线上的旧版本,在线上的系统之外搭建了一个使用新版本代码的全新系统。 这时候,一共有两套系统在运行,正在对外提供服务的老系统是绿色系统,新部署的系统是蓝色系统。

图片

蓝色系统不对外提供服务,用来做啥?

用来做发布前测试,测试过程中发现任何问题,可以直接在蓝色系统上修改,不干扰用户正在使用的系统。(注意,两套系统没有耦合的时候才能百分百保证不干扰)

蓝色系统经过反复的测试、修改、验证,确定达到上线标准之后,直接将用户切换到蓝色系统:

图片

切换后的一段时间内,依旧是蓝绿两套系统并存,但是用户访问的已经是蓝色系统。这段时间内观察蓝色系统(新系统)工作状态,如果出现问题,直接切换回绿色系统。

当确信对外提供服务的蓝色系统工作正常,不对外提供服务的绿色系统已经不再需要的时候,蓝色系统正式成为对外提供服务系统,成为新的绿色系统。 原先的绿色系统可以销毁,将资源释放出来,用于部署下一个蓝色系统。

蓝绿发布只是上线策略中的一种,它不是可以应对所有情况的万能方案。 蓝绿发布能够简单快捷实施的前提假设是目标系统是非常内聚的,如果目标系统相当复杂,那么如何切换、两套系统的数据是否需要以及如何同步等,都需要仔细考虑。

BlueGreenDeployment(https://martinfowler.com/bliki/BlueGreenDeployment.html)中给出的一张图特别形象:

图片

蓝绿发布缺点:切换是全量的,如果新版本有问题,则对用户体验有直接影响, 需要双倍机器资源。

蓝绿发布需要路由或者ingress的配合。

金丝雀发布(anCanary Releases) 核心原理

金丝雀发布(Canary)也是一种发布策略,和国内常说的灰度发布是同一类策略。蓝绿发布是准备两套系统,在两套系统之间进行切换,金丝雀策略是只有一套系统,逐渐替换这套系统。

譬如说,目标系统是一组无状态的Web服务器,但是数量非常多,假设有一万台。这时候,蓝绿发布就不能用了,因为你不可能申请一万台服务器专门用来部署蓝色系统(在蓝绿发布的定义中,蓝色的系统要能够承接所有访问)。

可以想到的一个方法是:  只准备几台服务器,在上面部署新版本的系统并测试验证。测试通过之后,担心出现意外,还不敢立即更新所有的服务器。 先将线上的一万台服务器中的10台更新为最新的系统,然后观察验证。确认没有异常之后,再将剩余的所有服务器更新。这个方法就是金丝雀发布。

金丝雀发布(canary release)的命名原因:

人们发现金丝雀这种生物对于有毒气体很敏感。因此矿工在下井采矿之前会把金丝雀鸟儿投入或携带到矿井中,如果鸟儿能够从矿井中飞出就表示井下有氧气,矿工就可以安心下井采矿了。

通过这个故事,我们就可以看出金丝雀部署就是先把新版本试水的一部分就叫金丝雀发布。金丝雀发布可以快速而有效地发现软件新版本存在的问题。

它的原理就是部署的时候让一小部分用户先试用功能 ,通过日志监控或者服务器监控,看下新用户的反馈。如果没有严重问题,尽快部署这个新版本,否则快速会退。小代价去试错

金丝雀发布(canary release)实际操作中还可以做更多控制,譬如说给最初更新的10台服务器设置较低的权重、控制发送给这10台服务器的请求数,然后逐渐提高权重、增加请求数。

这个控制叫做“流量切分”,既可以用于金丝雀发布,也可以用于后面的A/B测试。金丝雀部署也就是灰度发布的一种方式。

图片

蓝绿发布和金丝雀发布是两种发布策略,都不是万能的。

有时候两者都可以使用,有时候只能用其中一种。

上面的例子中可以用金丝雀,不能用蓝绿,那么什么时候可以用蓝绿呢?整个系统只有一台服务器的时候。或者说有足够的资源,同时支撑运行两套系统的时候。

金丝雀发布缺点: 自动化流程不够,发布期间需要人为去操作,可能会引起服务中断等。

滚动发布的 核心原理

滚动发布是在金丝雀发布基础上的进一步优化改进,是一种自动化程度较高的发布方式,用户体验比较平滑,是目前成熟型技术组织所采用的主流发布方式。一次滚动式发布一般由若干个发布批次组成,每批的数量一般是可以配置的(可以通过发布模板定义)。

例如,第一批1台(金丝雀),第二批10%,第三批 50%,第四批100%。

每个批次之间留观察间隔,通过手工验证或监控反馈确保没有问题再发下一批次,所以总体上滚动式发布过程是比较缓慢的 (其中金丝雀的时间一般会比后续批次更长,比如金丝雀10 分钟,后续间隔 2分钟)。

A/B测试(A/B Testing) 核心原理

首先需要明确的是,A/B测试和蓝绿发布以及金丝雀,完全是两回事。

蓝绿发布和金丝雀是发布策略,目标是确保新上线的系统稳定,关注的是新系统的BUG、隐患。A/B测试是效果测试,同一时间有多个版本的服务对外服务,这些服务都是经过足够测试,达到了上线标准的服务,有差异但是没有新旧之分(它们上线时可能采用了蓝绿发布的方式)。

A/B测试关注的是不同版本的服务的实际效果,譬如说转化率、订单情况等。

A/B版本

一般A/B版本用在创业公司第一次发布新版本时,不清楚顾客更喜欢哪一个新版本的时候用的。

同时部署A和B两个版本,通过后台统计数据,分析顾客更喜欢哪一个版本,然后选择这个版本上线。在新产品抢占市场份额时作用巨大。

A/B测试时,线上同时运行多个版本的服务,这些服务通常会有一些体验上的差异,譬如说页面样式、颜色、操作流程不同。相关人员通过分析各个版本服务的实际效果,选出效果最好的版本。

图片

在A/B测试中,需要能够控制流量的分配,譬如说,为A版本分配10%的流量,为B版本分配10%的流量,为C版本分配80%的流量。

蓝绿发布、金丝雀发布、滚动发布 实操

spring cloud 灰度实操

spring cloud 灰度的实操方案比较多:

方案一:spring cloud gateway也可以实现灰度发布,

方案二:还有一款 spring cloud 的Discovery增强组件,可以实现灰度、蓝绿等功能(https://github.com/Nepxion/Discovery)

简单来说,灰度发布实质是让指定用户访问指定版本的服务。spring cloud gateway也可以实现灰度发布大概的思路:

首先,需要指定用户匹配到指定的路由规则。其次,服务的版本号信息可以通过HTTP请求头字段来指定。最后,负载均衡算法需要能够根据版本号信息来做服务实例的选择。在实操层面,spring cloud gateway 灰度发布的实现思路应该比较简单:

1、首先编写自己的Predicate,实现指定用户匹配到指定的路由规则中;

2、动态修改请求,添加版本号信息,版本号信息可以放在HTTP Header中(此处可以通过原生AddRequestHeaderGatewayFilterFactory来实现,无需自己写代码);

3、自定义路由规则,重写负载均衡算法,根据版本号信息从注册中心的服务实例上选择相应的服务版本进行请求的转发。

反向代理网关灰度实操

利用云原生网关比如 APISIX :

  • https://apisix.apache.org/zh/docs/apisix/plugins/traffic-split/#蓝绿发布

  • https://apisix.apache.org/zh/docs/apisix/plugins/traffic-split/#灰度发布

Kubernetes 中的灰度策略

Kubernetes 中常见的发布策略主要有如下六种:

重建(recreate)  :即停止一个原有的容器,然后进行容器的新建。

滚动更新(rollingUpdate) :停掉一个容器,然后更新一个容器。

蓝绿布署(blue/green ):准备一套蓝色的容器和一套绿色的容器,进行流量切换。

金丝雀发布(canary) :更新部分容器,没有问题后进行逐步替换,直到切完。

A/B测试发布:即将发布的结果面向部分用户,这块没有现成的组件,需要进行自行处理,比如使用 Istio、Linkerd、Traefik 等。这种方式采用在 Http 的 Header 上进行处理。

无损发布:现在很多发布都是将容器停掉,当没有请求的时候发布,实现无损发布。

Deployment金丝雀部署:按照流量比例

金丝雀部署就是将部分新版本发在旧的容器池里边,然后进行流量观察,比如 30% 的流量切到新服务上,60% 的流量还在旧服务上。这里,采用 svc 的方式进行部署的选择,这里使用 label 进行 pod的选择。两个 Deployment 文件如下:

图片

app-v1-canary.yaml里边有 2 个 pod 支撑这个服务。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
  labels:
    app: my-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
      version: v1.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v1.0.0
    spec:
      containers:
        - name: my-app
          image: nien/nginx-gateway:v0.0.1
          ports:
            - name: http
              containerPort: 8008
            - name: probe
              containerPort: 8008
          env:
            - name: VERSION
              value: v1.0.0
            - name: env_flag
              value: VERSION-v1.0.0
          livenessProbe:
            httpGet:
              path: /env
              port: probe
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /env
              port: probe
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
    - name: http
      port: 8008   #对应deployment的容器端口
      targetPort: http
      nodePort: 30808 #外部端口
  selector:
    app: my-app

接下来,我们创建 v2 版本 app-v2-canary.yaml 文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
      version: v2.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v2.0.0
    spec:
      containers:
        - name: my-app
          image: nien/nginx-gateway:v0.0.1
          ports:
            - name: http
              containerPort: 8008
            - name: probe
              containerPort: 8008
          env:
            - name: VERSION
              value: v2.0.0
            - name: env_flag
              value: VERSION-v2.0.0
          livenessProbe:
            httpGet:
              path: /env
              port: probe
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /env
              port: probe
            periodSeconds: 5

说一下要执行的步骤:

  1. 启动 V1 的服务版本:2 个复本。

  2. 启动 V2 的服务版本:1 个复本。

  3. 观察 V2 流量正常的情况的话,那么启动 V2 的 2 个复本。

  4. 删除 V1 的 2 个复本,流量全部到 V2 上。

step1 :启动 V1 服务,查看服务是否正确,然后观察一下服务。
cd  /vagrant/chapter28/k8s/deployPolicy

kubectl apply -f app-v1-canary.yaml

kubectl get svc -l app=my-app

curl http://192.168.49.2:30808

图片

watch kubectl get pod  ,能看到两个 pod

图片

step2:启动 V2 的服务版本:1 个复本

新打开容器,执行命令,启动 V2 服务。上边的 watch 将观察到新增了 1 个 pod, 此时共有 3 个 pod, 2.0.0 的版本已经上来了。

cd  /vagrant/chapter28/k8s/deployPolicy

kubectl apply -f app-v2-canary.yaml

while sleep 1; do curl http://192.168.49.2:30808 | egrep VERSION; done

图片

此时我们观察版本 2 的服务是否正确。

step3: 观察 V2 流量正常的情况的话,那么启动 V2 的 2 个复本。

图片

v2能收到分发的流量,是正确的

如果正确,那么我们将版本 2 扩展到 2个副本。

kubectl scale --replicas=2 deploy my-app-v2

执行如下

图片

watch 将观察到新增了 1 个 pod, 此时共有4个 pod, 2.0.0 的版本已经上来了。

图片

这个时候版本 1 和版本 2 一样了。

step4:删除 V1 的 2 个复本,流量全部到 V2 上。

我们再将老版本删除, 将 版本 1 的2个 pod 给清除掉

kubectl delete deploy my-app-v1

图片

清除之后,watch的效果,只剩新版本的2个pod,到此为止,  金丝雀发布完成

图片

注意:因为有部分版本在线上运行,我们能够对其日志进行观察和追踪、定位问题;如果有问题也能快速将新版本清理掉;

kubectl delete deploy my-app-v1
实操总结

从实操的过程中,发现金丝雀发布较慢;但是风险比较小,如果对代码信心不足的情况,  可以采用此方法,影响范围较小。实验完成,可以清理所有服务

kubectl delete all -l app=my-app

图片

上面是按照流量的pod的比例,进行 流量的控制。

Service会对提供同一个服务的多个Pod进行聚合,并且提供一个统一的入口地址,通过访问Service的入口地址就能访问到后面的Pod服务。

图片

Service底层的 流量分发策略,默认为随机或者 轮询,具体与proxy代理的策略有关系。但是无论 随机或者 轮询, 整体上都是按照pod的比例,进行流量的分发。除了按照pod的比例分流,如果你想用更精细粒度的话,可以使用 滚动发布。如下图所示:

图片

Deployment实现滚动发布

利用Deployment的滚动更新策略maxSurge和maxUnavailable设置最大可超期望的节点数和最大不可用节点数可实现简单的金丝雀发布。

rollingUpdate.maxSurge最大可超期望的节点数,百分比 10% 或者绝对数值 5

rollingUpdate.maxUnavailable最大不可用节点数,百分比或者绝对数值

滚动更新步骤:

  1. 准备一个新版本的 POD,比如新版本为 V2,旧版本为 V1。

  2. 当 V2 版本的 POD 准备好后,在负载均衡中的实例池中将 V2 的版本加入。

  3. 在实例池中剔除一个 V1 版本的 POD。

  4. 逐个实例替换,直到每个版本都是 V2 版本。

如下图所示:

图片

滚动更新使用的 YAML 方式如下:

spec:
  replicas: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 一次可以添加多少个Pod
      maxUnavailable: 1  # 滚动更新期间最大多少个Pod不可用

我们准备两个版本, app-v1.yaml 文件。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v1.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v1.0.0
    spec:
      containers:
        - name: my-app
          image: nien/nginx-gateway:v0.0.1
          ports:
            - name: http
              containerPort: 8008
            - name: probe
              containerPort: 8008
          env:
            - name: VERSION
              value: v1.0.0
            - name: env_flag
              value: VERSION-v1.0.0
          livenessProbe:
            httpGet:
              path: /env
              port: probe
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /env
              port: probe
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
    - name: http
      port: 8008   #对应deployment的容器端口
      targetPort: http
      nodePort: 30808 #外部端口
  selector:
    app: my-app

然后我们再准备一下滚动更新的 v2 版本,app-v2-rolling.yaml 文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
  labels:
    app: my-app
spec:
  replicas: 3
  # maxUnavailable设置为0可以完全确保在滚动更新期间服务不受影响,还可以使用百分比的值来进行设置。
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: my-app
      version: v2.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v2.0.0
    spec:
      containers:
        - name: my-app
          image: nien/nginx-gateway:v0.0.1
          ports:
            - name: http
              containerPort: 8008
            - name: probe
              containerPort: 8008
          env:
            - name: VERSION
              value: v2.0.0
            - name: env_flag
              value: VERSION-v2.0.0
          livenessProbe:
            httpGet:
              path: /env
              port: probe
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /env
              port: probe
            periodSeconds: 5

接下来,我们按如下步骤进行操作:

  1. 启动 app-v1 应用

  2. 启动 app-v2-rolling 应用

  3. 观察所有容器版本变为 V2 版本

step1 :启动 V1 服务,查看服务是否正确,然后观察一下服务。
cd  /vagrant/chapter28/k8s/deployPolicy

kubectl apply -f app-v1.yaml

watch kubectl get pod 

kubectl get svc -l app=my-app

curl http://192.168.49.2:30808

while sleep 1; do curl http://192.168.49.2:30808 | egrep VERSION; done

图片

启动 app-v1 应用并观察,都已经启动,我们进行接口调用。

step2: 启动 app-v2-rolling 进行滚动发布

打开一个新的窗口,然后执行滚动更新命令

kubectl apply -f app-v2-rolling.yaml

在 watch 的终端观察,开始的时候并没删除旧的 pod,而是创建好新的 pod 后

图片

step3:观察所有容器版本变为 V2 版本

然后进行挨个替,我们可以使用如下命令观察接口请求, 渐渐的有了第二个版本的请求。

while sleep 1; do curl http://192.168.49.2:30808 | egrep VERSION; done

图片

在这个过程中,我们发现第二个版本有问题,我们需要进行回滚,此时我们需要执行一下如下命令

kubectl rollout undo deploy my-app

如果想两个版本都观察一下,这个时候需要执行命令。

kubectl rollout pause deploy my-app

如果发现第二个版本没有问题,那么我们要恢复执行

kubectl rollout resume deploy my-app

最后我们清理一下所有的部署

kubectl delete -l app=my-app
实操总结

从实操的过程中,发现:滚动部署没有控制流量的情况;各个版本部署的时候需要一定的时间。实验完成,可以清理所有服务。

kubectl delete all -l app=my-app

Deployment实现蓝绿部署

我们需要准备两个版本,一个蓝版本,一个绿蓝本,这两个版本同时存在,如下图所示:

图片

且两个版本是独立的,这个时候可以瞬间对两个版本的切换。接下来,我们采用 svc 的方式,通过 label selector 来进行版本之间的选择。

selector:
  app: my-app
  version: v1.0.0

我们创建一下 app-v1-svc.yaml 文件,我们首先创建一个 svc 服务,然后通过 label selector 来指定一下版本。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
  labels:
    app: my-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
      version: v1.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v1.0.0
    spec:
      containers:
        - name: my-app
          image: nien/nginx-gateway:v0.0.1
          ports:
            - name: http
              containerPort: 8008
            - name: probe
              containerPort: 8008
          env:
            - name: VERSION
              value: v1.0.0
            - name: env_flag
              value: VERSION-v1.0.0
          livenessProbe:
            httpGet:
              path: /env
              port: probe
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /env
              port: probe
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
    - name: http
      port: 8008   #对应deployment的容器端口
      targetPort: http
      nodePort: 30808 #外部端口
  selector:
    app: my-app
    # 注意这里我们匹配 app 和 version 标签,当要切换流量的时候,我们更新 version 标签的值,比如:v2.0.0
    version: v1.0.0

接下来,我们再创建另一个版本 app-v2.yaml 文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
      version: v2.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v2.0.0
    spec:
      containers:
        - name: my-app
          image: nien/nginx-gateway:v0.0.1
          ports:
            - name: http
              containerPort: 8008
            - name: probe
              containerPort: 8008
          env:
            - name: VERSION
              value: v2.0.0
            - name: env_flag
              value: VERSION-v2.0.0
          livenessProbe:
            httpGet:
              path: /env
              port: probe
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /env
              port: probe
            periodSeconds: 5

接下来,我们按如下步骤执行:

  1. 启动版本 1 服务

  2. 启动版本 2 服务

  3. 将版本 1 服务切换到版本 2,观察服务情况

step1 :启动 V1 服务,查看服务是否正确,然后观察一下服务。

启动版本的服务并观察,没有问题。

cd  /vagrant/chapter28/k8s/blueGreenPolicy

kubectl apply -f app-v1-svc.yaml

watch kubectl get pod 

kubectl get svc -l app=my-app

curl http://192.168.49.2:30808

while sleep 1; do curl http://192.168.49.2:30808 | egrep VERSION; done
step2:启动 V2 的服务版本

启动版本 2 的服务, 因为版本 2 没有挂到 SVC,所以没有办法观察,但是我们可以先启动。

kubectl apply -f app-v2.yaml
step3:将版本 1 服务切换到版本 2,观察服务情况

这个时候我们进行版本的切换,通过切换标签的方式

kubectl patch service my-app   -p'{"spec":{"selector":{"version":"v2.0.0"}}}'  

同时,当发现问题的时候,我们再把版本切回到 v1.0.0 即可。

kubectl patch service my-app   -p'{"spec":{"selector":{"version":"v1.0.0"}}}'  

图片

看看请求的变化

图片

patch

如果一个pod已经在运行,这时需要对pod属性进行修改,又不想删除pod,或不方便通过replace的方式进行更新,这时就可以使用patch命令。

命令格式

kubectl patch \
        (-f FILENAME | TYPE NAME) \
        [-p PATCH|--patch-file FILE] \
        [options]
实操总结

从实操的过程中,发现:蓝绿部署需要准备两套资源,相对有的时候需要的资源会多; 这种情况可以快速还原版本,不会出现滚动更新那样,回滚需要的时间会很久。实验完成,可以清理所有服务。

kubectl delete all -l app=my-app

Ingress Annotations实现金丝雀发布

Ingress-Nginx 是一个K8S ingress工具,支持配置 Ingress Annotations 来实现不同场景下的灰度发布和测试。 Nginx Annotations 支持以下 4 种 Canary 规则:

  • nginx.ingress.kubernetes.io/canary-by-header:基于 Request Header 的流量切分,适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always时,请求将会被一直发送到 Canary 版本;当 Request Header 设置为 never时,请求不会被发送到 Canary 入口;对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他金丝雀规则进行优先级的比较。

  • nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的 Request Header 的值,用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时,它将被路由到 Canary 入口。该规则允许用户自定义 Request Header 的值,必须与上一个 annotation (即:canary-by-header)一起使用。

  • nginx.ingress.kubernetes.io/canary-weight:基于服务权重的流量切分,适用于蓝绿部署,权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求。权重为 100 意味着所有请求都将被发送到 Canary 入口。

  • nginx.ingress.kubernetes.io/canary-by-cookie:基于 Cookie 的流量切分,适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的cookie。当 cookie 值设置为 always时,它将被路由到 Canary 入口;当 cookie 值设置为 never时,请求不会被发送到 Canary 入口;对于任何其他值,将忽略 cookie 并将请求与其他金丝雀规则进行优先级的比较。

注意:金丝雀规则按优先顺序进行如下排序:

canary-by-header - > canary-by-cookie - > canary-weight

我们可以把以上的四个 annotation 规则可以总体划分为以下两类:

  • 基于权重的 Canary 规则

图片

  • 基于用户请求的 Canary 规则

图片

注意: Ingress-Nginx 实在0.21.0 版本 中,引入的Canary 功能,因此要确保ingress版本OK

Ingress Annotations实现金丝雀发布 实操

华为云的金丝雀发布

采用金丝雀部署,你可以在生产环境的基础设施中小范围的部署新的应用代码。一旦应用签署发布,只有少数用户被路由到它。最大限度的降低影响。如果没有错误发生,新版本可以逐渐推广到整个基础设施。

下图示范了金丝雀部署:

图片

下图为华为云的金丝雀发布界面:

图片

图片

步骤一:将流量从待部署节点移出,更新该节点服务到待发布状态,将该节点称为金丝雀节点;

步骤二:根据不同策略,将流量引入金丝雀节点。

策略可以根据情况指定,比如随机样本策略(随机引入)、狗粮策略(就是内部用户或员工先尝鲜)、分区策略(不同区域用户使用不同版本)、用户特征策略(这种比较复杂,需要根据用户个人资料和特征进行分流,类似于千人千面);

步骤三:金丝雀节点验证通过后,选取更多的节点称为金丝雀节点。

重复步骤一步骤二,直到所有节点全部更新。金丝雀部署和蓝绿有点像,但是它更加规避风险。可以阶段性的进行,而不用一次性从蓝色版本切换到绿色版本。

参考

https://blog.csdn.net/qq_42494960/article/details/119385952

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值