首页
学习
活动
专区
圈层
工具
发布
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文章详情

Kubernetes部署策略详解

1、简介

Kubernetes中有几种不同的方式发布应用,所以为了让应用在升级期间依然平稳提供服务,选择一个正确的发布策略就非常重要了。

选择正确的部署策略是要依赖于我们的业务需求的,下面我们列出了一些可能会使用到的策略:

  • 重建(recreate):停止旧版本部署新版本
  • 滚动更新(rolling-update):一个接一个地以滚动更新方式发布新版本
  • 蓝绿(blue/green):新版本与旧版本一起存在,然后切换流量
  • 金丝雀(canary):将新版本面向一部分用户发布,然后继续全量发布
  • A/B测(a/b testing):以精确的方式(HTTP 头、cookie、权重等)向部分用户发布新版本。A/B测实际上是一种基于数据统计做出业务决策的技术。在 Kubernetes 中并不原生支持,需要额外的一些高级组件来完成改设置(比如Istio、Linkerd、Traefik、或者自定义 Nginx/Haproxy 等)。

接下来我们来介绍下每种策略,看看在什么场景下面适合哪种策略。

2、重建(Recreate) - 最好在开发环境

2.1简介

重新创建策略是一个虚拟部署,包括关闭版本A,然后在关闭版本A后部署版本B. 此技术意味着服务的停机时间取决于应用程序的关闭和启动持续时间。

2.2策略定义为RecreateDeployment,会终止所有正在运行的实例,然后用较新的版本来重新创建它们。

代码语言:javascript
复制
 spec:
 replicas: 3
 strategy:
   type: Recreate

2.3我们这里创建两个相关的资源清单文件

代码语言:javascript
复制
 [root@yygh-de test]# vim app-v1.yaml
 apiVersion: v1
 kind: Service
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 type: NodePort
 ports:
  -name: http
   port: 80
   targetPort: http
 selector:
   app: my-app
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 replicas: 3
 selector:
   matchLabels:
     app: my-app
 strategy:
   type: Recreate
 selector:
   matchLabels:
     app: my-app
 template:
   metadata:
     labels:
       app: my-app
       version: v1.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v1.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5
代码语言:javascript
复制
 [root@yygh-de test]# vim app-v2.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 replicas: 3
 strategy:
   type: Recreate
 selector:
   matchLabels:
     app: my-app
 template:
   metadata:
     labels:
       app: my-app
       version: v2.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v2.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5

2.4上面两个资源清单文件中的 Deployment 定义几乎是一直的,唯一不同的是定义的环境变量VERSION值不同,接下来按照下面的步骤来验证Recreate策略:

版本1提供服务

  • 删除版本1
  • 部署版本2
  • 等待所有副本准备就绪

2.5首先部署第一个应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v1.yaml
 service/my-app created
 deployment.apps/my-app created
 [root@yygh-de test]# kubectl get pods -l app=my-app
 NAME                     READY   STATUS   RESTARTS   AGE
 my-app-5b4755fccf-2k8kj   1/1     Running   0        5m8s
 my-app-5b4755fccf-mwpvb   1/1     Running   0        5m8s
 my-app-5b4755fccf-qck84   1/1     Running   0        5m8s
 [root@yygh-de test]# kubectl get svc my-app
 NAME     TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)       AGE
 my-app   NodePort   10.96.247.241   <none>        80:32077/TCP   6m19s
 [root@yygh-de test]# curl http://127.0.0.1:32077
 Host: my-app-5b4755fccf-mwpvb, Version: v1.0.0

2.6可以看到版本1的应用正常运行了。为了查看部署的运行情况,打开一个新终端并运行以下命令

代码语言:javascript
复制
 [root@yygh-de test]# watch kubectl get pod -l app=my-app

2.7然后部署版本2的应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v2.yaml

这个时候可以观察上面新开的终端中的 Pod 列表的变化,可以看到之前的3个 Pod 都会先处于Terminating状态,并且3个 Pod 都被删除后才开始创建新的 Pod。

2.8然后测试第二个版本应用的部署进度

