使用 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 也提供了丰富的设置,来控制发布的流程,满足不同的使用需求。