首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
kubernetes与velero的第一次尝试
2
在Kubernetes中如何针对Namespace进行资源限制?
3
kubernetes之metrics-server安装与配置
4
kubernetes部署metrics-server
5
Kubernetes1.20.9摘掉一个master节点再重新加入(ETCD需要注意的)
6
Kubernetes 1.17.17升级到1.18.20
7
Kubernetes 1.18.20升级到1.19.12
8
Kubernetes 1.19.12升级到1.20.9(强调一下selfLink)
9
Kubernetes 1.16.15升级到1.17.17
10
使用 kainstall 工具一键部署 kubernetes 高可用集群
11
附034.Kubernetes_v1.21.0高可用部署架构二
12
附016.Kubernetes_v1.17.4高可用部署
13
附022.Kubernetes_v1.18.3高可用部署架构一
14
附024.Kubernetes_v1.18.3高可用部署架构二
15
使用 StatefulSet 部署 etcd 集群
16
Kubernetes 稳定性保障手册 -- 极简版
17
Linux(centos7)离现安装kubernetes1.19.2和docker——组件部分
18
docker register 私有仓库部署 - http模式
19
KubeSphere 开源 KubeEye:Kubernetes 集群自动巡检工具
20
K8S 中的 CPUThrottlingHigh 到底是个什么鬼?
21
全链路分布式跟踪系统 Apache SkyWalking 入门教程
22
pod Evicted的状态究竟是何人所为
23
使用 ezctl 工具部署和管理 Kubernetes 集群
24
Kubernetes部署策略详解
25
kubernetes容器探针检测
26
使用Spring Boot实现动态健康检查HealthChecks
27
真一文搞定 ingress-nginx 的使用
28
K8S备份、恢复、迁移神器 Velero
29
一次关于k8s kubectl top 和 contained ps 不一致的问题探究
30
kubernetes备份恢复之velero
31
使用 Velero 进行集群备份与迁移
32
TKE集群中nginx-ingress使用实践
33
使用velero进行kubernetes灾备
34
Kubernetes 映射外部服务
35
运维体系建设套路
36
k8s解决pod调度不均衡的问题
37
ingress中虚拟路径解决方案
38
容器下的两地三中心建设
39
k8s集群外的主机访问pod的解决方案
40
k8s基础-健康检查机制
41
k8s基础-标签使用
42
ingress-nginx请求改写
43
nginx ingress server alias 多域名多证书问题
44
JAVA | Java 解决跨域问题 花式解决跨域问题
45
如何通过ingress-nginx实现应用灰度发布?
46
在Kubernetes(k8s)中使用GPU
47
使用 Prometheus-Operator 监控 Calico
48
使用Kubespray部署Kubernetes集群
49
云原生下的CI/CD:Argo CD 详解,手把手教你入门
50
Pod的健康检查机制
清单首页k8s文章详情

如何通过ingress-nginx实现应用灰度发布?

大家好,我是乔克。

在日常的工作中,我们会经常对应用进行发版升级,在互联网公司尤为频繁,主要是为了满足快速的业务发展。我们经常用到的发布方式有滚动更新、蓝绿发布、灰度发布。

  • 滚动更新:依次进行新旧替换,直到旧的全部被替换为止。
  • 蓝绿发布:两套独立的系统,对外提供服务的称为绿系统,待上线的服务称为蓝系统,当蓝系统里面的应用测试完成后,用户流量接入蓝系统,蓝系统将称为绿系统,以前的绿系统就可以销毁。
  • 灰度发布:在一套集群中存在稳定和灰度两个版本,灰度版本可以限制只针对部分人员可用,待灰度版本测试完成后,可以将灰度版本升级为稳定版本,旧的稳定版本就可以下线了,我们也称之为金丝雀发布。

这里主要给大家分享如果通过ingress-nginx controller实现灰度发布。

本文大纲如下。

如何通过ingress-nginx实现灰度发布

ingress-nginx是Kubernetes官方推荐的ingress controller,它是基于nginx实现的,增加了一组用于实现额外功能的Lua插件。