代码语言:javascript
复制
 [root@yygh-de test]# while sleep 0.1; do curl http://127.0.0.1:32077; done
 curl: (7) Failed connect to 127.0.0.1:32532; Connection refused
 curl: (7) Failed connect to 127.0.0.1:32532; Connection refused
 ......
 Host: my-app-759bffd84f-wzx2q, Version: v2.0.0
 Host: my-app-759bffd84f-ckdlm, Version: v2.0.0
 Host: my-app-759bffd84f-c7vnp, Version: v2.0.0
 Host: my-app-759bffd84f-ckdlm, Version: v2.0.0
 ......

可以看到最开始的阶段服务都是处于不可访问的状态,然后到第二个版本的应用部署成功后才正常访问,可以看到现在访问的数据是版本2了。

2.9最后,可以执行下面的命令来清空上面的资源对象:

代码语言:javascript
复制
 [root@yygh-de test]# kubectl delete all -l app=my-app
 pod "my-app-759bffd84f-c7vnp"deleted
 pod "my-app-759bffd84f-ckdlm"deleted
 pod "my-app-759bffd84f-wzx2q"deleted
 service"my-app"deleted
 deployment.apps "my-app"deleted
 replicaset.apps "my-app-5b4755fccf"deleted
 replicaset.apps "my-app-759bffd84f"deleted

2.10结论

  • 应用状态全部更新
  • 停机时间取决于应用程序的关闭和启动消耗的时间

3、滚动更新(rolling-update)

3.1简介

滚动更新通过逐个替换实例来逐步部署新版本的应用,直到所有实例都被替换完成为止。它通常遵循以下过程:在负载均衡器后面使用版本 A 的实例池,然后部署版本 B 的一个实例,当服务准备好接收流量时(Readiness Probe 正常),将该实例添加到实例池中,然后从实例池中删除一个版本 A 的实例并关闭,如下图所示:

3.2下面是 Kubernetes 中通过 Deployment 来进行滚动更新的关键参数以及滚动更新过程应用接收流量的示意图

代码语言:javascript
复制
 spec:
 replicas: 3
 strategy:
   type: RollingUpdate
   rollingUpdate:
     maxSurge: 2       # 一次可以添加多少个Pod
     maxUnavailable: 1 # 滚动更新期间最大多少个Pod不可用

3.3现在仍然使用上面的 app-v1.yaml 这个资源清单文件,新建一个定义滚动更新的资源清单文件 app-v2-rolling-update.yaml,文件内容如下

代码语言:javascript
复制
 [root@yygh-de test]# vim app-v2-rolling-update.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 replicas: 10
  # maxUnavailable设置为0可以完全确保在滚动更新期间服务不受影响,还可以使用百分比的值来进行设置。
 strategy:
   type: RollingUpdate
   rollingUpdate:
     maxSurge: 1
     maxUnavailable: 0
 selector:
   matchLabels:
     app: my-app
 template:
   metadata:
     labels:
       app: my-app
       version: v2.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v2.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
          # 初始延迟设置高点可以更好地观察滚动更新过程
         initialDelaySeconds: 15
         periodSeconds: 5

3.4上面的资源清单中我们在环境变量中定义了版本2,然后通过设置strategy.type=RollingUpdate来定义该 Deployment 使用滚动更新的策略来更新应用,接下来我们按下面的步骤来验证滚动更新策略:

  • 版本1提供服务
  • 部署版本2
  • 等待直到所有副本都被版本2替换完成

3.5同样,首先部署版本1应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v1.yaml
 service/my-app created
 deployment.apps/my-app created

3.6测试版本1是否部署成功

代码语言:javascript
复制
 [root@yygh-de test]# kubectl get pods -l app=my-app
 NAME                     READY   STATUS   RESTARTS   AGE
 my-app-5b4755fccf-bmwm4   1/1     Running   0        41s
 my-app-5b4755fccf-pbg9g   1/1     Running   0        41s
 my-app-5b4755fccf-x6f5h   1/1     Running   0        41s
 [root@yygh-de test]# kubectl get svc my-app
 NAME     TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)       AGE
 my-app   NodePort   10.96.250.164   <none>        80:30896/TCP   66s
 [root@yygh-de test]# curl http://127.0.0.1:30896
 Host: my-app-5b4755fccf-bmwm4, Version: v1.0.0

3.7同样,在一个新终端中执行下面命令观察 Pod 变化

代码语言:javascript
复制
 [root@yygh-de test]# watch kubectl get pod -l app=my-app

3.8然后部署滚动更新版本2应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v2-rolling-update.yaml
 deployment.apps/my-app configured

