为了解决这一矛盾,在软件开发领域催生出了灰度发布的概念。灰度发布是一种渐进式的软件发布方式,它允许将新功能或更新逐步推送给一部分用户,而不是一次性全部发布。这样的方式能够有效降低线上故障的风险,保障用户体验,同时也为开发团队提供了更多时间和机会在全面发布前进行验证和修复。
然而,随着软件架构的演进,尤其是微服务架构的普及,软件系统往往由多个微服务组成,不同服务的版本升级需要协调和同步。在这种背景下,单一服务的灰度发布已经不能完全适应需求,全链路灰度发布应运而生。
全链路灰度发布考虑到整个软件系统的多个微服务,允许多个微服务同时进行版本控制和升级,以确保整个系统的平稳过渡和稳定性,是一种更为全面和细致的灰度发布方式。通过全链路灰度发布,开发团队能够更加精确地控制不同服务版本的发布比例,降低系统风险,保障线上稳定性,最大程度地满足用户需求。
图片
以图 1 为例,软件系统包含网关和 4 个微服务,通过全链路灰度发布,可以使ServiceB 和 ServiceD 进行灰度发布,通过灰线所示的流量进行灰度功能验证,同时不影响蓝线所示的正常访问流量。
2全链路灰度发布核心问题
我们通过图1 可以清晰地看到,在实施全链路灰度发布时,需要部署相关服务的灰度版本,并确保在整个请求的调用链上,网关和微服务组件能够准确识别正式流量和特定版本灰度流量,并根据流量类型动态地将请求路由到正确版本的上游微服务上。所以,我们需要解决以下几个问题:
- 微服务实例具有版本信息,针对不同流量对应版本实例提供服务;
- 请求流量具有流量特征,可以区分出是请求不同版本微服务的流量;
- 调用链上各组件可以根据流量特征将请求动态路由到正确版本的微服务上。当前的微服务架构主要分为两类模式:一类是建立在传统微服务框架(例如Spring Cloud等)之上的微服务体系,另一类是云原生时代采用Kubernetes和服务网格(如Istio)构建的微服务架构,为了方便起见,我们姑且简称为传统模式和云原生模式。下面我们将介绍在两类模式下,如何解决以上问题,进行全链路灰度发布。
微服务标识
我们通过给微服务实例添加标识的方式,使微服务实例具有版本信息,针对不同流量对应版本实例提供服务。
传统模式
在传统模式下,一般需要产品根据使用的微服务框架决定添加标识的方式。我们以基于 Spring Cloud 框架 + Nacos注册中心 的微服务为例,一般通过在微服务元数据配置(spring.cloud.nacos.discovery.metadata)中添加标识,配置示例如下:
spring:
cloud:
nacos:
discovery:
metadata:
version: ${APP-VERSION:v1}
微服务在 Nacos 注册中心中服务信息如下:
图片
云原生模式
在云原生模式下,为微服务实例添加标识更加方便,不需要修改微服务代码配置,一般在微服务 Deployment 的 Pod 模版中添加Labal标识即可,实例如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
spec:
selector:
matchLabels:
app: demo
version: v1
template:
metadata:
labels:
app: demo
version: v1
流量标识
和微服务标识相同,我们也是通过为流量添加不同标识的方式区分流量,流量标识在业界还有一个更专业更形象的叫法叫流量染色。
流量染色分两步:第一步在流量源头染色,第二步在调用链内染色。
第一步比较简单,通过前端或者网关根据流量特点(比如浏览器类型,用户标识,地域标识等)为流量添加标识(比如在HTTP Header中添加version标签)。
第二步是全链路灰度中最核心也是最复杂的部分,需要流量标识在调用链中透传下去,保证调用链上的每个微服务组件都能根据标识识别流量并动态路由。
传统的微服务框架注重基础功能的实现,例如服务注册与发现、负载均衡等,而未将流量标识透传作为核心特性之一,导致在实际应用中难以实现流量标识的无缝传递。
云原生时代的 Kubernetes + Istio 同样无法帮助微服务实现流量标识透传。不少人存在误解,认为 Istio 能对微服务实例的出入流量进行拦截,应该原生支持流量标识透传,但实际上 Istio 本身没有流量标识透传的能力。
图片
Istio Sidecar 对微服务入口和出口流量拦截如上图所示,Sidecar 虽然能将入口流量1 拦截后转给微服务容器(入口流量2),也能将微服务容器出口流量 3 拦截并转发到 Pod 外(出口流量4),但 Sidecar 不知道出口流量 3 和入口流量 2 的对应关系,在实际情况中,Sidecar 会拦截很多的出口流量,也会拦截很多的入口流量,但 Sidecar 并不知道某一个出口流量对应哪个入口流量。只有微服务应用知道对应关系,因为微服务应用亲自做了流量处理(微服务应用收到入口流量请求2后,进行业务逻辑处理,然后再发出出口流量3,请求下一级微服务),所以 Istio 虽然能对微服务出入流量拦截,但不知道出入流量的对应关系,无法将入口流量的标识自动添加到出口流量上,无法做流量标识透传。
流量标识透传方式
那么如何进行流量标识透传呢,通常有以下3种方式:
微服务修改源码方式
微服务侧进行业务代码改造,从入口流量请求中获取流量标识,并在出口流量中添加流量标识,代码示例如下:
// 从请求中获取流量标识 version
String versionValue = request.getHeader("version");
// 构造新请求需要的 Header,获取到的流量标识添加到新请求的 Header 中
HttpHeaders headers = new HttpHeaders();
headers.set("version", versionValue);
// 发起出口流量请求
使用基础SDK方式
将从入口流量请求中获取流量标识,并在出口流量中添加流量标识这种共性逻辑封装到基础 SDK 中,其原理通常涉及 SDK 对请求和响应的拦截处理。这种方法的核心在于SDK能够拦截到微服务内部的请求,从请求中获取流量标识,并在微服务发起外部请求时,将这个标识加入请求中,实现流量标识的透传。基础 SDK 的工作机制一般包括以下几个关键步骤:
- 拦截请求和响应: SDK会通过某种方式(例如AOP、拦截器等)拦截微服务的请求和响应,这使得SDK能够在请求进入微服务之前或响应返回之后对其进行处理。
- 获取流量标识: 在请求被微服务处理之前,SDK会从请求中获取流量标识。这可能包括从HTTP头部、Cookie、请求参数等位置获取特定的标识符。
- 向外发送请求: 当微服务需要向外部服务发起请求时,SDK会在请求中添加之前获取到的流量标识。这意味着,SDK会将其添加到新的请求头、请求体或其他适当的位置,以确保这个标识被透传到外部服务。
通过以上步骤,基础 SDK 能够在微服务内部对流量进行拦截、获取流量标识,并在微服务发起外部请求时,将这个标识透传到外部服务中去。
在一些大型企业内部,基础设施团队会提供基础 SDK 供产品团队使用,也有一些相关的开源方案可以参考,例如阿里开源的KtEnv,提供了一个Java语言的SDK示例,采用Spring框架的切面机制来自动化"环境标签"的传递,其中环境标签即为一种流量标识。
使用基础Agent方式
使用 Agent 技术实现流量标识透传是一种相对隐式且高度可配置的方式。Agent 是一种可以介入到 JVM 运行时的程序,它可以对 Java 应用程序进行动态的字节码操作和增强。
在实现流量标识透传时,Agent 可以通过动态字节码增强技术,通过字节码操作工具(如ASM、ByteBuddy等)对特定类或方法进行字节码增强,动态地修改微服务应用的字节码,使得在请求处理链路中自动获取到流量标识,并在请求发起时将这些标识添加到外部请求中。这些标识可能包括从HTTP头部、上下文信息、或者其他标识性的数据。
有一些基础Agent开源方案可以选择参考,如:Homer,这是专门为javaweb应用提供了无感知的header透传的开源方案,华为的Sermant,Sermant 是利用JavaAgent技术为Java应用程序提供服务网格功能的开源方案,提供了流量透传插件tag-transmission,可以帮助微服务实现流量透传功能。
三种方式总结
这三种实现流量标识透传的方式各自具有独特的优势和适用场景。总结如下:
- 业务代码修改: 这种方式简单直接,但会增加业务代码的复杂性和维护成本,尤其在大型微服务体系中,需要在多个服务间添加相似的逻辑,不够灵活和智能,实际应用较少。
- 基础 SDK: 使用基础 SDK 相对于业务代码修改更为智能化和自动化,减少了对业务代码的侵入,同时也能够提供一定程度的可配置性和扩展性,但对业务代码仍然有侵入,并且和语言相关,在大型多语言微服务体系中需要提供多语言SDK,另外 SDK 版本升级困难。
- Agent 方式: 基于 Agent 技术,可以在不修改业务代码的情况下,实现流量标识的拦截和传递。这种方式尤其适用于不想或不能直接修改业务代码的场景,其灵活性和智能化程度较高,但也存在和语言绑定,版本升级相对困难的问题。
总体而言,三种方式各有利弊,在实际场景中,需要根据具体需求和现有架构,选择适合的方式。
流量路由
流量路由和微服务标识类似,由于传统模式和云原生模式都支持,较流量标识简单很多。
在传统微服务框架(如 Spring Cloud)中,实现动态路由通常通过 API 网关(如Spring Cloud Gateway)或负载均衡器(如Netflix Ribbon)等组件,根据特定的策略或规则,对流量进行分发和路由。例如,可以基于请求头中的流量标识信息,利用负载均衡策略,将请求分发到不同版本的微服务实例上,实现动态路由。
而在云原生架构下(例如 Kubernetes + Istio),动态路由更加简单。通过 Istio 中的流量管理功能,定义Gateway、VirtualService、DestinationRule等规则和配置来实现流量的精细化控制和路由。
3全链路灰度发布实践
我们在云原生模式下,对概述部分图1 所示的微服务进行全链路灰度发布实践。微服务版本情况及调用链路和图 1 一致,微服务实例列表如下:
图片
网关采用 Istio Ingress Gateway,流量标识透传采用基础 Agent 方式,可以在微服务调用链路中透传 key 为 et-mark 的 HTTP Request Header ,流量路由通过 Istio 流量管理功能实现,关键Gateway、VirtualService、DestinationRule 规则部分如下:
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: ingress-gateway
namespace: e2e-canary-release
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- 'www.e2e-canary-release.com'
port:
name: http
number: 80
protocol: HTTP
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: service-a
namespace: e2e-canary-release
spec:
gateways:
- e2e-canary-release/ingress-gateway
hosts:
- 'www.e2e-canary-release.com'
http:
- route:
- destination:
host: service-a
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: service-b
namespace: e2e-canary-release
spec:
hosts:
- service-b
http:
- match:
- headers:
et-mark:
exact: v2
route:
- destination:
host: service-b
subset: v2
- route:
- destination:
host: service-b
subset: v1
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: service-b
namespace: e2e-canary-release
spec:
host: service-b
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
实际效果如下,可以看到默认情况下请求流量流经的微服务版本均为正式版本v1,当请求header中包含流量标识时,即流量为灰度流量时,会按照图1 中的灰色路径流转,实现了全链路灰度发布。
图片
图片
4总结
本文首先介绍了全链路灰度发布的概念、作用以及实现全链路灰度发布时需要解决的关键问题,针对每个问题分别从传统模式和云原生模式介绍了对应的解决方案,其中对流量标识透传做了详细的介绍,然后在云原生模式下,对微服务 Demo 进行了全链路灰度发布实践,展示了实践效果。由于能力和时间有限,一些内容仅进行了粗浅介绍,希望后续可以继续深入研究分享,文中存在错误的地方,也望大家指正。
5参考文章及相关链接
- 深入剖析全链路灰度技术内幕
https://developer.aliyun.com/article/834510 - 基于 Istio 的全链路灰度方案探索和实践https://xie.infoq.cn/article/f6a1db8756e8bfa831947ee05
- 聊聊 Spring Cloud 全链路灰度发布方案~ https://z.itpub.net/article/detail/5D9F94265D666C4607B92CBC32667692
- Spring Cloud Alibaba-全链路灰度设计https://www.nowcoder.com/discuss/517248839594541056
- 标记透传:微服务系统如何做标记透传方案选型?https://leeshengis.com/archives/444794
- 流量治理的基石——基于字节码增强的全链路流量标签透传https://juejin.cn/post/7282957826510667816
- KtEnv
https://alibaba.github.io/virtual-environment/ - Homer
https://github.com/kaikeba/homer - Sermant
https://sermant.io/zh/
作者:张海文,中国移动云能力中心高级软件研发工程师,移动云服务网格负责人,QCon、KubeCon等大会分享者,专注于云原生、微服务、算力网络等。