为了实现灰度发布,ingress-nginx通过定义annotation来实现不同场景的灰度发布,其支持的规则如下:

  • nginx.ingress.kubernetes.io/canary-by-header:基于 Request Header 的流量切分,适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always时,请求将会被一直发送到 Canary 版本;当 Request Header 设置为 never时,请求不会被发送到 Canary 入口;对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他金丝雀规则进行优先级的比较。
  • nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的 Request Header 的值,用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时,它将被路由到 Canary 入口。该规则允许用户自定义 Request Header 的值,必须与上一个 annotation (即:canary-by-header)一起使用。
  • nginx.ingress.kubernetes.io/canary-weight:基于服务权重的流量切分,适用于蓝绿部署,权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求。权重为 100 意味着所有请求都将被发送到 Canary 入口。
  • nginx.ingress.kubernetes.io/canary-by-cookie:基于 Cookie 的流量切分,适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的cookie。当 cookie 值设置为 always时,它将被路由到 Canary 入口;当 cookie 值设置为 never时,请求不会被发送到 Canary 入口;对于任何其他值,将忽略 cookie 并将请求与其他金丝雀规则进行优先级的比较。

我们也是通过上面的annotation来实现灰度发布,其思路如下:

  1. 在集群中部署两套系统,一套是stable版本,一套是canary版本,两个版本都有自己的service
  2. 定义两个ingress配置,一个正常提供服务,一个增加canary的annotation
  3. 待canary版本无误后,将其切换成stable版本,并且将旧的版本下线,流量全部接入新的stable版本

发布场景介绍

上面介绍了ingress-nginx实现灰度发布的方法以及咱们自己的实现思路,这里来探讨一下灰度发布有哪些发布场景。

基于权重的发布场景

假如在生产上已经运行了A应用对外提供服务,此时开发修复了一些Bug,需要发布A2版本将其上线,但是我们又不希望直接的将所有流量接入到新的A2版本,而是希望将10%的流量进入到A2中,待A2稳定后,才会将所有流量接入进来,再下线原来的A版本。

image.png

要实现这种,只需要在canary的ingress中添加如下annotation。

代码语言:javascript
复制
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"

其中nginx.ingress.kubernetes.io/canary表示开启canary,nginx.ingress.kubernetes.io/canary-weight表示我们设置的权重大小。

基于用户请求的发布场景

基于权重的发布场景比较粗糙,它是所有用户中的20%,无法限制具体的用户。

我们有时候会有这样的需求,比如我们有广东、北京、四川这三个地区的用户,并且已经有A版本的应用为这三个地区提供服务,由于更新了需求,我们需要发布A2应用,但是我们不想所有地区都访问A2应用,而是希望只有四川的用户可以访问,待四川地区反馈没问题后,才开放其他地区。

image.png

对于这种我们需要在canary的ingress中添加如下annotation。

代码语言:javascript
复制
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "region"
nginx.ingress.kubernetes.io/canary-by-header-value: "sichuan"

主要就是上面两种发布场景,下面会针对这两种场景分别进行实验。

灰度发布具体实现

我这里准备了两个镜像,一个是稳定stable版本,一个是灰度canary版本。

  • registry.cn-hangzhou.aliyuncs.com/rookieops/go-test:v1
  • registry.cn-hangzhou.aliyuncs.com/rookieops/go-test:v2

由于两个场景只有在ingress处的配置不一致,其他都一样的,所以这里先将两个版本的应用都部署好。

(1)stable版本

代码语言:javascript
复制
apiVersion: apps/v1 
kind: Deployment
metadata:
  name: app-server-stable
spec:
  selector:
    matchLabels:
      app: go-test
      version: stable
  replicas: 1
  template:
    metadata:
      labels:
        app: go-test
        version: stable
    spec:
      containers:
      - name: app-server
        image: registry.cn-hangzhou.aliyuncs.com/rookieops/go-test:v1
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: app-server-stable-svc
spec:
  selector:
    app: go-test
    version: stable
  ports:
  - name: http
    port: 8080

访问效果如下:

