作者:扬少

历史上 90%的故障源于业务新版本上线,如何最大化保障功能迭代过程中业务流量无损一直是开发者比较关心的问题。尤其对于分布式架构的微服务应用而言,服务之间的依赖关系错综复杂,一个业务功能需要多个微服务共同提供能力,一次业务请求需要经过多个微服务才能完成处理,牵一发而动全身。业务的发展需要应用系统不断的迭代,我们无法避免应用频繁变更发版,但是我们可以提升应用升级过程中的稳定性和高可用。

场景分析

首先,我们来看一个真实的业务场景。如下是某电商公司的业务架构,包含用户中心(User)、购物车(Chart)和订单中心(Order),后端微服务统一通过网关对外提供服务,其中开发框架采用 Spring Boot 和 Dubbo,注册中心使用的 Nacos。其中下单请求的调用链路为:网关->User->Chart->Order。

 title=

随着用户群体和业务规模不断扩大,原有的下单功能逐渐暴露出了很多设计和程序上的缺陷。业务开发者针对这些亟待解决的问题进行了重新设计和优化,需要对线上的用户中心和订单中心同时发布新版本进行修复。在对系统中已有服务进行版本升级过程中,开发者的操作步骤一般为:

  1. 在正式上线之前,小流量验证新版本是否符合预期
  2. 验证符合预期之后,开始逐步下线服务的老版本
  3. 同时逐步上线服务的新版本,直到所有的流量切到新版本为止 

这种发布的处理方式是正确的,但是这些步骤中仍然有非常多的细节要考虑,遗漏或未处理好任何一个细节,都有可能影响线上流量,甚至引发故障,进而造成业务有损,用户体验下降。接下来会对每一个环节进行详细分析其中的痛点和挑战。

灰度验证

对于传统的单体应用来说,所有的服务模块耦合在一个应用进程中,灰度验证的实现相对简单。如果采用的是基于流量比权重的方式进行灰度引流,那么只需要在前面四层代理对应用新老版本的 IP 进行权重分配即可;如果采用的是基于请求内容的方式进行小流量验证,比如基于请求的 Header、路径参数或者 Cookie 的灰度方式,那么需要在应用前面的七层代理网关上配置对应的灰度路由。

在分布式微服务架构中,应用中被拆分出来的子服务都是独立部署、运行和迭代的,服务之间的依赖关系错综复杂,有时某个功能发版依赖多个服务同时升级上线。这时,对新功能验证时就涉及到了对多个服务同时灰度的问题,通过构建从网关到整个后端服务的环境隔离来对多个不同版本的服务进行灰度验证,这就是微服务架构中特有的全链路灰度场景,如图所示:

 title=

针对刚刚提到的业务场景,需要在集群中部署用户中心和订单中心的新版本,然后需要配置合适的灰度策略,确保灰度流量能够正确的流经这些服务的新版本。这里,有几个核心问题要解决:

  1. 端到端的灰度策略如何实现
  2. 每一跳的灰度节点如何识别
  3. 每一跳的流量容灾如何实现

正式上线—老版本下线

在小流量灰度验证环节,我们通过真实的线上业务流量和人工模拟的各种业务极端 case 的灰度请求,已经充分验证了新版本的正确性,现在开始对服务的老版本进行下线操作。

看似简单的下线操作,其实暗藏玄机。由于微服务应用自身调用特点,在高并发下,服务提供端应用实例的直接下线,会导致服务消费端应用实例无法实时感知下游实例的实时状态,因而出现继续将请求转发到已下线的实例从而出现请求报错,流量有损。

 title=

为什么会出现上述问题呢?我们从客户端视角和服务端视角分别来分析。

 title=

当服务端应用的某个节点下线时,由于注册中心的心跳保活机制存在时间窗口范围内的延迟,导致注册中心感知服务端下线的事件滞后,进一步延迟了客户端感知服务端下线的时间。在客户端收到下线事件之前,线上的流量仍然会被负载到已下线的节点,导致业务流量有损。

整个过程不仅依赖注册中心感知节点变更的时效性,还严重依赖客户端订阅的实现逻辑。比如,Spring Cloud 框架为了在可用性和性能方面做平衡,消费者默认是 30s 去注册中心拉取最新的服务列表,因此服务端实例的下线事件被客户端感知的时间存在较大的时间差,进一步扩大了调用已下线节点的持续时间。

如何确保服务老版本下线过程中流量无损呢?

正式上线—新版本上线

