Skip to content

第41讲:如何结合服务网格进行灰度发布

上一课时介绍了如何实现持续部署,本课时将继续介绍持续部署相关的话题。在实际的生产环境部署中,不太可能一次性更新全部的后台服务的实例,而是需要逐步更新,本课时将对这种部署模式进行介绍。

灰度发布

灰度发布是国内特有的与发布相关的名词,从名称来说,灰度表示从白色到黑色的渐进过程,代表的是新版本的部署从完全未部署(白色)到完全部署(黑色)的过程。从含义上来说,灰度发布与我们通常说的金丝雀发布(Canary)、蓝绿发布(Blue/Green)、红黑发布(Red/Black)的含义是相似的。这些发布模式的共同点是,每次发布新版本时,不是一次性的全部更新,而是先进行部分更新,再逐步扩大更新的范围,最后完成全部的更新。

部分更新

这里提到的部分更新,实际上有两个维度:一个是从用户的角度来考虑的,当一个新的功能被开发并发布之后,一开始只有部分用户能够使用这个新功能,而在新功能的使用过程中,用户的反馈可以作为改进功能的基础。当新功能完善之后,可以对全部用户都启用该功能,最终完成整个更新过程。

另外一个维度是从应用运行时的实例的角度。一个应用运行时可能有多个实例,对应于 Kubernetes 上的多个 Pod。在新版本发布时,首先更新其中的一部分实例,再逐步增加更新到新版本的实例的数量,直到最后完成全部的更新。

这两种方式的区别在于用户的选择上。在第一种方式中,新功能的试用用户是精心挑选的,一般通过对用户的历史行为进行分析,来选择合适的用户,一个用户能否使用新功能的状态是固定不变的。在第二种方式中,用户是否可以使用新版本是随机的,取决于负载均衡器把用户的请求发送到当前版本还是新版本的运行实例。当用户在不同的时候访问服务时,他所看到的内容可能是不同的。

灰度发布与 Kubernetes 上部署的滚动更新机制是不同的。滚动更新在进行过程中,也是先更新一部分的 Pod 实例,再扩展至全部的 Pod 实例。但是在滚动更新中,新旧版本共存只是一种暂时的中间状态。Kubernetes 通过 Pod 中容器所定义的**探测器(Readiness Probe)**来判断新版本是否可用,并不会进行复杂的测试。在灰度发布中,新旧版本会共存一段时间,有专门的针对应用行为的测试。

下面对金丝雀发布、蓝绿发布和红黑发布进行介绍。

金丝雀发布

在 20 世纪初期,煤矿工人在下井之前,会先把金丝雀放入矿井,用来检测一氧化碳等有毒气体,进而提早发现问题。金丝雀发布由此而得名,在进行完整的版本更新之前,首先在新的环境上部署新版本,再把很小一部分的流量转到新版本,这部分的流量充当了金丝雀的作用。接着在一段时间之内,通过各种指标来比较新旧两个版本,当确认新版本没有问题之后,再把新版本的更新范围扩大,直到完成全部的部署。

蓝绿发布

在蓝绿发布的策略中,需要两个完全相同的生产环境,分别称为蓝色环境绿色环境。在这两个生产环境中,只有一个负责接受实际的请求,另外一个是交互准备环境。比如,如果蓝色环境是目前实际的生产环境,那么当需要发布新版本时,首先在绿色环境上进行部署和测试;当绿色环境测试完成之后,通过切换负载均衡器的方式,把请求转到绿色环境,之前的蓝色环境则处于闲置状态。等下一次部署时,蓝色环境就成为交互准备环境,蓝绿两个环境交替使用。当新版本运行的过程中出现问题时,可以快速切换到另外一个环境,从而回退到上一个版本。

下图是蓝绿发布的示意图,负载均衡器会轮流指向蓝绿两个不同的环境。

红黑发布

红黑发布与蓝绿发布存在一些相似之处。下面是红黑发布的基本流程:

  1. 目前在生产环境上运行的称为红色分组;

  2. 创建新版本的生产环境,与之前的版本同时运行,这种状态称为红/红状态;

  3. 把流量从当前版本切换到新版本,此时的状态称为黑/红状态;

  4. 如果新版本运行正常,则删除之前的版本,只保留一个版本,为下一次发布做准备。

下图给出了红黑发布的示意图,按照从上到下的顺序对应于上述的 4 个步骤。

与蓝绿发布的区别在于,红黑发布在大部分情况下只有一个生产环境,也就是红色环境。只有在部署过程中,才会同时存在红黑两个环境,黑色环境的存在是暂时的。与蓝绿发布相比,红黑发布适合于发布频率较低的情况;如果发布的频率很高,那么蓝绿发布的模式更加适用,因为不需要重复的创建和销毁环境。

使用服务网格

对于上面的这些发布方式,都要求控制流量在不同目的地之间的分配,这刚好是服务网格可以起作用的地方。下面以蓝绿发布为例,来说明如何通过服务网格实现。

蓝绿部署

在进行部署时,我们需要准备蓝绿两个环境,这在 Kubernetes 上很容易实现,只需要创建两个不同的部署即可。为了支持这种部署方式,需要把 Helm 图表分成两个,一个用来创建部署,另外一个用来创建服务。