代码语言:javascript
复制
# curl 10.97.112.137:8080
{"data":"hello world","version":"v1"}

(2)canary版本

代码语言:javascript
复制
apiVersion: apps/v1 
kind: Deployment
metadata:
  name: app-server-canary
spec:
  selector:
    matchLabels:
      app: go-test
      version: canary
  replicas: 1
  template:
    metadata:
      labels:
        app: go-test
        version: canary
    spec:
      containers:
      - name: app-server
        image: registry.cn-hangzhou.aliyuncs.com/rookieops/go-test:v2
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: app-server-canary-svc
spec:
  selector:
    app: go-test
    version: canary
  ports:
  - name: http
    port: 8080

访问效果如下:

代码语言:javascript
复制
# curl 10.110.178.174:8080
{"data":"hello SB","version":"v2"}

上面已经将应用部署好了,下面将针对权重和用户请求两个场景进行测试。

基于权重的发布场景

(1)配置stable版本的ingress

代码语言:javascript
复制
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: app-server-stable-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: joker.coolops.cn
    http:
      paths:
      - path:
        backend:
          serviceName: app-server-stable-svc
          servicePort: 8080

(2)配置canary版本的ingress

代码语言:javascript
复制
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: app-server-canary-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: joker.coolops.cn
    http:
      paths:
      - path:
        backend:
          serviceName: app-server-canary-svc
          servicePort: 8080

然后我们通过访问测试,效果如下:

代码语言:javascript
复制
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello SB","version":"v2"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}

基本保持在9:1的比例。

基于用户请求的发布场景

(1)配置stable版本的ingress

代码语言:javascript
复制
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: app-server-stable-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: joker.coolops.cn
    http:
      paths:
      - path:
        backend:
          serviceName: app-server-stable-svc
          servicePort: 8080

(2)配置canary版本的ingress

代码语言:javascript
复制
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: app-server-canary-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "region"
    nginx.ingress.kubernetes.io/canary-by-header-value: "sichuan"
spec:
  rules:
  - host: joker.coolops.cn
    http:
      paths:
      - path:
        backend:
          serviceName: app-server-canary-svc
          servicePort: 8080

当我们访问的时候不带header,则只会访问stable版本应用,如下:

代码语言:javascript
复制
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}
# curl joker.coolops.cn
{"data":"hello world","version":"v1"}

如果我们在访问的时候带上region: sichuan 的header,则只会访问到canary版本应用,如下:

代码语言:javascript
复制
# curl joker.coolops.cn -H "region: sichuan"
{"data":"hello SB","version":"v2"}
# curl joker.coolops.cn -H "region: sichuan"
{"data":"hello SB","version":"v2"}

实现是不是很简单?

我们现在来想另外一个问题,上面的所有操作都是手动的,我们应该如何进行自动化?应该怎样来设计流水线?

下面来说说我个人的想法。

关于灰度发布流水线设计的想法

首先来捋捋过程:

  1. 发布canary版本应用进行测试
  2. 测试完成将canary版本替换成stable版本
  3. 删除canary版本的ingress配置
  4. 删除老的stable版本

整个过程很简单,但是对于已经部署好的deployment是不允许直接修改labels标签的。这时候是不是可以在canary版本测试i完成后直接更新stable版本的镜像?当然这种情况会存在滚动更新的一个过程。

那我们流水线可以这样设计,如下:

这样设计存在一个问题,那就是无法确定等待的时间,如果等待的时间很长,不仅很耗资源,也可能自动超时退出。

那我们是不是可以将其拆分为两条流水线?流程如下:

我比较倾向第二种,这种方式流水线跑完了就退出,不会占用额外的资源。

在开发流水线之前,我们需要先定义好命名标准,这样在操作的时候更加方便。

  1. 流水线名字格式如下:
    1. <APP_NAME>-stable
    2. <APP_NAME>-canary
  2. deployment的名字格式如下:
    1. <APP_NAME>-stable
    2. <APP_NAME>-canary
  3. service的名字格式如下:
    1. <APP_NAME>-stable-svc
    2. <APP_NAME>-canary-svc
  4. ingress的名字格式如下:
    1. <APP_NAME>-stable-ingress
    2. <APP_NAME>-canary-ingress

