使用 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。

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

创建 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。

执行应用升级
通过下面的命令,部署 bookstore v2 版本开始金丝雀发布。除了命令行,也可以在 Dashboard 的发布详情页面上修改容器的镜像。
kubectl argo rollouts set image bookstore bookstore=addozhang/bookstore-v2:latest
然后通过下面的命令可以查看稳定版和金丝雀版本的实例状态。
kubectl argo rollouts get rollout bookstore

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

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

推进到 50% 流量
验证完成后,就可以将发布推进到下一阶段。可以使用下面的命令,也可在 Dashboard 详情页上点击 PROMOTE 按钮。
kubectl argo rollouts promote bookstore
rollout 'bookstore' promoted
还是先清空应用页面计数器,然后查看效果。


推进到 100% 流量
接下来,可以继续推荐到下一阶段 90%。也可以通过下面的命令直接进入到最后的阶段 100%,在 Dashboard 详情页上点击 PROMOTE-FULL 也可达到同样的效果。
kubectl argo rollouts promote bookstore --full
rollout 'bookstore' fully promoted

其他操作
在发布的任何一个阶段,可以通过命令或者页面上的操作回到上一个阶段,或者退出发布。
#回到上一阶段
kubectl argo rollouts undo bookstore
#退出发布
kubectl argo rollouts abort bookstore
总结
服务网格以进程外无侵入的方式为服务提供了丰富的治理功能,从平台层面提升系统的可观测性、安全以及可靠性。金丝雀发布是服务网格的主要应用场景之一,大大减低了应用发布带来的风险,将不稳定性限制在可控的范围内。
同时 Argo Rollouts 也提供了丰富的设置,来控制发布的流程,满足不同的使用需求。