在服务老版本的下线过程中,我们需要逐步完成新版本上线,确保机器的容量满足线上流量规模。

同样,新版本上线过程中也是危机重重。当服务的新节点启动之后,注册中心会感知服务新节点上线事件并通知客户端拉取服务的最新节点信息列表,客户端更新连接池信息并加入新节点,新的访问请求会被负载到新节点上。由于新节点启动之后,需要进行一些资源初始化和预热操作。这时,如果大量的请求在资源初始化完毕之前到来,会导致请求出现超时或出错,甚至直接导致新节点宕机不可用。

 title=

以下举例是 Java 服务新节点启动之后一般涉及到的操作:

 title=

  1. 应用初始化:加载必须的类,装配处理器,监听连接
  2. 服务注册:向注册中心登记服务以及对应的节点信息
  3. 节点就绪:容器化应用上设置的就绪探针检查通过
  4. 流量进入:延迟加载所需的类,热点代码 JIT,缓存需要回源,数据库需要建链 

只有流量进入以后,服务所需的资源才被动加载。在大规模流量场景下,因资源加载消耗一定的硬件资源,此时大量请求得不到及时的响应,出现短暂的超时或错误,甚至拖垮其他上下游的服务,造成严重的业务不可用的后果。

如何确保服务新版本上线过程中流量无损呢?

解决方案

在上一节我们针对应用发布新版本过程中涉及到痛点进行了详细分析,这一节会给出一些比较好的解决方案供开发者参考。

全链路灰度

如何在实际业务场景中去快速落地全链路灰度呢?目前,主要有两种解决思路,基于物理环境隔离和基于逻辑环境隔离。

  • 物理环境隔离

物理环境隔离,顾名思义,通过增加机器的方式来搭建真正意义上的流量隔离。

 title=

这种方案需要为要灰度的服务搭建一套网络隔离、资源独立的环境,在其中部署服务的灰度版本。这个方案一般用于企业的测试、预发开发环境的搭建,对于线上灰度发布引流的场景来说其灵活性不够。况且,微服务多版本的存在在微服务架构中是家常便饭,需要为这些业务场景采用堆机器的方式来维护多套灰度环境。如果您的应用数目过多的情况下,会造成运维、机器成本过大,成本和代价远超收益;如果应用数目很小,就两三个应用,这个方式还是很方便的,可以接受的。

  • 逻辑环境隔离

另一种方案是构建逻辑上的环境隔离,我们只需部署服务的灰度版本,流量在调用链路上流转时,由流经的网关、各个中间件以及各个微服务来识别灰度流量,并动态转发至对应服务的灰度版本。如下图:

 title=

上图可以很好展示这种方案的效果,我们用不同的颜色来表示不同版本的灰度流量,可以看出无论是微服务网关还是微服务本身都需要识别流量,根据治理规则做出动态决策。当服务版本发生变化时,这个调用链路的转发也会实时改变。相比于利用机器搭建的灰度环境,这种方案不仅可以节省大量的机器成本和运维人力,而且可以帮助开发者实时快速的对线上流量进行精细化的全链路控制。

那么全链路灰度具体是如何实现呢?通过上面的讨论,我们需要解决以下问题:

  1. 链路上各个组件和服务能够根据请求流量特征进行动态路由 —— 标签路由
  2. 需要对服务下的所有节点进行分组,能够区分版本 ——  节点打标
  3. 需要对流量进行灰度标识、版本标识 —— 流量染色、分布式链路追踪 

如果想为现有的业务接入全链路灰度能力,不可避免的需要为业务使用的网关以及后端服务开发框架 SDK 进行改造。首先,需要支持动态路由功能,对于 Spring Cloud、Dubbo 开发框架,可以对出口流量实现自定义 Filter,在该 Filter 中完成流量识别以及标签路由。同时需要借助分布式链路追踪技术完成流量标识链路传递以及流量自动染色。此外,需要引入一个中心化的流量治理平台,方便各个业务线的开发者定义自己的全链路灰度规则。

无损下线

下线过程中出现流量有损的根本原因,在于应用提供者节点在停止服务前没有确保已经通知到所有消费者不再调用自己,也无法确保在处理完所有请求之后再停止应用。所以新发版的应用,即使业务代码没有任何问题,也可能在发布过程影响用户的体验。

要想达到无损下线的效果,就要依次解决上面的问题。

  1. 服务提供者待下线节点在主动从注册中心注销之后,并持续运行一小段时间之后,再退出并停止运行
  2. 服务提供者待下线节点主动告知客户端下线事件,避免后续新请求访问

 title=