在之前创建的地址管理服务的 Helm 图表中,删除掉 templates 目录下的 service.yaml 文件,同时在 _helpers.tpl 文件中定义两个新的变量 selectorLabelsWithDeploymentType 和 nameWithDeploymentType,如下面的代码所示。这两个变量都引用了配置项 deploymentType,表示部署的类型,可选值是 blue 和 green,分别表示蓝色和绿色部署。

变量 selectorLabelsWithDeploymentType 用在部署的 spec.selector.matchLabels 属性中,用来选择部署中的 Pod 实例,不同类型部署的 Pod 实例是分开的;nameWithDeploymentType 变量作为部署的名称。

java
{ {/* 
Selector labels with deployment type 
*/}} 
{ {- define "address-service.selectorLabelsWithDeploymentType" -}} 
{ { include "address-service.selectorLabels" . }} 
app.vividcode.io/deployment-type: { { .Values.deploymentType | quote }} 
{ {- end }} 
{ {/* 
Create the service name with deployment type 
*/}} 
{ {- define "address-service.nameWithDeploymentType" -}} 
{ {- printf "%s-%s" (include "address-service.name" .) .Values.deploymentType }} 
{ {- end }}

另外一个名为 address-service-common 的 Helm 图表中包含了 Kubernetes 中服务的声明,该声明是蓝绿两个部署所共用的,服务的名称固定为 address-service。服务的选择器的声明如下所示,从中可以看到,选择器只根据应用的名称来进行选择。

java
{ {/* 
Selector labels 
*/}} 
{ {- define "address-service-common.selectorLabels" -}} 
app.kubernetes.io/name: { { include "address-service-common.serviceName" . }} 
{ {- end }}

下面是地址管理服务对应的 helmfile.yaml 文件的内容,该文件定义了 3 个 Helm 发行,分别对应于 PostgreSQL 数据库、address-service-common 图表和地址管理服务本身。环境变量 DEPLOYMENT_TYPE 表示部署的类型,该变量的值作为 Helm 发行的名称的一部分,同时也传递给 Helm 图表中的配置项 deploymentType。

yaml
repositories: 
- name: bitnami 
  url: https://charts.bitnami.com/bitnami 
releases: 
  - name: postgresql-address 
    namespace: { { env "NAMESPACE" | default "happyride" }}
    chart: bitnami/postgresql 
    version: 8.10.13
    wait: false 
    values: 
      - ../postgresql-config.yaml 
      - config.yaml 
  - name: address-service-common 
    namespace: { { env "NAMESPACE" | default "happyride" }}
    chart: charts/address-service-common
  - name: address-service-{ { requiredEnv "DEPLOYMENT_TYPE" }}
    namespace: { { env "NAMESPACE" | default "happyride" }}
    chart: charts/address-service 
    values: 
      - config.yaml 
      - address-service-config.yaml 
      - deploymentType: { { requiredEnv "DEPLOYMENT_TYPE" | quote }}
        appVersion: { { requiredEnv "ADDRESS_SERVICE_VERSION" | quote }}
        image: 
          repository: { { printf "%shappyride/happyride-address-service" (env "CONTAINER_REGISTRY" | default "" ) | quote }}

下面的代码是 Kubernetes 部署的 YAML 文件的部分内容,对应于蓝色部署,从中可以看到不同标签的用法。

yaml
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: address-service-blue 
  labels: 
    helm.sh/chart: address-service-0.0.1 
    app.kubernetes.io/name: address-service 
    app.kubernetes.io/instance: address-service-blue 
    app.vividcode.io/deployment-type: "blue" 
    app.kubernetes.io/version: "1.0.0-fe220c2" 
    app.kubernetes.io/managed-by: Helm 
spec: 
  replicas: 1 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: address-service 
      app.kubernetes.io/instance: address-service-blue 
      app.vividcode.io/deployment-type: "blue" 
  template: 
    metadata: 
      labels: 
        app.kubernetes.io/name: address-service 
        app.kubernetes.io/instance: address-service-blue 
        app.vividcode.io/deployment-type: "blue"

在 helmfile 进行部署时,通过环境变量 DEPLOYMENT_TYPE 的不同值来触发蓝色或绿色部署,每个部署有各自的部署类型和版本号。

下面的代码是触发蓝色部署的命令的示例。

shell
$ ADDRESS_SERVICE_VERSION=1.0.0-fe220c2 DEPLOYMENT_TYPE=blue helmfile apply

在完成部署之后,我们需要通过服务网格来控制流量。

基于百分比的流量控制

在服务网格的帮助下,可以很容易地实现灰度发布所需要的流量控制功能。

以 Istio 为例来进行说明,下面代码中的目的地规则定义了地址管理服务的两个子集,分别对应于蓝色和绿色的部署,通过特定的标签来选择。

yaml
apiVersion: networking.istio.io/v1beta1 
kind: DestinationRule 
metadata: 
  name: address-service 
