使用 Argo Rollouts 和服务网格实现自动可控的金丝雀发布

使用 Argo Rollouts 和服务网格实现自动可控的金丝雀发布

金丝雀发布是服务治理中的重要功能,在发布时可以可控地将部分流量导入新版本的服务中;其余的流量则由旧版本处理。发布过程中,可以逐步增加新版本服务的流量。通过测试,可以决定是回滚还是升级所有实例,停用旧版本。

有了金丝雀发布,使用真实流量对服务进行测试,通过对流量的控制可以有效的降低服务发布的风险。

本文将介绍如何将使用 Argo Rollouts 和服务网格 osm-edge 来进行应用的自动、可控的金丝雀发布,不涉及工作原理的分析。对工作原理有兴趣的同学可以留言,可以再做一篇原理的介绍。

Argo Rollouts

Argo Rollouts 包括一个 Kubernetes控制器 和一组 CRD,提供如蓝绿色、金丝雀、金丝雀分析、体验等高级部署功能和 Kubernetes 的渐进交付功能。

Argo Rollouts 与 入口控制器 和服务网格集成,利用其流量管理能力,在发布期间逐步将流量转移到新版本。此外,Rollouts 可以查询和解析来自各种提供商的指标,以验证关键 KPI,并在更新期间推动自动推进或回滚。

Argo Rollouts 支持服务网格标准 SMI(Service Mesh Interface)TrafficSplit API,通过 TrafficSplit API 的使用来控制金丝雀发布时的流量控制。

服务网格 osm-edge

服务网格是处理服务间网络通信的基础设施组件,旨在从平台层面提供可观性、安全以及可靠性特性,以进程外的方式提供原本由部分应用层逻辑承载的基础能力,真正实现与业务逻辑的分离。

osm-edge 是面向云边一体的轻量化服务网格,采用实现了服务网格标准 SMI(Service Mesh Interface) 的 osm(Open Service Mesh) 作为控制平面,采用 Pipy 作为数据平面,采用 fsm 项目提供的 Ingress、Egress、Gateway API 多集群服务网格能力,具有高性能、低资源、简单、易用、易扩展、广泛兼容(支持 x86/arm64/龙芯/RISC-V)的特点。

环境准备

K3s

export INSTALL_K3S_VERSION=v1.23.8+k3s2
curl -sfL https://get.k3s.io | sh -s - --disable traefik --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config

安装服务网格

推荐使用 CLI 进行安装。

system=$(uname -s | tr [:upper:] [:lower:])
arch=$(dpkg --print-architecture)
release=v1.1.2
curl -L https://github.com/flomesh-io/osm-edge/releases/download/${release}/osm-edge-${release}-${system}-${arch}.tar.gz | tar -vxzf -
./${system}-${arch}/osm version
cp ./${system}-${arch}/osm /usr/local/bin/

CLI 下载之后就可以通过下面的命令进行安装了,这里默认开启 宽松流量策略模式 并安装 Ingress

export osm_namespace=osm-system 
export osm_mesh_name=osm 

osm install \
    --mesh-name "$osm_mesh_name" \
    --osm-namespace "$osm_namespace" \
    --set=osm.enablePermissiveTrafficPolicy=true \
    --set=fsm.enabled=true

确认 pod 正常启动并运行。

kubectl get pods -n osm-system
NAME                                           READY   STATUS    RESTARTS   AGE
fsm-repo-d785b55d-r92r5                        1/1     Running   0          2m20s
osm-bootstrap-646497898f-cdjq4                 1/1     Running   0          3m12s
osm-controller-7bbdcf748b-jdhsw                2/2     Running   0          3m12s
fsm-manager-7f9b665bd9-s8z6p                   1/1     Running   0          3m12s
fsm-bootstrap-57cb75d586-vvvzl                 1/1     Running   0          3m01s
osm-injector-86798c9ddb-gfqb4                  1/1     Running   0          3m12s
fsm-ingress-pipy-5bc7f4d6f6-7th6g              1/1     Running   0          3m12s
fsm-cluster-connector-local-7464b77ffd-cphxf   1/1     Running   0          4m22s

安装 Argo Rollouts

kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

Argo Rollouts 中默认使用 SMI TrafficSplit 的 v1alpha1 版本,通过下面的命令指定其使用 v1alpha2 的版本。

kubectl patch deployment argo-rollouts -n argo-rollouts --type=json -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args", "value": ["--traffic-split-api-version=v1alpha2"]}]'

确认 pod 正常启动并运行。

kubectl get pods -n argo-rollouts
NAME                             READY   STATUS    RESTARTS   AGE
argo-rollouts-58b9bc98f7-cj79l   1/1     Running   0          1m5s

安装 kubectl argo 插件

使用 kubectl argo 插件可以通过命令行对发布进行操作。

在 macOS 下,其他平台参考 官方安装文档

brew install argoproj/tap/kubectl-argo-rollouts