3.9这个时候在上面的 watch 终端中可以看到多了很多 Pod,还在创建当中,并没有一开始就删除之前的 Pod,同样,这个时候执行下面命令,测试应用状态

代码语言:javascript
复制
 [root@yygh-de test]# while sleep 0.1; do curl http://127.0.0.1:30896; done
 Host: my-app-5b4755fccf-qw4p6, Version: v1.0.0
 Host: my-app-5b4755fccf-x6f5h, Version: v1.0.0
 Host: my-app-5b4755fccf-qw4p6, Version: v1.0.0
 Host: my-app-5b4755fccf-zkwg2, Version: v1.0.0
 Host: my-app-7555568f55-qd4sb, Version: v2.0.0
 Host: my-app-5b4755fccf-qw4p6, Version: v1.0.0
 Host: my-app-5b4755fccf-zkwg2, Version: v1.0.0
 Host: my-app-5b4755fccf-m7lxv, Version: v1.0.0
 Host: my-app-5b4755fccf-zkwg2, Version: v1.0.0
 Host: my-app-7555568f55-hkzfc, Version: v2.0.0
 Host: my-app-7555568f55-qd4sb, Version: v2.0.0
 Host: my-app-5b4755fccf-x6f5h, Version: v1.0.0
 Host: my-app-5b4755fccf-fxmpz, Version: v1.0.0

我们可以看到上面的应用并没有出现不可用的情况,最开始访问到的都是版本1的应用,然后偶尔会出现版本2的应用,直到最后全都变成了版本2的应用,而这个时候看上面 watch 终端中 Pod 已经全部变成10个版本2的应用了,我们可以看到这就是一个逐步替换的过程。

3.10如果在滚动更新过程中发现新版本应用有问题,我们可以通过下面的命令来进行一键回滚

代码语言:javascript
复制
 [root@yygh-de test]# kubectl rollout undo deploy my-app
 deployment.apps/my-app rolled back

3.11如果你想保持两个版本的应用都存在,那么我们也可以执行 pause 命令来暂停更新

代码语言:javascript
复制
 [root@yygh-de test]# kubectl rollout pause deploy my-app
 deployment.apps/my-app paused

这个时候我们再去循环访问我们的应用就可以看到偶尔会出现版本1的应用信息了。

3.12如果新版本应用程序没问题了,也可以继续恢复更新

代码语言:javascript
复制
 [root@yygh-de test]# kubectl rollout resume deploy my-app
 deployment.apps/my-app resumed

3.13最后,可以执行下面的命令来清空上面的资源对象

代码语言:javascript
复制
 [root@yygh-de test]# kubectl delete all -l app=my-app
 pod "my-app-5b4755fccf-2zqjp"deleted
 pod "my-app-5b4755fccf-5r5kx"deleted
 pod "my-app-5b4755fccf-bmwm4"deleted
 pod "my-app-5b4755fccf-fxmpz"deleted
 pod "my-app-5b4755fccf-pbg9g"deleted
 pod "my-app-5b4755fccf-qzh6c"deleted
 pod "my-app-5b4755fccf-sw8xd"deleted
 pod "my-app-5b4755fccf-x6f5h"deleted
 pod "my-app-5b4755fccf-zkwg2"deleted
 pod "my-app-7555568f55-98zhs"deleted
 pod "my-app-7555568f55-hkzfc"deleted
 pod "my-app-7555568f55-qd4sb"deleted
 service"my-app"deleted
 deployment.apps "my-app"deleted
 replicaset.apps "my-app-5b4755fccf"deleted
 replicaset.apps "my-app-7555568f55"deleted

3.14结论

  • 版本在实例之间缓慢替换
  • rollout/rollback 可能需要一定时间
  • 无法控制流量

4、蓝/绿(blue/green) - 最好用来验证 API 版本问题

4.1简介

蓝/绿发布是版本2 与版本1 一起发布,然后流量切换到版本2,也称为红/黑部署。蓝/绿发布与滚动更新不同,版本2(绿) 与版本1()一起部署,在测试新版本满足要求后,然后更新更新 Kubernetes 中扮演负载均衡器角色的 Service 对象,通过替换 label selector 中的版本标签来将流量发送到新版本,如下图所示:

4.2下面是蓝绿发布策略下应用方法的示例图:

