作为业内首个全托管Istio兼容的阿里云服务网格产品ASM,一开始从架构上就保持了与社区、业界趋势的一致性,控制平面的组件托管在阿里云侧,与数据面侧的用户集群独立。ASM产品是基于社区Istio定制实现的,在托管的控制面侧提供了用于支撑精细化的流量管理和安全管理的组件能力。通过托管模式,解耦了Istio组件与所管理的K8s集群的生命周期管理,使得架构更加灵活,提升了系统的可伸缩性。从2022年4月1日起,阿里云服务网格ASM正式推出商业化版本, 提供了更丰富的能力、更大的规模支持及更完善的技术保障,更好地满足客户的不同需求场景,详情可进入阿里云官方网站 - 搜索服务网格ASM。
作为完全兼容社区Istio的服务网格产品,服务网格ASM针对全链路灰度场景提出了自己的思考,并针对性地设计了对应的流量标签方案,来解决上述全链路灰度实现方案中的痛点问题。
1. 灰度发布与全链路灰度
1.1. 回顾:以Istio实现灰度发布
灰度发布是一种常见的对新版本应用服务的发布手段,其特点在于能够将流量在服务的稳定版本和灰度版本之间时刻切换,以帮助我们用更加可靠的方式实现服务的升级。在流量比例切换的过程中,我们可以逐步验证新版本服务的功能特性、可靠性等特性,一旦新版本服务不满足需求,还可以时刻将流量切回老版本,因此灰度发布也是一种在软件工程领域中得到广泛应用的发布方案。
Istio的无侵入式灰度发布已经是一个非常成熟的特性:我们可以同时部署服务的多个版本,使用DestinationRule来制定工作负载的版本定义,并使用VirtualService的流量比例分割的能力将不同比例的服务流量发往不同版本的工作负载,直至所有流量都直接发往新版本服务。
1.2. 单体服务应用和微服务应用
回到应用本身,开发者在选择实现一个应用的基础架构时,往往会考虑单体服务架构或是微服务架构两种模式。
单体服务会将应用的业务逻辑全部实现在一个服务之中,应用的不同功能特性往往体现为同一个项目中的不同功能模块。而微服务架构则是以开发一组小型服务的方式来开发一个独立的应用系统,每个服务都以一个独立进程的方式运行,服务之间则采用HTTP/gRPC等轻量级通信机制来相互交互,不同的微服务之间通常以业务功能为基准进行划分,对应单体服务中的功能模块。bookinfo自身就是一个喜闻乐见的微服务架构应用。
单体服务 | 对比项 | 微服务架构 |
---|---|---|
较慢:内部功能模块相互耦合,难以适应敏捷流程 | 交付速度 | 较快:服务经过拆分,各个部分可以并行开发、测试、部署,交付效率提升 |
更低:故障往往影响整个系统 | 可用性 | 更高:由于服务拆分,能够隔离故障范围,降低单一服务对整体系统的影响 |
受限:由于服务各个模块相互耦合,重构和更换技术栈都带来较大代价 | 扩展性与灵活性 | 灵活:微服务粒度更小,有利于架构的持续演进,同时可以灵活更换部分微服务的技术栈 |
简单:单体服务模块间基本都为本地调用,模块都处于统一服务中,架构更简单 | 架构复杂度 | 复杂:微服务间通信基于远程调用,需要考虑服务注册发现、服务治理、负载均衡等问题,架构更复杂,对开发人员挑战更大 |
更低:仅需消耗单个服务资源,内部模块通信皆可在本地完成 | 时延与资源成本 | 更高:需要为大量微服务付出更多资源成本,远程调用的时延也更高 |
微服务架构在提升项目交付速度、缩小故障影响范围、灵活性等方面居功至伟,但这些优势也是用更加复杂的架构设计换来了,复杂架构则带来了很多前所未有的问题需要解决,一些早已成熟的最佳实践,甚至也需要被重新发明。
比如,微服务架构下的灰度发布形态。
1.3. 全链路灰度
如果应用使用的是单体服务架构,我们可以使用章节1中描述的方法轻松实现应用的灰度发布。这一切都基于一个自然成立的前提:即应用各个功能模块的不同版本、都是跟随单体服务本身而产生天然隔离的。当我们将流量发送到一个单体服务的canary版本,也就意味着处理流量的所有功能模块也都是canary版本。
而如果我们将应用切换到使用微服务架构,灰度发布这一问题就值得重新讨论。产生这个问题的原因,其本质就在于:上述的自然前提不存在了,之前耦合在同一个服务中的各功能模块、以独立服务的形态部署在Kubernetes集群之中。我们可以分类讨论微服务架构下的灰度发布场景:
1.3.1. 仅灰度发布应用的一个功能模块
微服务架构带来的最大变化,就是各个功能模块可以各自分开进行开发、测试、部署。很多时候,我们对应用的一次更新可能仅局限在一个功能模块之内。在这种情况下,如果使用单体服务架构,就需要针对整个服务进行灰度发布;而在微服务架构下则可以仅针对一个具体的微服务进行版本更新,其它微服务则保持原样,服务的发布效率将有很大提升。
在这种情况下,两种架构下的灰度发布模型类似,都是针对某一个具体服务进行灰度发布。因此,即使处于微服务架构下,也只需要采用常规的灰度发布方案即可。Istio的bookinfo traffic managedment demo中针对reviews服务的发布就属此类。
1.3.2. 同时灰度发布应用的多个功能模块
相比灰度发布一个功能模块,这种情况甚至可能会更加常见。由于应用的各个功能模块必然会因为业务需要进行某种程度的耦合,对应用的一次业务逻辑变更也往往涉及到同时变更多个功能模块。功能模块之间的相互调用,自然也需要遵循版本发布机制:某个版本的功能模块、只能调用同一版本的另一功能模块。
在单体服务架构下,此时的灰度发布模型与3.1中的单个功能模块发布完全一致,因为无论一个还是多个功能模块的变更,都只涉及单体服务内部的逻辑变更,只需要对单体服务进行灰度发布即可。
而在微服务架构下,则必须提出一种全新的灰度发布模型,来解决这样一个新问题:
核心问题:
由于单体服务这一天然的隔离屏障消失(每个微服务都独自部署在Kubernetes集群中,能够互相访问),必须在灰度发布流程中提供新的隔离机制,来隔离微服务(功能模块)的不同版本。
这种全新的灰度发布模型也就是所谓的全链路灰度。在全链路灰度发布中,我们不仅是确保在一个服务的不同版本之间进行流量切换,而是确保在一个微服务调用链路的不同版本之间进行流量切换。
这意味着在全链路灰度中,有一种机制确保一组微服务之间的调用链路是完全分离的,base版本的A服务在灰度过程中不会调用Canary版本的B服务,反之亦然。这样才能保证微服务架构下与单体服务灰度发布模型的完全对应。
2. 使用Istio实现全链路灰度:实践方法
在全链路灰度之后,我们来讨论一下使用Istio能够以怎样的实践方法来实现全链路灰度发布,是否能够同样以无侵入的方式完成全链路灰度发布流程?实际上全链路灰度也只是灰度发布的一种形式,我们大多数的实践基于单个服务的灰度发布实践,并加入保证调用链路隔离的机制即可。
我们以1.3.2中的图例为例,假设集群中存在mocka和mockb两个微服务,mocka -> mockb为微服务之间的调用链路,同时mocka和mockb各自存在base和canary两个版本。看看在这个环境下,如何实现mocka->mockb调用链路的全链路灰度。
2.1. 工作负载打标
只要涉及到灰度发布场景,工作负载打标基本上是必选项:无论何种灰度发布场景,Istio都必须具有区分不同版本工作负载的手段。我们仍然可以使用DestinationRule,用subset为不同版本的工作负载进行打标和分类。
示例:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mocka
spec:
host: mocka
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
subsets:
- labels:
version: base
name: base
- labels:
version: canary
name: canary
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mockb
spec:
host: mockb
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
subsets:
- labels:
version: base
name: base
- labels:
version: canary
name: canary
在全链路灰度场景中,主要差别是:我们必须为调用链路上的每个服务都声明各自的不同版本(mocka和mockb服务的base和canary版本)。
2.2. 从Istio网关向调用链路引流
在普通的灰度发布场景中,往往涉及到流量按比例的分隔,通过将不同比例的流量发往服务的不同版本,慢慢实现所有流量向灰度版本的切换。在微服务架构中,流量比例的分割往往发生在向调用链路中第一个微服务的引流过程中。向调用链路中的第一个服务的不同版本发送不同比例的流量,并在后续的调用链路中保证流量在同一版本服务之间传播,这就与单体服务的灰度发布中流量比例分割的过程对应了起来。
我们通常通过Istio网关向调用链路中的首个微服务转发外部请求,因此这里示例的也是通过网关向mocka服务的引流过程。
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: simple-gateway
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- "*"
port:
name: http
number: 80
protocol: HTTP
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-gateway-mocka
spec:
gateways:
- simple-gateway
hosts:
- "*"
http:
- route:
- destination:
host: mocka
subset: base
weight: 90
- route:
- destination:
host: mocka
subset: canary
weight: 10
2.3. 实现全链路的灰度路由
可以看到2.1和2.2中的实践方法与正常的灰度路由方案并无二致。我们主要的努力还是要解决1.3.2中提出的全链路灰度的核心问题:即保证流量传播在不同版本调用链路之间的相互隔离。如果只使用目前的方法,我们仅能在调用链路的第一跳实现灰度发布,而mocka调用mockb服务时,就会发生流量的逸散:无论哪个版本的mocka工作负载,其调用所发出的流量都将均等地发往所有版本的mockb服务。
重新思考,要实现调用链路之间的相互隔离,实际就是要实现如下效果:base版本的mocka服务,其发出的流量也只能发向base版本的mockb服务,反之亦然。
那么,使用成熟的社区方案,我们就可以想到去使用社区的标签路由能力,来维持流量在特定版本调用链路中的传递。
2.3.1. 基于工作负载标签的标签路由
由于灰度发布的要求,我们一定会为灰度发布中服务的工作负载打上特定的标签(如version: base和version: canary)。这些标签就可以成为请求发送时路由的依据,还是继续mocka->mockb的例子:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- match:
- sourceLabels:
app: mocka
version: base
route:
- destination:
host: mockb
subset: version-base
- match:
- sourceLabels:
app: mocka
version: canary
route:
- destination:
host: mockb
subset: version-canary
上述例子利用了工作负载的标签来对请求进行匹配。针对调用链路上的每一条调用关系,我们都需要维护一条用于隔离不同版本之间流量的路由规则,该规则匹配请求源工作负载的标签,并根据标签将请求路由至目标服务的特定版本。
2.3.2. 基于请求标签的标签路由
在2.3.1的例子中,我们的基本思路是通过标签识别不同来源的请求,并在调用链路中根据请求来源的版本将请求继续路由至对应版本的下一条服务。
基于工作负载标签的标签路由方案有比较明显的缺点:那就是这个方案需要使用者维护大量的VirtualService路由项。具体来说,微服务调用链路中每增加一条边,都需要为每一个版本增加一条对应路由项,来维持流量在每个版本调用链路的内部传递。
$$需要维护的路由项 = 版本数量 \times 调用链路中的边数$$
而调用链路的边数则与微服务数量有关,最差情况下,调用边数将达到微服务数量的平方级别,对于Istio使用者来说,如此数量的路由项是一个巨大的维护负担。
造成此般局面的原因之一是:我们无法从请求本身获取此请求所属调用链路的版本信息,因此只能在VirtualService中机械式地匹配该版本调用链路上所有源工作负载的标签,造成需要维护路由项的量级上涨。
如果我们放宽假设,假设不同版本的服务工作负载发出的请求中本身就携带版本信息,就可以进一步压缩需要维护的路由条目。
假设不同版本工作负载发出的请求都将打上所属调用链路版本的标签(例如,请求都拥有一个特定的headerversion来标识所属版本),就可以直接基于请求的标签进行路由:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- match:
- headers:
version: base
route:
- destination:
host: mockb
subset: base
- match:
- headers:
version: canary
route:
- destination:
host: mockb
subset: canary
在这种情况下:
$$需要维护的路由项 = 版本数量 \times 调用链路中被调用的服务数$$
可以看到相比基于工作负载标签的标签路由,基于请求标签的标签路由所需维护路由项数目将下降一个指数级。其本质是将不同来源的工作负载标签匹配都整合进了一条请求的标签匹配。
然而,基于请求的标签路由基于请求携带标签这一假设,实际环境中,我们无法保证业务应用逻辑满足这一假设。换句话说,这一方案是具有侵入性的,我们必须要求业务应用感知到我们这一灰度方案,并进行逻辑改造,有违我们以无侵入式方式实现全链路灰度的初衷。
3. 服务网格ASM使用TrafficLabel实现泳道模式的全链路灰度
作为业内首个全托管Istio兼容的阿里云服务网格产品ASM,一开始从架构上就保持了与社区、业界趋势的一致性,控制平面的组件托管在阿里云侧,与数据面侧的用户集群独立。ASM产品是基于社区Istio定制实现的,在托管的控制面侧提供了用于支撑精细化的流量管理和安全管理的组件能力。通过托管模式,解耦了Istio组件与所管理的K8s集群的生命周期管理,使得架构更加灵活,提升了系统的可伸缩性。从2022年4月1日起,阿里云服务网格ASM正式推出商业化版本, 提供了更丰富的能力、更大的规模支持及更完善的技术保障,更好地满足客户的不同需求场景,详情可进入阿里云官方网站 - 搜索服务网格ASM。
作为完全兼容社区Istio的服务网格产品,服务网格ASM针对全链路灰度场景提出了自己的思考,并针对性地设计了对应的流量标签方案,来解决上述全链路灰度实现方案中的痛点问题。
3.1. 无侵入式流量标签:基于工作负载标签自动为出口流量打标
全链路灰度是服务网格ASM在阿里云内部经过反复锤炼的灰度发布场景,其本质还是基于我们在第2节中讨论的全链路灰度实践方案。我们采用2.3.2小节中讨论的基于流量标签的标签路由来实现不同调用链路之间的完全隔离。
有侵入式的方案是流量标签路由方式的最大障碍,有关这个问题,我们可以进一步利用为每个工作负载注入的Sidecar代理以及其中envoy的强大拓展能力,实现无侵入式的流量打标。
Envoy路由出口流量时是一个为请求打标的好时机,我们可以在这个时间点为原本无任何特征的请求进行一次打标(即:带上一个特定的header),以表明请求所属调用链路之版本。具体来说,我们在envoy的http filter chain中插入一个自定义的traffic label filter,用来为请求打标,经过traffic label filter的请求将根据事先设定的方式获取请求的标签内容,并添加到请求的header之中。
对应地,我们在控制面提供TrafficLabel自定义资源,用以定义以何种方式为请求进行打标。
还是以mocka->mockb为例,我们可以定义如下的TrafficLabel:
apiVersion: istio.alibabacloud.com/v1
kind: TrafficLabel
metadata:
name: default
spec:
rules:
- labels:
- name: version
valueFrom:
- $getLabel(version)
TrafficLabel中可以定义一系列为流量打上的标签,比如这里就设定要为所有的出口流量增加名为version的标签。标签名下方的valueFrom是一个表达式数组,用来指定不同的获取流量标签值的方法。这里的$getLabel(version)方法则是指定了从源工作负载的version label中获取标签值,并填写在流量的version标签之中。
通过这种方式,我们以无侵入的方法为调用链路中的请求添加了version标签,接下来只需进行正常的标签路由配置即可。
3.2. 基于TrafficLabel的标签路由与全链路灰度
到这里,TrafficLabel已经替我们完成了大量工作,我们只需要同样使用2.3.2中使用的VirtualService,就可以基于打上的流量标签进行标签路由了。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- match:
- headers:
version: base
route:
- destination:
host: mockb
subset: base
- match:
- headers:
version: canary
route:
- destination:
host: mockb
subset: canary
在这个场景下,TrafficLabel为我们带来的主要价值是:为我们自动化、无侵入式地完成了工作负载标签 到 请求标签的转换。基于TrafficLabel为添加的统一请求标签,使用者就能够以更小的负担、更低的代价完成全链路灰度发布流程。
One more thing。由于TrafficLabel能够为我们所有的请求都添加统一的标签,我们就可以以此为前提、进一步简化需要管理的路由项内容。具体来说,服务网格ASM支持在TrafficLabel流量打标生效的情况下,简化基于流量标签的标签路由的声明:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- route:
- destination:
host: mockb
subset: $version
在这个VirtualService中,形似version的subset声明,实际指示了从TrafficLabel添加的流量version标签中动态获取目标服务subset,以决定请求链路中下一条的版本。
在这个形态下:
$$需要维护的路由项 = 调用链路中被调用的服务数$$
我们只需要针对调用链中每个需要被调用的服务声明一个如上的VirtualService即可,全链路灰度发布的配置门槛基本和普通灰度发布场景相当。
3.3. 观测与小结
产生实际请求后,我们可以使用网格拓扑来观测全链路灰度的发布结果,一如正常的服务灰度发布流程。
在这个全链路灰度发布实践中,我们使用工作负载标签来标识微服务的不同版本,使用TrafficLabel来自动为请求打标、实现版本信息在调用链路上的传递,并使用基于请求标签的标签路由VirtualService来保持不同版本的调用链路之间的完全隔离。
所有前述内容,都属于一类最为典型的全链路灰度实践场景,通过工作负载标签将不同版本的流量隔离在完全隔离的多个环境中,就像多条互不干涉的泳道一般,称之为泳道模式的全链路灰度发布。
4. 泳道之外:TrafficLabel还可以做到什么
泳道模式可以解决最为常见的全链路灰度发布场景,但仍然不能说是所有全链路灰度发布场景的通解。本节讨论更为广泛的全链路灰度发布场景定义,以及TrafficLabel如何服务这些场景。
4.1. 条条大路通罗马:TrafficLabel的其它流量打标方法
4.1.1. getExternalInboundRequestHeader
我们在2-3大节详细讨论了泳道模式下的全链路灰度场景以及其在Istio / 服务网格ASM 中的通用解法。然而,泳道模式看似通解,却仍然依赖一个默认的前提:
调用链上的任何服务均存在每个泳道对应的版本
当调用链上仅存在两个服务时,这一前提是自然存在的,但对于更长的调用链则未必成立。我们在调用链上增加一个服务,以mocka->mockb->mockc这样一条调用链为例,仍然设当前微服务存在canary和base两个版本,但mockb服务却只有base版本,仅mocka和mockc服务发布了canary版本。
此时,我们期望的调用链路其实是类似这样的:我们期望流量在发往仅有单版本的mockb服务后,从mockb发出的出口流量仍然能保持调用链路中下游服务的版本信息。对于泳道模式的全链路灰度来说,这是不可实现的。因为泳道中的服务必须拥有不同版本的工作负载和对应的工作负载标签,以区分发送出口流量的工作负载到底属于哪个泳道。
实际上,在这种情况下,任何基于工作负载标签的全链路灰度方案都是不可行的。其本质在于流量在流经仅具有单版本的服务时,调用链路版本这一上下文将被这个服务丢弃,导致无法继续灰度后续链路。因此,基本只能考虑使用基于流量标签的标签路由方案,用请求原本的特征实现不同调用链路之间的隔离。
看上去这种场景只能让业务应用感知到全链路灰度方案,并在服务发送出口流量时透传流量标签信息。但通过结合trace能力,TrafficLabel同样可以以无侵入式的方式实现流量标签在链路上的传递。
TrafficLabel提供getExternalInboundRequestHeader流量打标方法。可以基于trace的上下文,识别一条调用链路中一个服务入口流量的流量标签值,并将这个标签值传递到同一条链路服务的出口流量上:
apiVersion: istio.alibabacloud.com/v1
kind: TrafficLabel
metadata:
name: default
spec:
rules:
- labels:
- name: version
valueFrom:
- $getExternalInboundRequestHeader(version, x-request-id)
这里使用了getExternalInboundRequestHeader(headerName, contextId) 方法实现流量打标,这个方法从流量的trace上下文中获取流量标签信息。其中:
- headerName:请求头的关键字Key。该参数为必填项,且值不能为空,例如x-asm-prefer-tag。
- contextId:一个贯穿整个调用链路的请求头字段,其值可配置为一个指定的入口请求头或Tracing系统中的Trace ID。该参数为必填项,且值不能为空。
默认情况下,Sidecar代理从入口流量的请求头(名称为headerName)中获取对应的值,并以此作为流量打标的标签值tagValue。
为了将标签值tagValue附加到同一条调用链路的出口流量请求中,在Sidecar代理的内部逻辑中维持一个map。contextId是一个贯穿整个调用链路的请求头字段,其值可配置为一个指定的入口请求头或使用x-request-id作为contextId。
当业务容器发起出口请求时,Sidecar代理会通过contextId查找上下文Map。若找到关联tagValue,则Sidecar代理会为出口请求中附加两个新请求头:
- 请求头名称为headerName,值为tagValue。
- 请求头名称为流量标签TrafficLabel 中定义的标签名(例如version),值为tagValue。
通过这样与trace系统结合的方式,TrafficLabel就能在任意形态的调用链路中一直维持流量的标签信息,而无需依赖调用链路上工作负载本身的标签信息。接下来只要同样使用基于流量标签的标签路由,就能支持几乎任意的全链路灰度场景。
4.1.2. getLocalOutboundRequestHeader
上一节我们讨论了当应用已经介入链路追踪,并且业务逻辑能够透传trace ID的情况下,TrafficLabel如何帮我们实现自动的流量打标。不过,如果我们的应用已经具有透传某个header的能力,且这个被透传的header不作为trace ID使用,场景就能更加简化。我们甚至不需要TrafficLabel去给流量打标,直接使用这个被透传的header即可实现全链路灰度的场景。
我们只需要配置一些基础的DestinationRule和VirtualService。还是以mocka -> mockb -> mockc为例,这三个微服务实际上已经具有在链路上透传叫my-request-id的header的能力,我们就利用这一点,将这个现成的header利用起来、作为流量标签使用。
首先给mocka、mockb、mockc各制定一个DestinationRule,区分他们的三个版本。
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: mocka
namespace: istio-system
spec:
host: mocka.default.svc.cluster.local
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
- labels:
version: v3
name: v3
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: mockb
namespace: istio-system
spec:
host: mockb.default.svc.cluster.local
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
- labels:
version: v3
name: v3
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: mockc
namespace: istio-system
spec:
host: mockc.default.svc.cluster.local
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
- labels:
version: v3
name: v3
然后我们需要为每个服务指定它的ns,以确保流量根据作为标签的my-request-id被转发到服务的正确版本。这里我们模拟了一个复杂的场景,将所有发往mockb的请求都发向v1版本,再根据透传的my-request-id来决定发往mockc的请求的目的地。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: e2ecanary
namespace: istio-system
spec:
gateways:
- ingressgateway
hosts:
- '*'
http:
- match:
- headers:
my-request-id:
exact: v1
name: mockv1
route:
- destination:
host: mocka.default.svc.cluster.local
subset: v1
- match:
- headers:
my-request-id:
exact: v2
name: mockv2
route:
- destination:
host: mocka.default.svc.cluster.local
subset: v2
- match:
- headers:
my-request-id:
exact: v3
name: mockv3
route:
- destination:
host: mocka.default.svc.cluster.local
subset: v3
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: e2ecanary-mockb
namespace: istio-system
spec:
hosts:
- mockb.default.svc.cluster.local
http:
- route:
- destination:
host: mockb.default.svc.cluster.local
subset: v1
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: e2ecanary-mockc
namespace: istio-system
spec:
hosts:
- mockc.default.svc.cluster.local
http:
- match:
- headers:
my-request-id:
exact: v1
route:
- destination:
host: mockc.default.svc.cluster.local
subset: v1
- match:
- headers:
my-request-id:
exact: v2
route:
- destination:
host: mockc.default.svc.cluster.local
subset: v2
- match:
- headers:
my-request-id:
exact: v3
route:
- destination:
host: mockc.default.svc.cluster.local
subset: v3
最终我们形成了如下图所示的结果。可以看到所有流量都途径mockb的v1版本,但是这些流量可以根据透传的header来对上游的mockc进行全链路灰度。
上述的例子中,核心要义在于应用本身对某个不是trace ID的header进行了透传,从而形成了一个可以现成利用的流量标签。进一步地说,就是应用本身发出的请求中就携带着目标上游服务的版本灰度信息了。如果有这种现成信息的话,将其利用起来当然是极好的。
针对这种场景,TrafficLabel支持getLocalOutboundRequestHeader方法。这个方法的签名为$getLocalOutboundRequestHeader(headerName):
apiVersion: istio.alibabacloud.com/v1
kind: TrafficLabel
metadata:
name: default
spec:
rules:
- labels:
- name: version
valueFrom:
- $getLocalOutboundRequestHeader(other-header)
该方法表示从由应用服务发出至Sidecar代理的请求中获取名称为headerName的标头值。参数headerName表示请求头的关键字Key。
基于TrafficLabel的流量标签路由能够大大简化全链路灰度路由项配置的基础在于:TrafficLabel为调用链路上的每一次出口请求都附上了统一的流量标签(即:这些出口请求都有着统一的header,里面保存着流量的标签值)。有时,微服务应用的出口请求本身可能已经携带了标签信息,但不同的微服务可能将标签信息携带在不同的header里,造成路由项配置维护上的困难。使用TrafficLabel就能够解决这一问题,它能够将不同header中存储的流量标签都统一到一个header中来,方便管理全链路灰度路由项。
4.2. 有备无患:流量降级
全链路灰度的路由项管理是一个非常繁冗的任务,这其中涉及到对涉及调用链路上所有微服务的所有版本的路由配置。当链路长度和版本数量不断膨胀时,因为某个特定服务不可用而造成整个调用链路失败的情况也很有可能发生。
我们可以考虑为服务路由增加流量降级选项来最大程度维持调用链路的可用性。服务网格ASM对VirtualService提供了扩展定义,可以支持路由目标的fallback流量降级功能。可以在fallback中指定一个服务的备用版本(如base版本),并在标签路由的目标服务不可用(包括目标服务未定义或对应的Pod不存在等情况)时回退到备用服务。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- route:
- destination:
host: mockb
subset: $version
fallback:
target:
host: mockb
subset: base
实际上, fallback是一个更广泛的话题,它能够为服务时刻准备一个备用版本,在服务不可用时提供流量降级,以最大程度提高整个系统的可用性。这使得fallback不仅可以应用在全链路灰度发布的场景之中,在任何路由时可能因为服务不可用而导致调用链路失败的场景下它都可以发光发热。
在流量降级的情况下,我们仍然可以考虑使用getExternalInboundRequestHeader方法,仍然可以保持流量标签在调用链路中的传递。
5. 总结与展望
5.1. 服务网格ASM如何提供全链路灰度最佳实践
基于泳道模式的全链路灰度发布已经是在生产实践中反复验证的成熟场景。服务网格ASM在产品中提供了泳道相关的自定义资源,帮助用户快速配置微服务应用泳道以及泳道引流规则,进一步减少全链路灰度发布场景的配置门槛。
其本质就是对上述全链路灰度场景涉及到的TrafficLabel、DestinationRule、VirtualService等资源的整合与简化。用户仅需要指定每条泳道中服务的版本信息、以及调用链路中第一个服务的引流规则即可。
对应地,服务网格ASM也提供完善的界面配置,进一步减少用户使用全链路灰度能力的心智负担,请参考:https://help.aliyun.com/document_detail/459472.html
5.2 总结
试玩服务网格ASM,拥抱美好生活!😀
👉👉👉https://www.aliyun.com/product/servicemesh👈👈👈