通过下面的命令启动 Argo Rollouts Dashboard,在浏览器中打开 http://localhost:3100/rollouts 就可访问 Dashboard。

kubectl argo rollouts dashboard

接下来就是部署示例应用。

部署应用

示例应用使用常见的 bookstore 系统,包含下面几个组件。这里只对 bookstore 应用进行金丝雀发布,为了方便演示,除了 bookstore 以外的几个应用继续使用 Deployment 的方式部署,只有 bookstore 使用 Argo Rollouts 的 Rollout CRD

  • bookbuyer 是一个 HTTP 客户端,它发送请求给 bookstore。这个流量是 允许 的。
  • bookthief 是一个 HTTP 客户端,很像 bookbuyer,也会发送 HTTP 请求给 bookstore。这个流量应该被 阻止
  • bookstore 是一个服务器,负责对 HTTP 请求给与响应。同时,该服务器也是一个客户端,发送请求给 bookwarehouse 服务。这个流量是被 允许 的。
  • bookwarehouse 是一个服务器,应该只对 bookstore 做出响应。bookbuyer 和 bookthief 都应该被其阻止。
  • mysql 是一个 MySQL 数据库,只有 bookwarehouse 可以访问。

这里舍弃了 bookthief 应用。

创建命名空间

创建命名空间 rollouts-demo ,并纳入到网格管理中。

kubectl create namespace rollouts-demo
kubectl config set-context --current --namespace rollouts-demo
osm namespace add rollouts-demo

创建 Service

kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: bookbuyer
  namespace: rollouts-demo
  labels:
    app: bookbuyer
spec:
  ports:
  - port: 14001
    name: bookbuyer-port
  selector:
    app: bookbuyer
---
apiVersion: v1
kind: Service
metadata:
  name: bookstore
  namespace: rollouts-demo
  labels:
    app: bookstore
spec:
  ports:
  - port: 14001
    name: bookstore-port
  selector:
    app: bookstore
---
apiVersion: v1
kind: Service
metadata:
  name: bookstore-v1
  namespace: rollouts-demo
  labels:
    app: bookstore
    version: v1
spec:
  ports:
  - port: 14001
    name: bookstore-port
  selector:
    app: bookstore
---
apiVersion: v1
kind: Service
metadata:
  name: bookstore-v2
  namespace: rollouts-demo
  labels:
    app: bookstore
    version: v2
spec:
  ports:
  - port: 14001
    name: bookstore-port
  selector:
    app: bookstore
---
apiVersion: v1
kind: Service
metadata:
  name: bookwarehouse
  namespace: rollouts-demo
  labels:
    app: bookwarehouse
spec:
  ports:
  - port: 14001
    name: bookwarehouse-port
  selector:
    app: bookwarehouse
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: rollouts-demo
spec:
  ports:
  - port: 3306
    targetPort: 3306
    name: client
    appProtocol: tcp
  selector:
    app: mysql
  clusterIP: None
EOF

部署 Deployment 应用

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bookbuyer
  namespace: rollouts-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bookbuyer
      version: v1
  template:
    metadata:
      labels:
        app: bookbuyer
        version: v1
    spec:
      nodeSelector:
        kubernetes.io/os: linux
      containers:
        - name: bookbuyer
          image: flomesh/bookbuyer:latest
          imagePullPolicy: Always
          command: ["/bookbuyer"]
          env:
            - name: "BOOKSTORE_NAMESPACE"
              value: rollouts-demo
            - name: "BOOKSTORE_SVC"
              value: bookstore
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bookwarehouse
  namespace: rollouts-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bookwarehouse
  template:
    metadata:
      labels:
        app: bookwarehouse
        version: v1
    spec:
      nodeSelector:
        kubernetes.io/os: linux
      containers:
        - name: bookwarehouse
          image: flomesh/bookwarehouse:latest
          imagePullPolicy: Always
          command: ["/bookwarehouse"]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: rollouts-demo
spec:
  serviceName: mysql
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      nodeSelector:
        kubernetes.io/os: linux
      containers:
      - image: mariadb:10.7.4
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: mypassword
        - name: MYSQL_DATABASE
          value: booksdemo
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - mountPath: /mysql-data
          name: data
        readinessProbe:
          tcpSocket:
            port: 3306
          initialDelaySeconds: 15
          periodSeconds: 10
      volumes:
        - name: data
          emptyDir: {}
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 250M
EOF

部署 bookstore Rollout

我们为 bookstore 的金丝雀发布设置了三个阶段:10%、50%、90% 流量,每个阶段后都会进入暂停状态(也可以设置暂停时间、条件等等)。

前面创建 Service 时,我们为 bookstore 创建了如下三个服务,这里正好会用到:

  • 根服务 bookstore
  • 稳定版服务 bookstore-v1
  • 金丝雀版本 bookstore-v2
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: bookstore
  namespace: rollouts-demo