在资源对象中,最重要的就是 Service 中 label selector 的定义

代码语言:javascript
复制
 selector:
 app: my-app
 version: v1.0.0

在 Kubernetes 中,我们可以用两种方法来实现蓝绿发布,通过单个 Service 对象或者 Ingress 控制器来实现蓝绿发布,实际操作都是类似的,都是通过 label 标签去控制。

4.3实现蓝绿发布的关键点就在于 Service 对象中 label selector 标签的匹配方法,比如我们重新定义版本1 的资源清单文件 app-v1-single-svc.yaml,文件内容如下

代码语言:javascript
复制
 [root@yygh-de test]# vim app-v1-single-svc.yaml
 apiVersion: v1
 kind: Service
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 type: NodePort
 ports:
  -name: http
   port: 80
   targetPort: http
  # 注意这里我们匹配 app 和 version 标签,当要切换流量的时候,我们更新 version >标签的值,比如:v2.0.0
 selector:
   app: my-app
   version: v1.0.0
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app-v1
 labels:
   app: my-app
 spec:
 replicas: 3
 selector:
   matchLabels:
     app: my-app
     version: v1.0.0
 template:
   metadata:
     labels:
       app: my-app
       version: v1.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v1.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5

4.4版本2 的应用定义和以前一样,新建文件 app-v2-single-svc.yaml,文件内容如下

代码语言:javascript
复制
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app-v2
 labels:
   app: my-app
 spec:
 replicas: 3
 selector:
   matchLabels:
     app: my-app
     version: v2.0.0
 template:
   metadata:
     labels:
       app: my-app
       version: v2.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v2.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5

4.5然后按照下面的步骤来验证使用单个 Service 对象实现蓝/绿部署的策略

  • 版本1 应用提供服务
  • 部署版本2 应用
  • 等到版本2 应用全部部署完成
  • 切换入口流量从版本1 到版本2
  • 关闭版本1 应用

4.6首先,部署版本1 应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v1-single-svc.yaml
 service/my-app created
 deployment.apps/my-app-v1 created

4.7测试版本1 应用是否部署成功

代码语言:javascript
复制
 [root@yygh-de test]# kubectl get pods -l app=my-app
 NAME                         READY   STATUS   RESTARTS   AGE
 my-app-v1-5b4755fccf-6nvg4   1/1     Running   0        25s
 my-app-v1-5b4755fccf-8qtjj   1/1     Running   0        25s
 my-app-v1-5b4755fccf-z8kzg   1/1     Running   0        25s
 [root@yygh-de test]# kubectl get svc -l app=my-app
 NAME     TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)       AGE
 my-app   NodePort   10.96.226.55   <none>        80:31905/TCP   43s
 [root@yygh-de test]# curl http://127.0.0.1:31905
 Host: my-app-v1-5b4755fccf-z8kzg, Version: v1.0.0

4.8同样,新开一个终端,执行如下命令观察 Pod 变化

代码语言:javascript
复制
 [root@yygh-de ~]# watch kubectl get pod -l app=my-app

4.9部署版本2 应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v2-single-svc.yaml
 deployment.apps/my-app-v2 created

4.10在上面 watch 终端中可以看到会多3个my-app-v2开头的 Pod,待这些 Pod 部署成功后,我们再去访问当前的应用

代码语言:javascript
复制
 [root@yygh-de test]# while sleep 0.1; do curl http://127.0.0.1:31905; done
 Host: my-app-v1-5b4755fccf-6nvg4, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-6nvg4, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-z8kzg, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-8qtjj, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-z8kzg, Version: v1.0.0

我们会发现访问到的都是版本1 的应用,和我们刚刚部署的版本2 没有任何关系,这是因为我们 Service 对象中通过 label selector 匹配的是version=v1.0.0这个标签,我们可以通过修改 Service 对象的匹配标签,将流量路由到标签version=v2.0.0的 Pod 去:

代码语言:javascript
复制
 [root@yygh-de test]# kubectl patch service my-app -p '{"spec":{"selector":{"version":"v2.0.0"}}}'
 service/my-app patched

4.11再去访问应用,可以发现现在都是版本2 的信息了

