【阅读原文】戳:阿里云服务网格ASM多集群实践(二):高效按需的应用多环境部署与全链路灰度发布
引言
在变化迅速、应用架构日趋复杂的云原生微服务世界中,管理所有服务的敏捷发布迭代流程、同时为应用构建开发与测试环境成为一项巨大的挑战。
从构建开发与测试环境的角度来说,被广泛采用的部署方式主要是以下两种:
• 全量单一测试环境:最简单的部署模式,所有开发者共享一套测试环境,对于给定的应用,当前环境里只能是该应用的某一个测试版本,应用开发者将环境部署为自己的版本进行测试时,其他开发者只能等待。同时,一旦某一应用的开发者部署了无法正常工作的版本,则调用链上所有其他应用开发者的测试都受到影响。
• 全量多套测试环境:每一个开发者单独部署一套测试环境,对于规模稍大的应用来说,这种部署方式的成本可能是不可接受的(试想为一个上千服务规模的应用做一套独立部署是多么大的资源浪费)。
应用发布也存在同样的问题:当为应用发布新的版本迭代时,最简单直接、容易管理的办法当然是将所有服务共同打包发布新的版本,并在两个版本之间进行类似流量比例灰度这样的发布流程。然而,当服务规模达到一定量级后,这种发布方式带来的资源消耗将会不容忽视(尤其是当一次迭代只更新了应用中的几个个别服务时),如果这些服务中还包括依赖GPU资源的AI服务,每次发布带来的消耗就更加难以计量。
所有以上的困难都指向一个共同的诉求:为云原生微服务应用构建高效、按需部署、节省资源、高可靠性的隔离环境,在应用的整个开发测试、部署发布流程中“降本增效”。
阿里云服务网格ASM在产品能力基础上,通过介绍一种多集群部署下的流量泳道场景化解决方案来统一解决这些问题,它的主要特征包括:
• 权限、部署隔离:通过双集群部署分离开发测试与生产部署环境,双集群带来了分离的权限控制、部署机器,避免了开发测试服务对生产的干扰,提升应用整体可靠性。
• 按需高效部署:基于服务网格ASM的宽松模式流量泳道能力,在开发测试/灰度版本的环境中,只需要部署少量需要更新的服务新版本,当请求目标服务在当前环境中不存在时,请求目标将根据被称为“基线版本”的设定回退到目标服务的稳定版本、进而完成整个调用链条。通过这种方式,开发和运维人员将能够以更快的速度、更低的成本进行应用迭代,实现真正意义上的敏捷开发。
• 统一流量控制:通过使用一个服务网格实例同时管理两个集群,服务网格管理员将能够以统一的方式控制开发测试与发布节奏,同时让开发中的服务的上下游依赖与生产环境保持最大程度的一致,避免因为开发与生产的不一致导致的线上问题。
• 无侵入/低侵入性:服务网格ASM的流量泳道能力基于baggage透传实现,通过OpenTelemetry自动插装与服务网格Sidecar注入,所有上述能力可以在对业务代码无侵入的前提下实现,开发人员只需关注业务逻辑本身。
本文将介绍基于流量泳道的多集群多环境部署方案,演示一个示例云原生微服务应用从开发、测试到发布灰度的整套流程。
简介与环境准备
让我们开始吧!要实现刚才所说的一切,我们首先需要两个ACK集群、并将他们加入同一个服务网格实例中。
具体来说,首先需要准备下面的这些前提条件:
• 已创建ASM企业版或旗舰版实例,且版本为1.21.6.54及以上。具体操作,请参见创建ASM实例[1]或升级ASM实例[2]。
• 已创建两个ACK集群并添加集群到ASM实例。具体操作,请参见添加集群到ASM实例[3]。两个集群将分别作为生产集群和开发集群。
• 在生产集群和开发集群中分别创建名为ingressgateway和ingressgateway-dev的网关。具体操作,请参见创建入口网关[4]。
• 分别创建名称为ingressgateway、ingressgateway-dev,且命名空间为istio-system的网关规则。具体操作,请参见管理网关规则[5]。
apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: ingressgateway namespace: istio-system spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - '*' --- apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: ingressgateway-dev namespace: istio-system spec: selector: istio: ingressgateway-dev servers: - port: number: 80 name: http protocol: HTTP hosts: - '*'
当添加多个集群到服务网格ASM时,你可能需要一些网络规划以及配置,以保证两个集群之间可以相互访问,可以参考ASM准备的多集群管理概述[6]。
集群及服务网格环境就绪后,本文将通过使用OpenTelemetry自动插装的方法,为之后部署在两个集群内的服务添加Baggage透传能力。
在两个集群中,我们都需要执行以下步骤:
1. 部署OpenTelemetry Operator。
通过kubectl连接到ASM实例添加的Kubernetes集群。执行以下命令,创建opentelemetry-operator-system命名空间。
kubectl create namespace opentelemetry-operator-system
执行以下命令,使用Helm在opentelemetry-operator-system命名空间下安装OpenTelemetry Operator。(关于Helm安装步骤,请参见安装Helm[7])
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts helm install \ --namespace=opentelemetry-operator-system \ --version=0.46.0 \ --set admissionWebhooks.certManager.enabled=false \ --set admissionWebhooks.certManager.autoGenerateCert=true \ --set manager.image.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/opentelemetry-operator" \ --set manager.image.tag="0.92.1" \ --set kubeRBACProxy.image.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/kube-rbac-proxy" \ --set kubeRBACProxy.image.tag="v0.13.1" \ --set manager.collectorImage.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/opentelemetry-collector" \ --set manager.collectorImage.tag="0.97.0" \ --set manager.opampBridgeImage.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/operator-opamp-bridge" \ --set manager.opampBridgeImage.tag="0.97.0" \ --set manager.targetAllocatorImage.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/target-allocator" \ --set manager.targetAllocatorImage.tag="0.97.0" \ --set manager.autoInstrumentationImage.java.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/autoinstrumentation-java" \ --set manager.autoInstrumentationImage.java.tag="1.32.1" \ --set manager.autoInstrumentationImage.nodejs.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/autoinstrumentation-nodejs" \ --set manager.autoInstrumentationImage.nodejs.tag="0.49.1" \ --set manager.autoInstrumentationImage.python.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/autoinstrumentation-python" \ --set manager.autoInstrumentationImage.python.tag="0.44b0" \ --set manager.autoInstrumentationImage.dotnet.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/autoinstrumentation-dotnet" \ --set manager.autoInstrumentationImage.dotnet.tag="1.2.0" \ --set manager.autoInstrumentationImage.go.repository="registry-cn-hangzhou.ack.aliyuncs.com/acs/opentelemetry-go-instrumentation" \ --set manager.autoInstrumentationImage.go.tag="v0.10.1.alpha-2-aliyun" \ opentelemetry-operator open-telemetry/opentelemetry-operator
执行以下命令,检查opentelemetry-operator是否正常运行。
kubectl get pod -n opentelemetry-operator-system
预期输出:
NAME READY STATUS RESTARTS AGE opentelemetry-operator-854fb558b5-pvllj 2/2 Running 0 1m
2. 配置自动插装(auto-instrumentation)。
使用以下内容,创建instrumentation.yaml文件。
apiVersion: opentelemetry.io/v1alpha1 kind: Instrumentation metadata: name: demo-instrumentation spec: propagators: - baggage sampler: type: parentbased_traceidratio argument: "1"
执行以下命令,在default命名空间下声明自动插装。
kubectl apply -f instrumentation.yaml
看到这儿大家可能一头雾水这是在干啥。所以接下来我们先插点题外话,谈谈Baggage透传、自动插装以及服务网格ASM的宽松模式流量泳道是什么,以了解接下来一切发生的基础。或者也可以直接跳到步骤一开始。
1.分布式系统和调用链路
对于云原生微服务应用这样的分布式系统来说,系统整体往往通过一个网关对外暴露访问,而组成应用本体的多个微服务则部署在集群中,通过服务本地域名相互调用。
当一个请求到达网关时,为了对该请求作出响应,集群中往往将发起多次调用,因为服务在进行业务逻辑处理时,往往会对其依赖的其它服务发起远程方法调用(RPC)、这次调用就对应着发送了一个集群内的请求。其它服务收到请求后、也可能继续对其依赖的另外的服务发送请求。所有这些请求将组成一条调用链路。
例如,下图展示了一个由三个服务组成的分布式系统、依赖关系为mocka -> mockb -> mockc,当外部请求到达mocka时,将对应发起mocka调用mockb、mockb调用mockc的额外两次请求。这三个请求共同组成一条调用链路。
最后来看下宏观解释:
在广义上,一个调用链路代表一个事务或者流程在(分布式)系统中的执行过程。在OpenTracing标准中,调用链是多个Span组成的一个有向无环图(Directed Acyclic Graph,简称DAG),每一个Span代表调用链中被命名并计时的连续性执行片段。
可以看到,一条调用链路对应着多个相互独立的请求,它们之间的唯一关联就是都是为了响应同一条外部请求时发起的。但当大量的请求持续不断到达网关时,将无法建立后续请求与调用链路的任何关联。
2. Baggage:调用链路上下文透传标准
Baggage是OpenTelemetry推出的一种标准化机制,旨在实现分布式系统调用链路中跨进程传递上下文信息。
那么首先,OpenTelemetry是什么?我们可以看看这里的介绍:
https://opentelemetry.io/docs/what-is-opentelemetry/
OpenTelemetry is an Observabilityframework and toolkit designed to create and manage telemetry data such as traces, metrics, and logs.
如引用,OpenTelemetry是一个为了实现可观测数据统一管理而开发的可观测框架与工具集。那么这样的一个可观测工具能够如何帮到我们呢?这就需要提到instrumentation的概念。引用自OpenTelemetry官方文档:
In order to make a system observable, it must be instrumented: That is, code from the system’s components must emit traces, metrics, and logs.
OpenTelemetry的一项重要工作就是应用系统的instrumentation,也就是让业务代码变得“可被观测”,这意味着系统需要具有这些能力:
• 产生日志,以对接日志采集系统
• 在调用链路中透传链路信息(包括trace id等),以对接链路追踪系统
• 产生指标,以对接prometheus等指标采集系统
OpenTelemetry项目由CNCF社区在2019年提出,背靠CNCF和多家重量级云厂商支持,现在已经成为CNCF的顶级项目与云原生可观测领域的事实标准。
回到Baggage,它其实就是HTTP头部中名为“Baggage”的一个请求头,请求头内容则有着严格的规范,可通过键值对记录租户ID、追踪ID、安全凭证等调用链路的上下文数据。例如:
baggage: userId=alice,serverNode=DF%2028,isProduction=false
OpenTelemetry社区在提出Baggage标准的同时,也提供了多种方法帮助应用服务在同一条调用链路上透传Baggage请求头,这样就可以在调用链路的任意一个请求上获取到当前调用链路的上下文信息。
一般来说,可以在服务的代码中接入OpenTelemetry SDK来透传Baggage;而对于部署在Kubernetes集群中的云原生应用来说,则还可以使用OpenTelemetry Operator进行自动插装,这种方式无需修改业务代码,本文中将采用这种方式。
3. 调用链路上下文与流量泳道
前面说了一大堆调用链路与链路上下文相关概念,这和本文场景有什么关系呢?本文场景主要基于服务网格ASM的多集群管理和流量泳道(宽松模式)来实现。
多集群管理在前文中已有所描述,这里来看看流量泳道(宽松模式),它主要提供了按需创建应用的隔离环境(以用于开发测试或灰度)的能力。
如下图示例,当需要构建开发环境/灰度版本环境时,我们可以通过服务pod的标签来区分pod版本(例如,通过pod的version标签区分)。流量泳道能够将应用的相关版本(也可以是其他特征)隔离成一个独立的运行环境(即泳道),并控制整条调用链路内部请求的走向,保证请求目标始终是相同的版本。
同时,流量泳道(宽松模式)额外提供了在环境中按需部署服务的能力。例如下方示例中,mockb服务并没有在v2版本进行任何更改,此时便无需额外部署mockb的v2版本,只需要将mockb的v1版本指定为基线,请求调用目标会自动转向v1版本,并在同一条调用链路的后续请求中、继续向v2版本的mockc服务发起调用。
流量泳道(宽松模式)主要通过以下过程来实现上述场景:
1)流量打标:
当请求经过ASM网关后,可以通过服务网格的虚拟服务定义路由规则、将请求路由到系统入口服务的不同版本。此时,可以通过虚拟服务为请求打标、以标识请求目标是应用的哪个版本。所谓打标就是为请求添加一个特定的请求头,例如上图中添加了一个tag请求头,可通过tag: v1、tag: v2来区分请求的目标版本。
2)流量标签透传:
服务网格ASM将设法在一条调用链路上维持流量标签的透传(也就是为调用链路上的所有请求加入相同的标签请求头)。
由于调用链路上下文的透传和业务代码高度相关,对业务不具备侵入性的服务网格无法直接完成。服务网格ASM借助云原生可观测业界的几种链路透传成熟场景来完成这一步骤:
• Baggage透传:也就是上文提到的Baggage标准。Baggage是OpenTelemetry社区主推的链路上下文透传标准,因此ASM也推荐基于这种方式完成标签透传。而且,基于OpenTelemetry Operator自动插装,我们还有望以无侵入式的方法完成。
• Trace ID透传:在多种分布式链路追踪标准中(如W3C TraceContext、b3、datadog),都存在Trace ID的概念。Trace ID往往是一个不重复的随机id,用来独立标识每条调用链路,一条调用链路上的不同请求将具有相同的Trace ID。
• 自定义请求头透传:在应用服务代码中,可能本身就透传了某些有业务意义的请求头。
在使用流量泳道(宽松模式时),只需要指定自己的应用满足了以上哪种场景,服务网格ASM将会自动根据对应场景帮您配置流量标签信息的保存与恢复,保证一条请求链路上始终都有流量标签这个请求头的。
3)基于流量标签进行路由:
在调用链路上的每次请求发起时,服务网格会首先帮您在请求上恢复链路的标签信息(即添加标签请求头),接下来将根据标签请求头、将请求头路由到服务的对应版本。路由本身基于虚拟服务完成。
4)流量回退:
在对请求进行路由时,服务网格将检测服务的目标版本是否存在。在目标版本不存在的情况下,请求目标将回退到一个预设的基线版本。流量回退是按需部署的关键,这大大提高了开发测试与发布流程的效率与灵活性,并降低了资源消耗。
一、部署应用第一个稳定版本v1
在示例中,我们将使用名为mock的应用来模拟微服务应用的测试与发布流程。该应用由三个服务(mocka、mockb、mockc)组成,每个服务在环境变量中声明彼此的依赖关系以及自身版本信息,并形成一条mocka->mockb-> mockc的调用链路。
当访问mocka服务时,响应体中将会记录整条调用链路上服务的版本以及ip地址信息,以方便观察,例如:
-> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32)
示例涉及到使用两个集群的kubeconfig、分别连接到两个集群来部署工作负载。在后续的内容中,本文假设两个集群的kubeconfig文件已经分别保存在~/.kube/config和~/.kube/config2路径中;,通过这种方式,可以通过kubectl操作生产部署集群中的服务、可以通过kubectl --kubeconfig ~/.kube/config2的方式操作开发测试集群中的服务。
1.部署v1版本服务
1)为default命名空间启用Sidecar网格代理自动注入。具体操作,请参见管理全局命名空间[8]。
2)使用以下内容,创建mock.yaml文件。
apiVersion: v1 kind: Service metadata: name: mocka labels: app: mocka service: mocka spec: ports: - port: 8000 name: http selector: app: mocka --- apiVersion: apps/v1 kind: Deployment metadata: name: mocka-v1 labels: app: mocka version: v1 spec: replicas: 1 selector: matchLabels: app: mocka version: v1 ASM_TRAFFIC_TAG: v1 template: metadata: labels: app: mocka version: v1 ASM_TRAFFIC_TAG: v1 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v1 - name: app value: mocka - name: upstream_url value: "http://mockb:8000/" ports: - containerPort: 8000 --- apiVersion: v1 kind: Service metadata: name: mockb labels: app: mockb service: mockb spec: ports: - port: 8000 name: http selector: app: mockb --- apiVersion: apps/v1 kind: Deployment metadata: name: mockb-v1 labels: app: mockb version: v1 spec: replicas: 1 selector: matchLabels: app: mockb version: v1 ASM_TRAFFIC_TAG: v1 template: metadata: labels: app: mockb version: v1 ASM_TRAFFIC_TAG: v1 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v1 - name: app value: mockb - name: upstream_url value: "http://mockc:8000/" ports: - containerPort: 8000 --- apiVersion: v1 kind: Service metadata: name: mockc labels: app: mockc service: mockc spec: ports: - port: 8000 name: http selector: app: mockc --- apiVersion: apps/v1 kind: Deployment metadata: name: mockc-v1 labels: app: mockc version: v1 spec: replicas: 1 selector: matchLabels: app: mockc version: v1 ASM_TRAFFIC_TAG: v1 template: metadata: labels: app: mockc version: v1 ASM_TRAFFIC_TAG: v1 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v1 - name: app value: mockc ports: - containerPort: 8000
对于每个实例服务Pod,都加入了instrumentation.opentelemetry.io/inject-java: "true"和instrumentation.opentelemetry.io/container-names: "default"两个注解,以声明该实例服务使用Java语言实现,并要求OpenTelemetry Operator对名称为default的容器进行自动插装。
同时,每个Pod都具有version: v1的标签,以表明自己属于最初的稳定版本v1。
3)执行以下命令,部署实例服务。
kubectl apply -f mock.yaml
基于OpenTelemetry自动插装机制,部署的服务Pod将自动具有在调用链路中传递Baggage的能力。
2. 为应用创建泳道组和流量泳道
服务网格ASM通过泳道组和泳道来管理分布式应用中的所有服务。泳道组(ASMSwimLaneGroup)负责统一声明应用中包含的服务、泳道模式、依赖的链路透传方式以及每个服务的基线版本等信息;泳道(ASMSwimLane)则声明了应用的一套隔离环境需要包含的信息,主要包括使用哪个标签来识别服务版本,并可以按需向泳道中添加泳道组中的部分服务。
1)使用以下内容,创建swimlane-v1.yaml文件。
# 泳道组的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLaneGroup metadata: name: mock spec: ingress: gateway: name: ingressgateway namespace: istio-system type: ASM isPermissive: true # 声明泳道组内泳道都为宽松模式 permissiveModeConfiguration: routeHeader: version # 流量标签请求头为version serviceLevelFallback: # 声明服务的基线版本,当前集群中只有v1版本,所以基线都为v1 default/mocka: v1 default/mockb: v1 default/mockc: v1 traceHeader: baggage # 基于baggage透传来完成流量标签透传 services: # 声明整个应用包含哪些服务 - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 # 生产集群id name: mocka namespace: default - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 name: mockb namespace: default - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 name: mockc namespace: default --- # 泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: v1 spec: labelSelector: version: v1 services: # 指定v1版本中包含应用里的哪些服务,对于第一个稳定版本,该泳道内必然包含泳道组内全部服务 - name: mocka namespace: default - name: mockb namespace: default - name: mockc namespace: default
2)执行以下命令,为mock应用部署泳道组和v1版本的泳道定义。
kubectl apply -f swimlane-v1.yaml
3. 创建网关上的虚拟服务,为v1版本应用引流
在上述两步之后,集群中的应用以及版本隔离环境都已就绪,最后只需要将应用通过网关对外暴露就可以完成应用上线了。
1)使用以下内容,创建ingress-vs.yaml文件。
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs namespace: istio-system spec: gateways: - istio-system/ingressgateway hosts: - '*' http: - route: - destination: host: mocka.default.svc.cluster.local subset: v1 # 输入泳道名称v1 headers: # 进行流量打标 request: set: version: v1
说明
• 要让虚拟服务在网关上生效,需要依赖已经创建的网关规则(Gateway CR)。在前提条件中,我们已经创建了名为ingressgateway的ASM网关以及同名的网关规则。上述虚拟服务中引用的就是这个网关规则。
• 在虚拟服务中,为路由项添加了headers部分的配置,此部分主要用于流量打标,当路由目标被确定为v1版本时,通过设置version: v1的请求头让调用链带上v1版本的上下文信息。
• 在实际生产实践中,还包括在网关上配置域名、https、限流等实践,本文省略了这些内容,专注开发测试与发布流程。
2)执行以下命令,为mock应用的第一个版本创建对外暴露的路由规则。
kubectl apply -f ingress-vs.yaml
3)测试访问应用:可以通过curl网关ip地址的方式来访问mock应用。
curl {网关IP地址} -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32)
有关如何获取ASM网关的地址,可以参考查看网关信息[9]。可以从输出中看到,整体流量的链路始终经过服务的v1版本,符合预期,应用上线成功!此时集群中的流量拓扑如图:
二、在v1版本的基础上开始迭代开发测试
当应用的v1版本上线成功后,开发及测试人员将会在v1版本的基础上对应用进行迭代,开发v2版本。首先在开发测试集群中也创建相同的mock Service。
执行以下命令:
kubectl --kubeconfig ~/.kube/config2 apply -f- <<EOF apiVersion: v1 kind: Service metadata: name: mocka labels: app: mocka service: mocka spec: ports: - port: 8000 name: http selector: app: mocka --- apiVersion: v1 kind: Service metadata: name: mockb labels: app: mockb service: mockb spec: ports: - port: 8000 name: http selector: app: mockb --- apiVersion: v1 kind: Service metadata: name: mockc labels: app: mockc service: mockc spec: ports: - port: 8000 name: http selector: app: mockc EOF
现在假设应用的v2版本需要更新mocka和mockc两个服务,而现在有Alice和Caros两名开发人员、分别对mocka和mockc进行开发。两人可以通过创建泳道来分别创建自己的开发环境。
对于Alice而言,她可以使用如下内容,创建alice-dev.yaml文件。
# 开发版本的工作负载 apiVersion: apps/v1 kind: Deployment metadata: name: mocka-dev-alice labels: app: mocka version: dev-alice spec: replicas: 1 selector: matchLabels: app: mocka version: dev-alice template: metadata: labels: app: mocka version: dev-alice annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: dev-alice - name: app value: mocka - name: upstream_url value: "http://mockb:8000/" ports: - containerPort: 8000 --- # 泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: dev-alice spec: labelSelector: version: dev-alice services: # 指定alice的开发环境中包含哪些服务(其它服务都走基线版本) - name: mocka namespace: default --- # 开发环境的引流配置 apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs-alice namespace: istio-system spec: gateways: - istio-system/ingressgateway-dev hosts: - '*' http: - match: - headers: alice-dev: exact: "true" name: route-alice route: - destination: host: mocka.default.svc.cluster.local subset: dev-alice # 发往开发版本的mocka服务 headers: request: set: version: dev-alice #流量打标
接下来执行如下指令来创建她的开发环境:
kubectl --kubeconfig ~/.kube/config2 apply -f alice-dev.yaml
这为Alice正在开发的服务mocka创建了一个工作负载,并通过泳道和引流规则的声明式定义指定了Alice使用的隔离环境,通过向请求头添加alice-dev: true就可以访问到这个环境。访问时,使用开发集群中的ASM网关IP地址,IP获取方式同上。
curl -H "alice-dev: true" {开发测试环境ASM网关IP} -> mocka(version: dev-alice, ip: 192.168.0.23)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32)
对于Caros来说,也是如法炮制,使用如下内容创建caros-dev.yaml文件。
# 开发版本的工作负载 apiVersion: apps/v1 kind: Deployment metadata: name: mockc-dev-caros labels: app: mockc version: dev-caros spec: replicas: 1 selector: matchLabels: app: mockc version: dev-caros template: metadata: labels: app: mockc version: dev-caros annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: dev-caros - name: app value: mockc ports: - containerPort: 8000 --- # 泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: dev-caros spec: labelSelector: version: dev-caros services: # 指定caros的开发环境中包含哪些服务(其它服务都走基线版本) - name: mockc namespace: default --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs-caros namespace: istio-system spec: gateways: - istio-system/ingressgateway-dev hosts: - '*' http: - match: - headers: caros-dev: exact: "true" name: route-caros route: - destination: host: mocka.default.svc.cluster.local subset: v1 # caros的环境仍使用v1的mocka服务,但为流量打上dev-caros的标签,后续发往dev-caros版本 headers: request: set: version: dev-caros #流量打标
执行指令以创建环境:
kubectl --kubeconfig ~/.kube/config2 apply -f caros-dev.yaml
现在,用不同的请求头对开发集群的网关进行访问,可以观测到如下结果:
curl -H "alice-dev: true" {开发集群ASM网关IP} -> mocka(version: dev-alice, ip: 192.168.0.23)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) curl -H "caros-dev: true" {开发集群ASM网关IP} -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: dev-caros, ip: 192.168.0.24)
此时的流量拓扑如图所示:
生产与开发环境分别使用各自的ASM网关进行引流,避免了生产与开发测试环境引流规则的互相干扰。同时,开发人员可以按需、高效地创建自己的开发测试环境,在开发测试环境中,只需要部署自己开发的服务即可,应用中的其它服务可以直接使用基线稳定版本。
在开发结束后,可以使用刚才创建环境使用的yaml文件快速销毁开发环境(可选操作)。
kubectl --kubeconfig ~/.kube/config2 delete -f alice-dev.yaml kubectl --kubeconfig ~/.kube/config2 delete -f caros-dev.yaml
或者也可以继续保留这两个开发环境,本文中选择继续保留这两个开发环境、供开发及测试人员继续后续使用。
三、部署应用的v2版本,并在v1/v2两个版本进行流量比例灰度
当开发测试完成后,运维人员可以将新的mocka、mockc服务发布到生产集群中,并为应用新建一个v2版本,并开启对v2版本的灰度发布流程。整个过程与发布v1版本时类似。
1. 部署v2版本的服务
1)使用以下内容,创建mock-v2.yaml。
apiVersion: apps/v1 kind: Deployment metadata: name: mocka-v2 labels: app: mocka version: v2 spec: replicas: 1 selector: matchLabels: app: mocka version: v2 ASM_TRAFFIC_TAG: v2 template: metadata: labels: app: mocka version: v2 ASM_TRAFFIC_TAG: v2 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v2 - name: app value: mocka - name: upstream_url value: "http://mockb:8000/" ports: - containerPort: 8000 --- apiVersion: apps/v1 kind: Deployment metadata: name: mockc-v2 labels: app: mockc version: v2 spec: replicas: 1 selector: matchLabels: app: mockc version: v2 ASM_TRAFFIC_TAG: v2 template: metadata: labels: app: mockc version: v2 ASM_TRAFFIC_TAG: v2 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v2 - name: app value: mockc ports: - containerPort: 8000
2)执行以下命令,部署实例服务。
kubectl apply -f mock-v2.yaml
相比于应用的v1版本,v2迭代并没有增加新的服务,并且只迭代了mocka与mockc服务,因此部署内容只包括这两个服务的带有version: v2标签的deployment。
2. 为v2版本创建流量泳道
如法炮制,当迭代应用的一个新版本时,我们只需要根据其对应的工作负载标签以及迭代包含的服务、以声明式方法创建一个流量泳道。
1)使用以下内容,创建swimlane-v2.yaml文件。
# v2泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: v2 spec: labelSelector: version: v2 services: # 指定v2版本中包含应用里的哪些服务(mocka和mockc) - name: mocka namespace: default - name: mockc namespace: default
2)执行以下命令,为mock应用部署泳道组和v2版本的泳道定义。
kubectl apply -f swimlane-v2.yaml
相比v1版本,v2版本的泳道只包含mocka和mockc服务,并且使用version: v2来匹配对应版本的服务。
3. 修改网关上的虚拟服务,在v1和v2两个版本之间进行比例灰度
v2版本环境部署完成后,可以通过修改原先生效在网关上的虚拟服务、让流量以一定的比例发送到v2版本,完成一个灰度发布的流程。
修改上文中创建的ingress-vs.yaml文件,变更为以下内容:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs namespace: istio-system spec: gateways: - istio-system/ingressgateway hosts: - '*' http: - route: - destination: host: mocka.default.svc.cluster.local subset: v1 # 输入泳道名称v1 weight: 80 headers: # 进行流量打标 request: set: version: v1 - destination: host: mocka.default.svc.cluster.local subset: v2 # 输入泳道名称v2 weight: 20 headers: # 进行流量打标 request: set: version: v2
这个虚拟服务相比原先增加了一个路由目的地(v2版本的mocka服务),并为请求加入version: v2的请求(即请求打标)、 标识链路上的后续请求要保持在v2版本的泳道内部。v1、v2版本之间通过weight字段设置了80:20的流量比例,从而使得少部分流量发往v2版本,在线上测试v2版本的稳定性。
此时不断对生产集群的网关发起访问,可以看到流量以约80:20的比例发送到两个版本。v1版本应用的调用链路为v1 -> v1 -> v1,而v2版本应用则为v2 -> v1 -> v2。
for i in {1..100}; do curl http://{ASM网关ip}/ ; echo ''; sleep 1; done; -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v2, ip: 192.168.0.30)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v2, ip: 192.168.0.31) -> mocka(version: v1, ip: 192.168.0.26)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v1, ip: 192.168.0.32) -> mocka(version: v2, ip: 192.168.0.30)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v2, ip: 192.168.0.31) ……
此时,集群中的流量拓扑如图所示:
四、正式上线应用v2版本,切换服务的基线版本
1. 正式上线v2版本
在v2版本灰度上线后,我们可以逐步切换v1、v2两个版本的流量,这通过调整上述ingress-vs.yaml文件中两个泳道的比例来完成。最终,当v2版本的流量比例切成100后,线上已经没有请求发送到v1版本了。
此时的ingress-vs.yaml文件应该如下:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs namespace: istio-system spec: gateways: - istio-system/ingressgateway hosts: - '*' http: - route: - destination: host: mocka.default.svc.cluster.local subset: v2 headers: request: set: version: v2
此时集群中的流量拓扑如下:
此时再去访问生产集群网关,可以看到所有的请求返回都是v2 -> v1 -> v2(请求输出不再演示)。v2版本已经完成正式上线。
2. 切换服务基线版本
在发版的最后,由于线上的mocka、mockc服务已经更新,我们要切换这两个服务的基线版本、并清理不再使用的mocka、mockc服务的v1版本。
修改上文中创建的swimlane-v1.yaml文件(这个文件同时包含了泳道组和v1版本泳道的定义),替换为以下内容:
# 泳道组的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLaneGroup metadata: name: mock spec: ingress: gateway: name: ingressgateway namespace: istio-system type: ASM isPermissive: true # 声明泳道组内泳道都为宽松模式 permissiveModeConfiguration: routeHeader: version # 流量标签请求头为version serviceLevelFallback: # 声明服务的基线版本,将mocka、mockc服务的基线更新为v2 default/mocka: v2 default/mockb: v1 default/mockc: v2 traceHeader: baggage # 基于baggage透传来完成流量标签透传 services: # 声明整个应用包含哪些服务 - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 # 生产集群id name: mocka namespace: default - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 name: mockb namespace: default - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 name: mockc namespace: default --- # 泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: v1 spec: labelSelector: version: v1 services: # 指定v1版本中包含应用里的哪些服务,在更新基线后,v1版本泳道中可以去除mocka、mockc服务 - name: mockb namespace: default
上述的声明式配置更新将mocka、mockc服务的基线版本调整为v2,并在v1版本泳道中去除了mocka、mockc服务,代表这两个服务的v1版本已经被废弃掉了。
此时所有的流量都不再经过v1版本的mocka、mockc服务,集群中的流量拓扑如图所示:
通过访问Alice的开发环境,可以观察到基线版本的切换:
curl -H "alice-dev:true" {开发集群ASM网关IP} -> mocka(version: dev-alice, ip: 192.168.0.23)-> mockb(version: v1, ip: 192.168.0.20)-> mockc(version: v2, ip: 192.168.0.31)
最后可以删除这两个服务的v1版本deployment,完成下线:
kubectl delete deployment mocka-v1 mockc-v1
五、开发并测试第二个迭代版本v3
在v2版本正式发布后,应用接下来进入v3版本的开发迭代流程中。我们假设v3版本仅对mockb服务进行了一轮更新。假设有一名负责mockb的开发人员Bob现在正在对mockb服务进行新一轮的开发,他可以像Alice和Caros一样、如法炮制地创建一个自己的开发环境:
使用如下内容,创建bob-dev.yaml文件:
# 开发版本的工作负载 apiVersion: apps/v1 kind: Deployment metadata: name: mockb-dev-bob labels: app: mockb version: dev-bob spec: replicas: 1 selector: matchLabels: app: mockb version: dev-bob template: metadata: labels: app: mockb version: dev-bob annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: dev-bob - name: app value: mockb - name: upstream_url value: "http://mockc:8000/" ports: - containerPort: 8000 --- # 泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: dev-bob spec: labelSelector: version: dev-bob services: # 指定bob的开发环境中包含哪些服务(其它服务都走基线版本) - name: mockb namespace: default --- # 开发环境的引流配置 apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs-bob namespace: istio-system spec: gateways: - istio-system/ingressgateway-dev hosts: - '*' http: - match: - headers: bob-dev: exact: "true" name: route-bob route: - destination: host: mocka.default.svc.cluster.local subset: v2 # 发往基线版本的mocka服务 headers: request: set: version: dev-bob #流量打标,指定后续流量发往bob开发环境
接下来执行如下指令来创建他的开发环境:
kubectl --kubeconfig ~/.kube/config2 apply -f bob-dev.yaml
这为Bob正在开发的服务mockb创建了一个工作负载,并通过泳道和引流规则的声明式定义指定了Bob使用的隔离环境,通过向请求头添加bob-dev: true就可以访问到这个环境。访问时,使用开发集群中的ASM网关IP地址,IP获取方式同上。
curl -H "bob-dev: true" {开发测试环境ASM网关IP} -> mocka(version: v2, ip: 192.168.0.30)-> mockb(version: dev-bob, ip: 192.168.0.80)-> mockc(version: v2, ip: 192.168.0.31)
在添加了Bob的开发测试环境后,集群中的流量拓扑如下:
六、灰度发布上线应用v3版本,删除不再接受流量的版本v1
最后,我们对应用的v3版本进行上线,运维人员将执行发布v2版本时类似的操作。
1.部署v3版本的服务
1)使用以下内容,创建mock-v3.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: mockb-v3 labels: app: mockb version: v3 spec: replicas: 1 selector: matchLabels: app: mockb version: v3 ASM_TRAFFIC_TAG: v3 template: metadata: labels: app: mockb version: v3 ASM_TRAFFIC_TAG: v3 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v3 - name: app value: mockb - name: upstream_url value: "http://mockc:8000/" ports: - containerPort: 8000
2)执行以下命令,部署实例服务。
kubectl apply -f mock-v3.yaml
v3版本只迭代了mocka与mockc服务,因此部署内容只包括mockb服务带有version: v3标签的deployment。
2. 为v3版本创建流量泳道
1)使用以下内容,创建swimlane-v3.yaml文件。
# v3泳道的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLane metadata: labels: swimlane-group: mock # 指定从属泳道组 name: v3 spec: labelSelector: version: v3 services: # 指定v3版本里包含哪些服务迭代(只有mockb迭代了) - name: mockb namespace: default
2)执行以下命令,为mock应用部署泳道组和v2版本的泳道定义。
kubectl apply -f swimlane-v3.yaml
相比v1版本,v2版本的泳道只包含mocka和mockc服务,并且使用version: v2来匹配对应版本的服务。
3. 修改网关上的虚拟服务,对v3版本进行发布上线
v3版本环境部署完成后,可以通过修改原先生效在网关上的虚拟服务将流量转发到应用的v3版本。
修改上文中创建的ingress-vs.yaml文件,变更为以下内容:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: swimlane-ingress-vs namespace: istio-system spec: gateways: - istio-system/ingressgateway hosts: - '*' http: - route: - destination: host: mocka.default.svc.cluster.local subset: v2 # 将流量发往mocka服务的最新版本(目前是v2) headers: # 进行流量打标 request: set: version: v3
此处省略了灰度发布的过程(正常来说还要在v2、v3之间进行流量比例的切换),直接对v3版本的应用进行发布上线。
上线后,仍然和上线v2版本时相同,修改泳道组中记录的服务基线版本。修改上文中创建的swimlane-v1.yaml文件,变更为以下内容:
# 泳道组的声明式配置 apiVersion: istio.alibabacloud.com/v1 kind: ASMSwimLaneGroup metadata: name: mock spec: ingress: gateway: name: ingressgateway namespace: istio-system type: ASM isPermissive: true # 声明泳道组内泳道都为宽松模式 permissiveModeConfiguration: routeHeader: version # 流量标签请求头为version serviceLevelFallback: # 声明服务的基线版本,将mockb服务的基线更新为v3 default/mocka: v2 default/mockb: v3 default/mockc: v2 traceHeader: baggage # 基于baggage透传来完成流量标签透传 services: # 声明整个应用包含哪些服务 - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 # 生产集群id name: mocka namespace: default - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 name: mockb namespace: default - cluster: id: ce6ed3969d62c4baf89fb3b7f60be7f73 name: mockc namespace: default
执行以下指令来更新泳道组中的服务基线版本声明:
kubectl apply -f swimlane-v1.yaml
再次访问生产集群ASM网关,可以发现链路已经变成v2 -> v3 -> v2。
curl {ASM网关IP} -> mocka(version: v2, ip: 192.168.0.30)-> mockb(version: v3, ip: 192.168.0.19)-> mockc(version: v2, ip: 192.168.0.31)
此时集群中的流量拓扑如图所示:
我们发现,对于最开始应用的v1版本来说,已经没有任何服务的v1版本还在接受流量了,这也意味着我们的v1版本可以进行彻底的下线,对应就是删除掉v1版本剩余的mockb服务deployment以及对应的泳道声明v1。
kubectl delete deployment mockb-v1 kubectl delete asmswimlane v1
对于这个应用今后的迭代,也只需参考上述流程循环往复即可。在本文中,涉及到的所有操作都基于YAML的声明式定义,因此这套方法也可以很顺滑地与已有的基于KubeAPI操作的CICD系统进行对接、实现发布流程的自动化(例如ArgoCD、云效等)。
小结
本文主要介绍了基于阿里云服务网格ASM的流量泳道以及多集群管理能力,在应用持续迭代的同时又可以在单独的开发测试集群中为每个开发及测试人员构建单独的应用隔离环境。
此外,隔离环境可以实现按需部署,并且所有操作可以使用声明式配置YAML完成,这些特点都大大增强了应用开发测试部署流程的效率并显著降低部署环境时的资源消耗。当基于OpenTelemetry Baggage透传方案实现流量泳道(宽松模式)时,整套方案对业务代码也可以实现一个无侵入效果,降低开发运维负担。
上述方案的前提是需要实现两个集群的网络打通,使得两个集群的服务可以彼此访问。对于这套最佳实践来说,可以选用“通过ASM东西向网关打通跨地域网络”的方法来进行网络互通配置,由于开发测试集群和生产集群之间的流量不会特别大,这套方案便可以显著地降低成本。具体可参考系列上一篇文章:阿里云服务网格ASM多集群实践(一)多集群管理概述。
相关链接:
[1]创建ASM实例
https://help.aliyun.com/zh/asm/getting-started/create-an-asm-instance#task-2370657
[2]升级ASM实例
https://help.aliyun.com/zh/asm/user-guide/update-an-asm-instance
[3]添加集群到ASM实例
https://help.aliyun.com/zh/asm/getting-started/add-a-cluster-to-an-asm-instance-1#task-2372122
[4]创建入口网关
https://help.aliyun.com/zh/asm/user-guide/create-an-ingress-gateway#task-2372970
[5]管理网关规则
https://help.aliyun.com/zh/asm/user-guide/manage-istio-gateways
[6]多集群管理概述
https://help.aliyun.com/zh/asm/user-guide/multi-cluster-management-overview
[7]安装Helm
https://helm.sh/zh/docs/intro/install/
[8]管理全局命名空间
https://help.aliyun.com/zh/asm/user-guide/manage-global-namespaces#section-30o-vil-3n7
[9]查看网关信息
我们是阿里巴巴云计算和大数据技术幕后的核心技术输出者。
获取关于我们的更多信息~