标准定义好之后,在实现的时候就简单多了。

代码位置:https://gitee.com/coolops/gary-devops.git

我定义了两个Jenkinsfile,一个叫canary.Jenkinsfile,一个叫stable.Jenkinsfile,他们分别用来部署canary和stable版本。

然后我们会创建两条流水线,如下:

其中joker-gary-devops-canary是用来部署canary版本,另外一个是用来部署stable版本。

现在在集群里运行着stable版本,如下:

代码语言:javascript
复制
# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}

我们修改了需求,更改了代码,变动如下:

代码语言:javascript
复制
package main

import (
 "net/http"

 "github.com/gin-gonic/gin"
)

func main() {
 g := gin.Default()
 g.GET("/", func(c *gin.Context) {
  c.JSON(http.StatusOK, gin.H{
   "version": "v1",
   "data":    "hello Joker!",
  })

 })
 _ = g.Run(":8080")
}

首先发布canary流水线,待流水线发布完成,可以在集群中看到canary版本的pod以及ingress等,如下:

代码语言:javascript
复制
# kubectl get po| grep canary
gray-devops-canary-59c88846dc-j2vlc   1/1     Running   0          55s
# kubectl get svc| grep canary
gray-devops-canary-svc   ClusterIP   10.233.18.235   <none>        8080/TCP            3h14m
# kubectl get ingress| grep canary
gray-devops-canary-ingress   joker.coolops.cn                               192.168.100.61   80        63s

查看一下canary-ingress的内容,看是否是我们需要的,如下:

代码语言:javascript
复制
# kubectl get ingress gray-devops-canary-ingress -o yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
  creationTimestamp: "2022-02-15T05:43:32Z"
  generation: 1
  name: gray-devops-canary-ingress
  namespace: default
  resourceVersion: "412247041"
  selfLink: /apis/extensions/v1beta1/namespaces/default/ingresses/gray-devops-canary-ingress
  uid: fe13b38d-1f6f-45fb-8d89-504b4b8288ea
spec:
  rules:
  - host: joker.coolops.cn
    http:
      paths:
      - backend:
          serviceName: gray-devops-canary-svc
          servicePort: 8080
status:
  loadBalancer:
    ingress:
    - ip: 192.168.100.61

可以发现跟我们预设的一样。

访问测试也没问题,如下:

代码语言:javascript
复制
# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello Joker!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello Joker!","version":"v1"}[root@master ~]# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello world!","version":"v1"}

现在就可以发布stable版本了,运行stable版本的流水线。发布完成后,集群里就只有stable版本的应用了,如下:

代码语言:javascript
复制
# kubectl get po | grep gray
gray-devops-stable-7f977bb6cf-8jzgt   1/1     Running   0          35s
# kubectl get ingress | grep gray
gray-devops-stable-ingress   joker.coolops.cn                               192.168.100.61   80        111m

通过域名访问也是符合预期的。

代码语言:javascript
复制
# curl -H "Host: joker.coolops.cn" http://192.168.100.61
{"data":"hello Joker!","version":"v1"}

到此基本实现了自己的想法。

说明:Jenkinsfile中涉及的用户名和密码都保存在Jenkins的凭据中,插件需要安装kubernetes deploy插件,到插件中心搜索就行。

最后

上面我们基本实现了灰度发布的过程,也只是仅仅将手动的变成了自动。但是你有没有发现什么问题?

首先需要切换流水线进行发布,其次是发布控制方面也不是很友好,比如要增加canary版本的节点,就需要我们手动去做。

其实我更推荐使用argo-rollouts结合argocd进行灰度发布,argo-rollouts自定义了一套CRD用于控制发布流程,可以省去很多手动操作过程,argocd是基于gitops实现的一套软件,便于我们进行CD控制,也提供了UI面板进行操作。不过要用这套就需要更改现有的发布方式以及应用模板,不复杂,但是存在一定的风险,需要进行一定程度的测试。

下一篇
举报
领券