代码语言:javascript
复制
 [root@yygh-de test]# while sleep 0.1; do curl http://127.0.0.1:31905; done
 Host: my-app-v2-759bffd84f-zxkf2, Version: v2.0.0
 Host: my-app-v2-759bffd84f-zxkf2, Version: v2.0.0
 Host: my-app-v2-759bffd84f-lm45n, Version: v2.0.0
 Host: my-app-v2-759bffd84f-zxkf2, Version: v2.0.0
 Host: my-app-v2-759bffd84f-lm45n, Version: v2.0.0
 Host: my-app-v2-759bffd84f-zxkf2, Version: v2.0.0
 Host: my-app-v2-759bffd84f-zxkf2, Version: v2.0.0
 Host: my-app-v2-759bffd84f-lm45n, Version: v2.0.0
 Host: my-app-v2-759bffd84f-zxkf2, Version: v2.0.0
 Host: my-app-v2-759bffd84f-lm45n, Version: v2.0.0

4.12如果你需要回滚到版本1,同样只需要更改 Service 的匹配标签即可

代码语言:javascript
复制
 [root@yygh-de test]# kubectl patch service my-app -p '{"spec":{"selector":{"version":"v1.0.0"}}}'
 service/my-app patched

4.13如果新版本已经完全符合我们的需求了,就可以删除版本1 的应用了

代码语言:javascript
复制
 [root@yygh-de test]# kubectl delete deploy my-app-v1
 deployment.apps "my-app-v1"deleted

4.14最后,同样,执行如下命令清理上述资源对象

代码语言:javascript
复制
 [root@yygh-de test]# kubectl delete all -l app=my-app
 pod "my-app-v2-759bffd84f-lm45n"deleted
 pod "my-app-v2-759bffd84f-wjtvk"deleted
 pod "my-app-v2-759bffd84f-zxkf2"deleted
 service"my-app"deleted
 deployment.apps "my-app-v2"deleted
 replicaset.apps "my-app-v2-759bffd84f"deleted

4.15结论

  • 实时部署/回滚
  • 避免版本问题,因为一次更改是整个应用的改变
  • 需要两倍的资源
  • 在发布到生产之前,应该对整个应用进行适当的测试

5、金丝雀(Canary) - 让部分用户参与测试

5.1简介

金丝雀部署是让部分用户访问到新版本应用,在 Kubernetes 中,可以使用两个具有相同 Pod 标签的 Deployment 来实现金丝雀部署。新版本的副本和旧版本的一起发布。在一段时间后如果没有检测到错误,则可以扩展新版本的副本数量并删除旧版本的应用。

如果需要按照具体的百分比来进行金丝雀发布,需要尽可能的启动多的 Pod 副本,这样计算流量百分比的时候才方便,比如,如果你想将 1% 的流量发送到版本 B,那么我们就需要有一个运行版本 B 的 Pod 和 99 个运行版本 A 的 Pod,当然如果你对具体的控制策略不在意的话也就无所谓了,如果你需要更精确的控制策略,建议使用服务网格(如 Istio),它们可以更好地控制流量。

5.2金丝雀发布的应用请求示意图

在下面的例子中,我们使用 Kubernetes 原生特性来实现一个穷人版的金丝雀发布,如果你想要对流量进行更加细粒度的控制,请使用豪华版本的 Istio。

5.3接下来我们按照下面的步骤来验证金丝雀策略

  • 10个副本的版本1 应用提供服务
  • 版本2 应用部署1个副本(意味着小于10%的流量)
  • 等待足够的时间来确认版本2 应用足够稳定没有任何错误信息
  • 将版本2 应用扩容到10个副本
  • 等待所有实例完成
  • 关闭版本1 应用

5.4创建版本1 的应用资源清单,app-v1-canary.yaml,内容如下

代码语言:javascript
复制
 [root@yygh-de test]# vim app-v1-canary.yaml
 apiVersion: v1
 kind: Service
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 type: NodePort
 ports:
  -name: http
   port: 80
   targetPort: http
 selector:
   app: my-app
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app-v1
 labels:
   app: my-app
 spec:
 replicas: 10
 selector:
   matchLabels:
     app: my-app
     version: v1.0.0
 template:
   metadata:
     labels:
       app: my-app
       version: v1.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v1.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5

5.5其中核心的部分也是 Service 对象中的 label selector 标签,不在具有版本相关的标签了,然后定义版本2 的资源清单文件,app-v2-canary.yaml,文件内容如下