spec:
  replicas: 1
  strategy:
    canary:
      canaryService: bookstore-v2
      stableService: bookstore-v1
      trafficRouting:
        smi:
          rootService: bookstore
      steps:
      - setWeight: 10
      - pause: {}
      - setWeight: 50
      - pause: {}
      - setWeight: 90
      - pause: {}
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: bookstore
  template:
    metadata:
      labels:
        app: bookstore
    spec:
      nodeSelector:
        kubernetes.io/os: linux
      containers:
        - name: bookstore
          image: addozhang/bookstore-v1:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 14001
              name: web
          command: ["/bookstore"]
          args: ["--port", "14001"]
          env:
            - name: BOOKWAREHOUSE_NAMESPACE
              value: rollouts-demo
EOF

部署完成后,此时 bookstore-v1 的版本会运行,并不会部署新的版本,此时三个 Service 都会选择到稳定版本的 pod。

kubectl get endpoints -n rollouts-demo -l app=bookstore
NAME           ENDPOINTS                           AGE
bookstore-v1   10.42.1.34:14001                    54s
bookstore      10.42.1.34:14001                    54s
bookstore-v2   10.42.1.34:14001                    54s

查看 TrafficSplit 的设定,访问 bookstore 的所有流量都会进入 bookstore-v1 的 endpoint 中:

kubectl get trafficsplit bookstore -n rollouts-demo -o yaml
apiVersion: split.smi-spec.io/v1alpha2
kind: TrafficSplit
metadata:
  name: bookstore
  namespace: rollouts-demo
spec:
  backends:
  - service: bookstore-v2
    weight: 0
  - service: bookstore-v1
    weight: 100
  service: bookstore

查看 Argo Rollouts Dashboard,可以看到刚刚创建的 Rollout

init-rollout

点开可以看到详细信息,包括当前的 revision,已经我们设置的发布步骤。

rollout-dashboard-rollout-detail

创建 bookbuyer Ingress

为了方便查看运行效果,为 bookbuyer 创建 Ingress。然后就可以通过 http://ingress_host:80 来访问 bookbuyer 应用了,也可以通过 http://ingress_host:80/reset 来重置应用页面的计数器,方便灰度发布的效果确认。

kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: bookbuyer
  namespace: rollouts-demo
spec:
  ingressClassName: pipy
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: bookbuyer
            port:
              number: 14001
---
kind: IngressBackend
apiVersion: policy.openservicemesh.io/v1alpha1
metadata:
  name: bookbuyer
  namespace: rollouts-demo
spec:
  backends:
  - name: bookbuyer
    port:
      number: 14001
      protocol: http
  sources:
  - kind: Service
    namespace: osm-system
    name: fsm-ingress-pipy-controller
EOF

发布测试

笔者的 ingress IP 是 192.168.1.12,因此在浏览器中打开 http://192.168.1.12。此时,所有的流量都到了 bookstore-v1

bookbuyer

执行应用升级

通过下面的命令,部署 bookstore v2 版本开始金丝雀发布。除了命令行,也可以在 Dashboard 的发布详情页面上修改容器的镜像。

kubectl argo rollouts set image bookstore bookstore=addozhang/bookstore-v2:latest

然后通过下面的命令可以查看稳定版和金丝雀版本的实例状态。

kubectl argo rollouts get rollout bookstore

argo-rollout-started

重置 bookstore 应用页面的计数器后,可以发现 v1 和 v2 流量接近 9:1。

argo-rollout-in-progress

Dashboard 的发布详情页上,会显示 v2 版本的 revision,右侧 steps 列表中可以看到当前处于第一个 pause 阶段。

argo-dashboard-rollout-in-progress

推进到 50% 流量

验证完成后,就可以将发布推进到下一阶段。可以使用下面的命令,也可在 Dashboard 详情页上点击 PROMOTE 按钮。

kubectl argo rollouts promote bookstore
rollout 'bookstore' promoted

还是先清空应用页面计数器,然后查看效果。

argo-rollout-promote-50-percent

argo-dashboard-rollout-promote-50-percent

推进到 100% 流量

接下来,可以继续推荐到下一阶段 90%。也可以通过下面的命令直接进入到最后的阶段 100%,在 Dashboard 详情页上点击 PROMOTE-FULL 也可达到同样的效果。

kubectl argo rollouts promote bookstore --full
rollout 'bookstore' fully promoted

argo-dashboard-rollout-promote-100-percent

其他操作

在发布的任何一个阶段,可以通过命令或者页面上的操作回到上一个阶段,或者退出发布。

#回到上一阶段
kubectl argo rollouts undo bookstore
#退出发布
kubectl argo rollouts abort bookstore

总结

服务网格以进程外无侵入的方式为服务提供了丰富的治理功能,从平台层面提升系统的可观测性、安全以及可靠性。金丝雀发布是服务网格的主要应用场景之一,大大减低了应用发布带来的风险,将不稳定性限制在可控的范围内。

同时 Argo Rollouts 也提供了丰富的设置,来控制发布的流程,满足不同的使用需求。

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus