作者:微信小助手
发布时间:2024-01-04T21:02:51
作者:张海文,中国移动云能力中心高级软件研发工程师,移动云服务网格负责人,QCon、KubeCon等大会分享者,专注于云原生、微服务、算力网络等。 在当今快节奏的软件开发环境中,随着用户需求的不断变化和竞争日益激烈,软件更新和发布的频率已成为常态。然而,与此同时,保证用户体验的稳定性和可靠性也是至关重要的。传统的大规模软件发布往往会面临着线上故障风险,可能导致用户体验下降,甚至影响业务正常运行。 为了解决这一矛盾,在软件开发领域催生出了灰度发布的概念。灰度发布是一种渐进式的软件发布方式,它允许将新功能或更新逐步推送给一部分用户,而不是一次性全部发布。这样的方式能够有效降低线上故障的风险,保障用户体验,同时也为开发团队提供了更多时间和机会在全面发布前进行验证和修复。 然而,随着软件架构的演进,尤其是微服务架构的普及,软件系统往往由多个微服务组成,不同服务的版本升级需要协调和同步。在这种背景下,单一服务的灰度发布已经不能完全适应需求,全链路灰度发布应运而生。 全链路灰度发布考虑到整个软件系统的多个微服务,允许多个微服务同时进行版本控制和升级,以确保整个系统的平稳过渡和稳定性,是一种更为全面和细致的灰度发布方式。通过全链路灰度发布,开发团队能够更加精确地控制不同服务版本的发布比例,降低系统风险,保障线上稳定性,最大程度地满足用户需求。 以图 1 为例,软件系统包含网关和 4 个微服务,通过全链路灰度发布,可以使ServiceB 和 ServiceD 进行灰度发布,通过灰线所示的流量进行灰度功能验证,同时不影响蓝线所示的正常访问流量。 我们通过图1 可以清晰地看到,在实施全链路灰度发布时,需要部署相关服务的灰度版本,并确保在整个请求的调用链上,网关和微服务组件能够准确识别正式流量和特定版本灰度流量,并根据流量类型动态地将请求路由到正确版本的上游微服务上。所以,我们需要解决以下几个问题: 我们通过给微服务实例添加标识的方式,使微服务实例具有版本信息,针对不同流量对应版本实例提供服务。 在传统模式下,一般需要产品根据使用的微服务框架决定添加标识的方式。我们以基于 Spring Cloud 框架 + Nacos注册中心 的微服务为例,一般通过在微服务元数据配置(spring.cloud.nacos.discovery.metadata)中添加标识,配置示例如下: 微服务在 Nacos 注册中心中服务信息如下: 在云原生模式下,为微服务实例添加标识更加方便,不需要修改微服务代码配置,一般在微服务 Deployment 的 Pod 模版中添加Labal标识即可,实例如下: 和微服务标识相同,我们也是通过为流量添加不同标识的方式区分流量,流量标识在业界还有一个更专业更形象的叫法叫流量染色。 流量染色分两步:第一步在流量源头染色,第二步在调用链内染色。 第一步比较简单,通过前端或者网关根据流量特点(比如浏览器类型,用户标识,地域标识等)为流量添加标识(比如在HTTP Header中添加version标签)。 第二步是全链路灰度中最核心也是最复杂的部分,需要流量标识在调用链中透传下去,保证调用链上的每个微服务组件都能根据标识识别流量并动态路由。 传统的微服务框架注重基础功能的实现,例如服务注册与发现、负载均衡等,而未将流量标识透传作为核心特性之一,导致在实际应用中难以实现流量标识的无缝传递。 云原生时代的 Kubernetes + Istio 同样无法帮助微服务实现流量标识透传。不少人存在误解,认为 Istio 能对微服务实例的出入流量进行拦截,应该原生支持流量标识透传,但实际上 Istio 本身没有流量标识透传的能力。 Istio Sidecar 对微服务入口和出口流量拦截如上图所示,Sidecar 虽然能将入口流量1 拦截后转给微服务容器(入口流量2),也能将微服务容器出口流量 3 拦截并转发到 Pod 外(出口流量4),但 Sidecar 不知道出口流量 3 和入口流量 2 的对应关系,在实际情况中,Sidecar 会拦截很多的出口流量,也会拦截很多的入口流量,但 Sidecar 并不知道某一个出口流量对应哪个入口流量。只有微服务应用知道对应关系,因为微服务应用亲自做了流量处理(微服务应用收到入口流量请求2后,进行业务逻辑处理,然后再发出出口流量3,请求下一级微服务),所以 Istio 虽然能对微服务出入流量拦截,但不知道出入流量的对应关系,无法将入口流量的标识自动添加到出口流量上,无法做流量标识透传。 那么如何进行流量标识透传呢,通常有以下3种方式: 微服务侧进行业务代码改造,从入口流量请求中获取流量标识,并在出口流量中添加流量标识,代码示例如下: 将从入口流量请求中获取流量标识,并在出口流量中添加流量标识这种共性逻辑封装到基础 SDK 中,其原理通常涉及 SDK 对请求和响应的拦截处理。这种方法的核心在于SDK能够拦截到微服务内部的请求,从请求中获取流量标识,并在微服务发起外部请求时,将这个标识加入请求中,实现流量标识的透传。基础 SDK 的工作机制一般包括以下几个关键步骤: 使用 Agent 技术实现流量标识透传是一种相对隐式且高度可配置的方式。Agent 是一种可以介入到 JVM 运行时的程序,它可以对 Java 应用程序进行动态的字节码操作和增强。 在实现流量标识透传时,Agent 可以通过动态字节码增强技术,通过字节码操作工具(如ASM、ByteBuddy等)对特定类或方法进行字节码增强,动态地修改微服务应用的字节码,使得在请求处理链路中自动获取到流量标识,并在请求发起时将这些标识添加到外部请求中。这些标识可能包括从HTTP头部、上下文信息、或者其他标识性的数据。 有一些基础Agent开源方案可以选择参考,如:Homer,这是专门为javaweb应用提供了无感知的header透传的开源方案,华为的Sermant,Sermant 是利用JavaAgent技术为Java应用程序提供服务网格功能的开源方案,提供了流量透传插件tag-transmission,可以帮助微服务实现流量透传功能。 这三种实现流量标识透传的方式各自具有独特的优势和适用场景。总结如下: 流量路由和微服务标识类似,由于传统模式和云原生模式都支持,较流量标识简单很多。 在传统微服务框架(如 Spring Cloud)中,实现动态路由通常通过 API 网关(如Spring Cloud Gateway)或负载均衡器(如Netflix Ribbon)等组件,根据特定的策略或规则,对流量进行分发和路由。例如,可以基于请求头中的流量标识信息,利用负载均衡策略,将请求分发到不同版本的微服务实例上,实现动态路由。 而在云原生架构下(例如 Kubernetes + Istio),动态路由更加简单。通过 Istio 中的流量管理功能,定义Gateway、VirtualService、DestinationRule等规则和配置来实现流量的精细化控制和路由。 我们在云原生模式下,对概述部分图1 所示的微服务进行全链路灰度发布实践。微服务版本情况及调用链路和图 1 一致,微服务实例列表如下: 网关采用 Istio Ingress Gateway,流量标识透传采用基础 Agent 方式,可以在微服务调用链路中透传 key 为 et-mark 的 HTTP Request Header ,流量路由通过 Istio 流量管理功能实现,关键Gateway、VirtualService、DestinationRule 规则部分如下: 实际效果如下,可以看到默认情况下请求流量流经的微服务版本均为正式版本v1,当请求header中包含流量标识时,即流量为灰度流量时,会按照图1 中的灰色路径流转,实现了全链路灰度发布。 本文首先介绍了全链路灰度发布的概念、作用以及实现全链路灰度发布时需要解决的关键问题,针对每个问题分别从传统模式和云原生模式介绍了对应的解决方案,其中对流量标识透传做了详细的介绍,然后在云原生模式下,对微服务 Demo 进行了全链路灰度发布实践,展示了实践效果。由于能力和时间有限,一些内容仅进行了粗浅介绍,希望后续可以继续深入研究分享,文中存在错误的地方,也望大家指正。 深入剖析全链路灰度技术内幕 https://developer.aliyun.com/article/834510#slide-1 基于 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/#/zh-cn/doc/use-sdk?id=%E4%BD%BF%E7%94%A8sdk Homer https://github.com/kaikeba/homer Sermant https://sermant.io/zh/1概述
2全链路灰度发布核心问题
微服务标识
传统模式
spring:
cloud:
nacos:
discovery:
metadata:
version: ${APP-VERSION:v1}云原生模式
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
spec:
selector:
matchLabels:
app: demo
version: v1
template:
metadata:
labels:
app: demo
version: v1流量标识
流量标识透传方式
微服务修改源码方式
// 从请求中获取流量标识 version
String versionValue = request.getHeader("version");
// 构造新请求需要的 Header,获取到的流量标识添加到新请求的 Header 中
HttpHeaders headers = new HttpHeaders();
headers.set("version", versionValue);
// 发起出口流量请求使用基础SDK方式
使用基础Agent方式
三种方式总结
流量路由
3全链路灰度发布实践
---
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: v24总结
5参考文章及相关链接