代码语言:javascript
复制
 [root@yygh-de test]# vim app-v2-canary.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app-v2
 labels:
   app: my-app
 spec:
 replicas: 1
 selector:
   matchLabels:
     app: my-app
     version: v2.0.0
 template:
   metadata:
     labels:
       app: my-app
       version: v2.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v2.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5

版本1 和版本2 的 Pod 都具有一个共同的标签app=my-app,所以对应的 Service 会匹配两个版本的 Pod。

5.6部署版本1 应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v1-canary.yaml
 service/my-app created
 deployment.apps/my-app-v1 created

5.7然后测试版本1 应用是否正确部署了

代码语言:javascript
复制
 [root@yygh-de test]# kubectl get svc -l app=my-app
 NAME     TYPE       CLUSTER-IP   EXTERNAL-IP   PORT(S)       AGE
 my-app   NodePort   10.96.39.92   <none>        80:30927/TCP   25s
 [root@yygh-de test]# curl http://127.0.0.1:30927
 Host: my-app-v1-5b4755fccf-dfzwx, Version: v1.0.0

5.8同样,新开一个终端,查看 Pod 的变化

代码语言:javascript
复制
 [root@yygh-de ~]# watch kubectl get pod

5.9然后部署版本2 应用

代码语言:javascript
复制
 [root@yygh-de test]# kubectl apply -f app-v2-canary.yaml
 deployment.apps/my-app-v2 created

5.10然后在 watch 终端页面可以看到多了一个 Pod,现在一共 11 个 Pod,其中只有1 个 Pod 运行新版本应用,然后同样可以循环访问该应用,查看是否会有版本2 的应用信息

代码语言:javascript
复制
 [root@yygh-de test]# while sleep 0.1; do curl http://127.0.0.1:30927; done
 Host: my-app-v1-5b4755fccf-n84kt, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-dfzwx, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-p2mh2, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-cvx2f, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-ntbf8, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-ntbf8, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-cvx2f, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-qjqbg, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-6qvb6, Version: v1.0.0
 Host: my-app-v2-759bffd84f-lv8wc, Version: v2.0.0
 Host: my-app-v1-5b4755fccf-dfzwx, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-n84kt, Version: v1.0.0
 Host: my-app-v2-759bffd84f-lv8wc, Version: v2.0.0
 Host: my-app-v1-5b4755fccf-p2mh2, Version: v1.0.0
 Host: my-app-v1-5b4755fccf-x9csb, Version: v1.0.0
 Host: my-app-v2-759bffd84f-lv8wc, Version: v2.0.0

5.11正常情况下可以看到大部分都是返回的版本1 的应用信息,偶尔会出现版本2 的应用信息,这就证明我们的金丝雀发布成功了,待确认了版本2 的这个应用没有任何问题后,可以将版本2 应用扩容到10 个副本

代码语言:javascript
复制
 [root@yygh-de test]# kubectl scale --replicas=10 deploy my-app-v2
 deployment.apps/my-app-v2 scaled

5.12其实这个时候访问应用的话新版本和旧版本的流量分配是1:1了,确认了版本2 正常后,就可以删除版本1 的应用了

代码语言:javascript
复制
 [root@yygh-de test]# kubectl delete deploy my-app-v1
 deployment.apps "my-app-v1"deleted
 # 最终留下的是 10 个新版本的 Pod 了,到这里我们的整个金丝雀发布就完成了。

5.13同样,最后,执行下面的命令删除上面的资源对象

代码语言:javascript
复制
 [root@yygh-de test]# kubectl delete all -l app=my-app
 pod "my-app-v2-759bffd84f-4vr6p"deleted
 pod "my-app-v2-759bffd84f-69fwj"deleted
 pod "my-app-v2-759bffd84f-7w24k"deleted
 pod "my-app-v2-759bffd84f-d8wwz"deleted
 pod "my-app-v2-759bffd84f-flptr"deleted
 pod "my-app-v2-759bffd84f-gznbk"deleted
 pod "my-app-v2-759bffd84f-lv8wc"deleted
 pod "my-app-v2-759bffd84f-q8rcn"deleted
 pod "my-app-v2-759bffd84f-smbk5"deleted
 pod "my-app-v2-759bffd84f-t6q9q"deleted
 service"my-app"deleted
 deployment.apps "my-app-v2"deleted
 replicaset.apps "my-app-v2-759bffd84f"deleted