spec: 
  host: address-service.happyride.svc.cluster.local 
  subsets: 
    - name: blue 
      labels: 
        app.vividcode.io/deployment-type: "blue" 
    - name: green 
      labels: 
        app.vividcode.io/deployment-type: "green"

下面的代码是地址管理服务对应的 Istio 中的虚拟服务。该虚拟服务定义了在蓝绿两个部署之间的流量分配策略,其中 99% 的请求会被发到当前版本对应的蓝色部署,剩下 1% 的请求才会被发到新版本对应的绿色部署。在一开始的时候,只有极少的请求会被发送到新版本的实例,这些请求充当了金丝雀的作用。通过这些请求,可以对新版本进行验证。

随着测试的进行,我们会对两个部署之间的流量分配进行调整。等测试完成之后,蓝色部署的 weight 值将变为 0,而绿色部署的 weight 值会变为 100,从而完成新旧两个版本的完全切换。

yaml
apiVersion: networking.istio.io/v1beta1 
kind: VirtualService 
metadata: 
  name: address-service-deployment 
spec: 
  hosts: 
    - address-service.happyride.svc.cluster.local 
  http: 
    - name: "address-service-http" 
      route: 
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: blue 
          weight: 99 
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: green 
          weight: 1

在完成一次版本更新之后,蓝绿部署的角色会进行互换。每次在进行新版本的部署时,总是从当前 weight 值为 0 的部署开始。

基于用户的流量控制

除了根据百分比来分配蓝绿两个部署的流量之外,还可以根据自定义的标识符来进行区分,从而允许为特定的用户启用新版本。当 API 网关接收到请求之后,可以根据当前的用户标识符来判断是否应该启用新版本,如果启用新版本,则在请求中添加自定义的 HTTP 头。服务网格根据该 HTTP 头来选择路由。

在下面代码的虚拟服务中,我们定义了两个路由。第一个路由使用自定义 HTTP 头 x-latest-version 来匹配,把请求发送到绿色部署对应的服务子集;第二个路由则默认把请求发送到蓝色部署对应的服务子集。

yaml
apiVersion: networking.istio.io/v1beta1 
kind: VirtualService 
metadata: 
  name: address-service-deployment 
spec: 
  hosts: 
    - address-service.happyride.svc.cluster.local 
  http: 
    - name: "latest" 
      match: 
        - headers: 
            x-latest-version: 
              exact: "true" 
      route: 
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: green 
    - name: "current" 
      route:
        - destination: 
            host: address-service.happyride.svc.cluster.local 
            subset: blue

源代码管理

在实现灰度发布中的一个重要问题是如何与源代码管理系统进行集成。在两个版本同时部署和运行时,首先要做出的选择是,当新版本已经部分部署之后,是否还需要对当前版本进行修改,这个选择会确定后续的策略。

有些公司使用的是基于主干的开发方式(Trunk Based Development),也就是只有一个作为主干的源代码分支,所有开发都在这个分支上进行。在进行部署时,只需要从主分支中选择一个 Git 提交作为要部署的版本即可。在部署完成之后,代码的修改在主分支中进行。在下一次部署时,选择另外一个部署环境。

在下图中,圆圈表示 Git 提交,虚线表示把 Git 提交对应的代码部署到环境上。蓝色和绿色环境的部署交替进行。

当需要开发一个较大的新功能时,所花费的时间可能很长。在新功能的开发过程中,仍然需要对当前的版本进行 bug 修复。这种情况下,基于主干的开发方式的管理会变得复杂,可以考虑使用分支。

新旧版本有各自的 Git 分支,当前版本的代码使用主分支,当需要开发新版本时,从主分支创建新的分支来进行开发,两个分支都有各自的持续集成和部署流程。在新版本部署之后,仍然需要对旧版本进行 bug 修复,新版本也需要根据用户的反馈进行修改。当新版本更新完成之后,其分支被合并到主分支,准备下一个版本的开发。

在下图中,当需要开发新功能时,从主分支中创建一个新分支,并部署到蓝色环境。与此同时,主分支的开发仍然在进行中,并部署到绿色环境。不过在主分支中所做的改动只限于严重 bug 的修复,大部分的开发仍然在新分支中进行。在主分支中所做的修改,需要被定期合并到新分支中,这样就确保了新分支中包含了全部相关的改动。当新分支开发完成,并合并到主分支之后,新分支的部署环境会变成当前的生产环境。

新功能分支的版本号可以与主分支保持一致,也可以根据语义化版本的规范来更新版本号,是否更新版本号取决于改动的大小。新版本的分支都有各自的持续集成流程。由于持续集成所创建的容器镜像的标签使用 Git 提交的标识符作为后缀,因此不更新版本号也不会产生冲突。

总结

通过使用灰度发布,我们可以更加安全地对应用进行更新,不但可以进行更多的测试,当出现问题时还可以方便地回退部署。通过本课时的学习,你可以了解灰度发布相关的基本概念,还可以了解如何通过服务网格来实现,最后了解与灰度发布相对应的源代码管理策略。

最后呢,成老师邀请你为本专栏课程进行结课评价,因为你的每一个观点都是我们最关注的点。点击链接,即可参与课程评价