开源的微服务框架本身是不具备这些能力,开发者需要深入了解各个微服务框架的实现细节,需要设计好 PreStop 脚本,需要修改底层通信框架来做主动通知机制,并要确保在途的请求都处理完毕才能下线。此外,还需要修改现有的服务订阅 SDK,配合主动通知机制触发客户端主动刷新。

无损上线

上线过程中出现流量有损的根本原因,是因为在某些场景下,服务提供者需要经过一段时间资源预热才能正常地接收大流量的请求并成功返回。同时在 K8s 场景下,还需要和 K8s 中的 readiness 、滚动发布等生命周期紧密结合,才能确保应用发布过程中能不出现业务报错。

一般场景下,刚发布的微服务应用实例跟其他正常实例一样一起平摊线上总 QPS。要想达到无损上线的效果,就是控制负载到新节点上流量是渐进式递增的,给予新节点充足的时间进行资源预热。

 title=

目前, 开源 Dubbo 所实现的小流量服务预热过程原理如下:

 title=

服务提供端在向注册中心注册服务的过程中,将自身的预热时⻓ WarmupTime、服务启动时间 StartTime 通过元数据的形式注册到注册中心中,服务消费端在注册中心订阅相关服务实例列 表,调用过程中根据 WarmupTime、StartTime 计算个实例所分批的调用权重。刚启动 StartTime 距离调用时刻差值较小的实例权重下,从而实现对刚启动应用分配更少流量实现对其进行小流量预热。

Dubbo 提供的方案是线性增长的,那么如何做到刚开始慢,后面越来越快呢?如何建设无损上线的可观测体系呢?这样可以更方便的了解应用上线过程中各个环节的状况。

无侵入式解决方案

上面我们分析了问题并给出了对应的解决方案,无外乎都需要开发者对现有的网关和微服务框架进行改造,存在一定的开发成本和维护成本,也给业务发展带来了不稳定的因素。目前,我们结合阿里巴巴十几年双十一大促经验总结做成云产品,对外输出这些能力。用户可以以低成本、无侵入的方式接入,享受沉淀多年的微服务治理能力。

  • 单体应用的灰度发布

对于业务规模比较小的企业,可能更倾向于选择单体应用。对于单体应用的灰度发布来说,可以直接利用云原生网关支持的基于权重或者基于请求内容的灰度路由即可完成灰度发布。

 title=

  • 微服务应用的灰度发布

对于采用微服务架构且业务规模较大的企业,一次新功能上线可能要涉及到多个微服务同时发版上线,这时具备端到端的环境隔离特性的全链路灰度可以非常方便助力业务快速迭代。针对微服务应用场景,用户可以参考以下两种方案来助力业务快速迭代发展。

1. 云原生网关 + MSE 微服务治理

  • 云原生网关将流量网关、微服务网关和安全网关三合一,支持标准 K8s Ingress,兼容 Nginx Ingress Annotaition,支持标签路由、流量染色、路由 Fallback,支持多种服务发现方式。
  • MSE 微服务治理提供一站式微服务治理解决方案,无侵入增强主流 Spring Cloud, Apache Dubbo 等开源微服务框架

 title=

2. 云原生网关 + EDAS

企业级分布式应用服务 EDAS 是一个应用托管和微服务管理的云原生 PaaS 平台,提供应用开发、部署、监控、运维等全栈式解决方案。目前,云原生网关与 EDAS 完成了集成。容器用户习惯使用标准 Ingress 资源定义路由规则来对外提供服务,可以在 EDAS 平台选择 MSE Ingress 轻松完成服务对外暴露。

 title=

动手实践

目前,我们针对微服务场景和 K8s 服务场景下的灰度发布,分别创建了免费的实验室,欢迎感兴趣的读者前来体验。

  1. 基于云原生网关实现微服务的多版本线上灰度https://developer.aliyun.com/...
  2. 基于 MSE 实现 K8s 集群内多服务的灰度发布https://developer.aliyun.com/...

总结

本文详细分析了应用发布新版本过程中遇到的问题以及应对方法,最后给出了低成本、无侵入式的 MSE 云原生网关+MSE 微服务治理、MSE 云原生网关+EDAS 的解决方案。

MSE 云原生网关、注册配置中心首购 8 折优惠,首购 1 年及以上 7 折优惠

扫码了解更多产品信息~

 title=


阿里云云原生
1k 声望302 粉丝