5.14结论

  • 部分用户获取新版本
  • 方便错误和性能监控
  • 快速回滚
  • 发布较慢
  • 流量精准控制很浪费(99%A / 1%B = 99 Pod A,1 Pod B)

如果你对新功能的发布没有信心,建议使用金丝雀发布的策略。

6、A/B测试(A/B testing) - 最适合部分用户的功能测试

6.1简介

A/B 测试实际上是一种基于统计信息而非部署策略来制定业务决策的技术,与业务结合非常紧密。但是它们也是相关的,也可以使用金丝雀发布来实现。

除了基于权重在版本之间进行流量控制之外,A/B 测试还可以基于一些其他参数(比如 Cookie、User Agent、地区等等)来精确定位给定的用户群,该技术广泛用于测试一些功能特性的效果,然后按照效果来进行确定。

我们经常可以在今日头条的客户端中就会发现有大量的 A/B 测试,同一个地区的用户看到的客户端有很大不同。

要使用这些细粒度的控制,仍然还是建议使用 Istio,可以根据权重或 HTTP 头等来动态请求路由控制流量转发。

6.2下面是使用 Istio 进行规则设置的示例,因为 Istio 还不太稳定,以下示例规则将来可能会更改:

代码语言:javascript
复制
 route:
 -tags:
 version: v1.0.0
 weight: 90
 -tags:
 version: v2.0.0
 weight: 10

6.3部署Istio

在本示例中,使用Istio 1.0.0。要安装Istio,请遵循Istio网站上的 说明。

默认情况下应启用自动边车注入。然后注释默认名称空间以启用它。

代码语言:javascript
复制
 [root@yygh-de AB]# kubectl label namespace default istio-injection=enabled
 namespace/default labeled

6.4创建应用资源清单app-v1.yaml

代码语言:javascript
复制
 [root@yygh-de AB]# vim app-v1.yaml
 apiVersion: v1
 kind: Service
 metadata:
 name: my-app-v1
 labels:
   app: my-app
 spec:
 ports:
  -name: http
   port: 80
   targetPort: http
 selector:
   app: my-app
   version: v1.0.0
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app-v1
 labels:
   app: my-app
 spec:
 replicas: 1
 selector:
   matchLabels:
     app: my-app
     version: v1.0.0
 template:
   metadata:
     labels:
       app: my-app
       version: v1.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v1.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5
代码语言:javascript
复制
 # 创建应用资源清单app-v2.yaml
 [root@yygh-de AB]# vim app-v2.yaml
 apiVersion: v1
 kind: Service
 metadata:
 name: my-app-v2
 labels:
   app: my-app
 spec:
 ports:
  -name: http
   port: 80
   targetPort: http
 selector:
   app: my-app
   version: v2.0.0
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
 name: my-app-v2
 labels:
   app: my-app
 spec:
 replicas: 1
 selector:
   matchLabels:
     app: my-app
     version: v2.0.0
 template:
   metadata:
     labels:
       app: my-app
       version: v2.0.0
     annotations:
       prometheus.io/scrape: "true"
       prometheus.io/port: "9101"
   spec:
     containers:
      -name: my-app
       image: containersol/k8s-deployment-strategies
       ports:
        -name: http
         containerPort: 8080
        -name: probe
         containerPort: 8086
       env:
        -name: VERSION
         value: v2.0.0
       livenessProbe:
         httpGet:
           path: /live
           port: probe
         initialDelaySeconds: 5
         periodSeconds: 5
       readinessProbe:
         httpGet:
           path: /ready
           port: probe
         periodSeconds: 5

6.5部署两个应用程序

返回此仓库中的a / b测试目录,使用istioctl命令部署两个应用程序,以注入用于代理请求的Istio sidecar容器:

代码语言:javascript
复制
 [root@yygh-de AB]# kubectl apply -f app-v1.yaml -f app-v2.yaml
 service/my-app-v1 created
 deployment.apps/my-app-v1 created
 service/my-app-v2 created
 deployment.apps/my-app-v2 created

6.6通过Istio网关公开这两个服务,并创建一个VirtualService来将请求与my-app-v1服务进行匹配:

代码语言:javascript
复制
 [root@yygh-de AB]# vim gateway.yaml
 apiVersion: networking.istio.io/v1alpha3
 kind: Gateway
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 servers:
    -port:
       number: 80
       name: http
       protocol: HTTP
     hosts:
        -my-app.local
 ----------------------------------------------------------------------------------------
 [root@yygh-de AB]# vim virtualservice.yaml
 apiVersion: networking.istio.io/v1alpha3
 kind: VirtualService
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 hosts:
    -my-app.local
 gateways:
    -my-app
 http:
    -route:
        -destination:
           host: my-app-v1

6.7此时,如果您使用给定的主机向Istio入口网关发出请求,则my-app.local应该只看到版本1做出响应

代码语言:javascript
复制
 [root@yygh-de AB]# curl $(minikube service istio-ingressgateway -n istio-system --url | head -n1) -H 'Host: my-app.local'
 Host: my-app-v1-6d577d97b4-lxn22, Version: v1.0.0

6.8根据体重转移交通

根据权重应用Istio VirtualService规则:

代码语言:javascript
复制
 [root@yygh-de AB]# vim virtualservice-weight.yaml # 您可以编辑每个目标的权重并将更新后的规则应用于Minikube
 apiVersion: networking.istio.io/v1alpha3
 kind: VirtualService
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 hosts:
    -my-app.local
 gateways:
    -my-app
 http:
    -route:
        -destination:
           host: my-app-v1
         weight: 90
        -destination:
           host: my-app-v2
         weight: 10

6.9现在,您可以测试两种版本之间的流量分配是否正确

代码语言:javascript
复制
 [root@yygh-de AB]# service=$(minikube service istio-ingressgateway -n istio-system --url | head -n1)
 # 您应该大约在10个版本2中看到1个请求
 [root@yygh-de AB]# while sleep 0.1; do curl "$service" -H 'Host: my-app.local'; done

6.10根据标题转移流量

根据标题应用Istio VirtualService规则:

代码语言:javascript
复制
 [root@yygh-de AB]# vim virtualservice-match.yaml
 apiVersion: networking.istio.io/v1alpha3
 kind: VirtualService
 metadata:
 name: my-app
 labels:
   app: my-app
 spec:
 hosts:
    -my-app.local
 gateways:
    -my-app
 http:
    -route:
        -destination:
           host: my-app-v1
     match:
        -headers:
           x-api-version:
             exact: v1.0.0
    -route:
        -destination:
           host: my-app-v2
     match:
        -headers:
           x-api-version:
             exact: v2.0.0

6.11现在,您可以测试流量是否达到了正确的实例集

代码语言:javascript
复制
 [root@yygh-de AB]# service=$(minikube service istio-ingressgateway -n istio-system --url | head -n1)
 [root@yygh-de AB]# curl $service -H 'Host: my-app.local' -H 'X-API-Version: v1.0.0'
 Host: my-app-v1-6d577d97b4-s4h6k, Version: v1.0.0
 [root@yygh-de AB]# curl $service -H 'Host: my-app.local' -H 'X-API-Version: v2.0.0'
 Host: my-app-v2-65f9fdbb88-jtctt, Version: v2.0.0

6.12清理

代码语言:javascript
复制
 [root@yygh-de AB]# kubectl delete gateway/my-app virtualservice/my-app
 [root@yygh-de AB]# kubectl delete -f ./app-v1.yaml -f ./app-v2.yaml
 [root@yygh-de AB]# kubectl delete -f <PATH-TO-ISTIO>/install/kubernetes/istio-demo.yaml

6.13结论

  • 几个版本并行运行
  • 完全控制流量分配
  • 特定的一个访问错误难以排查,需要分布式跟踪
  • Kubernetes 没有直接的支持,需要其他额外的工具

7、总结

发布应用有许多种方法,当发布到开发/测试环境的时候,重建或者滚动更新通常是一个不错的选择。在生产环境,滚动更新或者蓝绿发布比较合适,但是新版本的提前测试是非常有必要的。如果你对新版本的应用不是很有信心的话,那应该使用金丝雀发布,将用户的影响降到最低。最后,如果你的公司需要在特定的用户群体中进行新功能的测试,例如,移动端用户请求路由到版本 A,桌面端用户请求路由到版本 B,那么你就看使用A/B 测试,通过使用 Kubernetes 服务网关的配置,可以根据某些请求参数来确定用户应路由的服务。

下一篇
举报
领券