背景
灰度发布又叫A/B测试,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
因为最近刚好有灰度发布的需求,我又学了一遍istio,记录了本次灰度发布的实施过程(只包括应用,不包括数据库升级)
实验过程
- 先确定目前的应用版本为V1
- 通过helm包部署应用版本为V2的pod到K8S集群中
- 确定V2版本灰度的用户,方法包括IP,或者特定用户
- 通过istio的virtualservice功能把特定用户的流量指向V2版本
- 检查特定用户使用一段时间后,是否出现问题
- 若无问题,通过istio将所有用户的流量都指向V2版本
- 若所有用户都使用V2无问题,删除掉V1版本的pod
示例介绍
前端应用frontend,后端应用mqtt-server,后端应用mqtt-server 通过mqtt协议与设备相连接。
前端部署3个版本,分别是V1,V2,V3,后端同样部署3个版本,也是V1,V2,V3。3个前端版本,按钮文字不一样。3个后端版本,连接的mqtt设备不一样
版本 | 前端页面 | 后端返回参数 |
V1 | 显示V11按钮 | {"message":["wsytest010","wsytest002", "wsytest003","wsytest007","wsytest006", "wsytest001","wsytest005","wsytest009", "wsytest008","wsytest004"]} |
V2 | 显示V22按钮 | {"message":["wsytest019","wsytest020", "wsytest017","wsytest012","wsytest011", "wsytest014","wsytest018","wsytest015", "wsytest013","wsytest016"]} |
V3 | 显示V33按钮 | {"message":["wsytest024","wsytest028", "wsytest022","wsytest026","wsytest027", "wsytest021","wsytest025","wsytest030", "wsytest023","wsytest029"]} |
根据需求,版本不能串,比如前端V1->后端V1,不允许出现前端V1→后端V2这样的情况发生
这里我们在选择分配流量方式时,不能使用权重的方式进行分配,只能选择指定用户或者指定IP,如果选择权重的方式,可能会出现如下的问题:
前端会访问多个js,css等文件,如果使用权重的方式,会出现一部分js来源于v1版本,一部分css来源于v2版本。
后端也同理,如果一个页面打开时,触发多个后端请求,部分来源于V2,部分来源于V1,肯定会导致前端显示出现问题。
所以只有把前后端通过某种方式一一对应,才能正常使用
代码实现与注意事项
1.部署前端的3个应用程序,所有的pod都加上 labels:[app:frontend,version:#{对应版本}]
--- apiVersion: apps/v1 kind: Deployment metadata: name: frontend labels: app: frontend version: v1 spec: replicas: 1 selector: matchLabels: app: frontend version: v1 template: metadata: labels: app: frontend version: v1 spec: containers: - name: frontend image: 前端镜像:v1 securityContext: capabilities: add: ["NET_ADMIN", "NET_RAW"] # 按照istio的说明,最好把这个pod安全策略加上 imagePullPolicy: Always ports: - containerPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: frontend-v2 labels: app: frontend version: v2 spec: replicas: 1 selector: matchLabels: app: frontend version: v2 template: metadata: labels: app: frontend version: v2 spec: containers: - name: frontend image: 前端镜像:v2 securityContext: capabilities: add: ["NET_ADMIN", "NET_RAW"] imagePullPolicy: Always ports: - containerPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: frontend-v3 labels: app: frontend version: v3 spec: replicas: 1 selector: matchLabels: app: frontend version: v3 template: metadata: labels: app: frontend version: v3 spec: containers: - name: frontend image: 前端镜像:v3 securityContext: capabilities: add: ["NET_ADMIN", "NET_RAW"] imagePullPolicy: Always ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: frontend spec: selector: app: frontend type: ClusterIP #这个不用NodePort,因为流量如果是从NodePort进来的,就控不住的 ports: - port: 80 targetPort: 80 name: http-web
2.部署后端应用程序,与前端应用类似
--- apiVersion: apps/v1 kind: Deployment metadata: name: mqtt-server-v1 labels: app: mqtt-server version: v1 spec: replicas: 1 selector: matchLabels: app: mqtt-server version: v1 template: metadata: labels: app: mqtt-server version: v1 spec: serviceAccountName: mqtt-server containers: - name: mqtt-server image: 后端镜像:latest securityContext: capabilities: add: ["NET_ADMIN", "NET_RAW"] # 按照istio的说明,最好把这个pod安全策略加上 imagePullPolicy: Always ports: - containerPort: 8000 --- apiVersion: apps/v1 kind: Deployment metadata: name: mqtt-server-v2 labels: app: mqtt-server version: v2 spec: replicas: 1 selector: matchLabels: app: mqtt-server version: v2 template: metadata: labels: app: mqtt-server version: v2 spec: serviceAccountName: mqtt-server containers: - name: mqtt-server image: 后端镜像:latest securityContext: capabilities: add: ["NET_ADMIN", "NET_RAW"] imagePullPolicy: Always ports: - containerPort: 8000 --- apiVersion: apps/v1 kind: Deployment metadata: name: mqtt-server-v3 labels: app: mqtt-server version: v3 spec: replicas: 1 selector: matchLabels: app: mqtt-server version: v3 template: metadata: labels: app: mqtt-server version: v3 spec: serviceAccountName: mqtt-server containers: - name: mqtt-server image: 后端镜像:latest securityContext: capabilities: add: ["NET_ADMIN", "NET_RAW"] imagePullPolicy: Always ports: - containerPort: 8000 --- apiVersion: v1 kind: Service metadata: name: mqtt-server spec: selector: app: mqtt-server type: NodePort #这个不用NodePort,因为流量如果是从NodePort进来的,就控不住的 ports: - port: 8000 targetPort: 8000 name: http-web
3.区分外部流量和内部流量。我们将浏览器到前端的称为外部流量,K8S里的例如前端到后端的称为内部流量4.外部流量出去,需要被istio的ingress gateway管控起来,所以需要配置一个gateway
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: bookinfo spec: selector: istio: ingressgateway # use istio default controller servers: - port: number: 80 name: http protocol: HTTP hosts: - "*"
5.配置后端的virtualservice和destination,确保后端程序能与前端程序产生一对一的关系,在无对应关系时,默认使用V1版本
--- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: mqtt-server-internal spec: hosts: - "mqtt-server" #此处是关键,把匹配到该url的流量,全部走到这个特定的virtualservice里 http: - match: - sourceLabels: version: v1 route: - destination: host: mqtt-server subset: v1 # 将匹配到的流量,转向subset的v1版本,这个subset: v1在destination.yaml里定义 headers: response: add: user: v1 - match: - sourceLabels: version: v2 route: - destination: host: mqtt-server subset: v2 headers: response: add: user: v2 - match: - sourceLabels: version: v3 route: - destination: host: mqtt-server subset: v3 headers: response: add: user: v3 - route: - destination: host: mqtt-server subset: v1 headers: response: add: user: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: mqtt-server spec: host: mqtt-server.default.svc.cluster.local subsets: - name: v1 labels: version: v1 # 根据pod的 version: v1 的label来进行匹配 - name: v2 labels: version: v2 - name: v3 labels: version: v3
6.配置前端的virtualservice和destination,我们可以设置来源于192.168.0.58这个IP的走V2版本,其余IP走V1版本
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: frontend-server spec: hosts: - "外网域名" #此处是关键,把匹配到该url的流量,全部走到这个特定的virtualservice里 gateways: - bookinfo-gateway #此处必须对应上gateway的名字 http: - match: - headers: X-Forwarded-For: exact: "192.168.0.58" #此处表示匹配header里有{"user":"v1"} route: - destination: host: mqtt-server subset: v2 # 将匹配到的流量,转向subset的v1版本,这个subset: v1在destination.yaml里定义 headers: response: add: user: v2 - route: - destination: host: frontend subset: v1 # 将匹配到的流量,转向subset的v1版本,这个subset: v1在destination.yaml里定义 headers: response: add: user: v1 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: frontend spec: host: frontend subsets: - name: v1 labels: version: v1 # 根据pod的 version: v1 的label来进行匹配 - name: v2 labels: version: v2 - name: v3 labels: version: v3
7.因为我们的浏览器访问的时候,会经过istio,所以前端收到的IP并不是真是的IP,我们需要修改istio的ingress文件,把spec.externalTrafficPolicy设置成Local,如下图所示8.最终情况
实验效果图
1.当本机IP地址不符合条件时,前端和后端都是V1版本的结果,第一张图是实际效果,第二张图是kiali显示的流量图2.当本机IP符合条件时,前端和后端都是V2版本的结果,左图是实际效果,右图是kiali显示的流量图3.当同时有满足IP和不满足IP条件的机器访问时,流量图效果如下