Kubernetes 部署策略

Posted 看,未来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kubernetes 部署策略相关的知识,希望对你有一定的参考价值。

Kubernetes 部署策略

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

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

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

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

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

spec:
  replicas: 3
  strategy:
    type: Recreate

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

我们这里创建两个相关的资源清单文件,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

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

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

版本1提供服务
删除版本1
部署版本2
等待所有副本准备就绪

首先部署第一个应用:

$ kubectl apply -f app-v1.yaml
service "my-app" created
deployment.apps "my-app" created

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

$ kubectl get pods -l app=my-app
NAME                      READY     STATUS    RESTARTS   AGE
my-app-7b4874cd75-m5kct   1/1       Running   0          19m
my-app-7b4874cd75-pc444   1/1       Running   0          19m
my-app-7b4874cd75-tlctl   1/1       Running   0          19m
$ kubectl get svc my-app
NAME      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
my-app    NodePort   10.108.238.76   <none>        80:32532/TCP   5m
$ curl http://127.0.0.1:32532
Host: my-app-7b4874cd75-pc444, Version: v1.0.0

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

$ watch kubectl get po -l app=my-app

然后部署版本2的应用:

$ kubectl apply -f app-v2.yaml

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

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

$ while sleep 0.1; do curl http://127.0.0.1:32532; 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-f885c8d45-sp44p, Version: v2.0.0
Host: my-app-f885c8d45-t8g7g, Version: v2.0.0
Host: my-app-f885c8d45-sp44p, Version: v2.0.0
......

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

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

$ kubectl delete all -l app=my-app

结论:

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

滚动更新(rolling-update)

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

下图是滚动更新过程应用接收流量的示意图:

下面是 Kubernetes 中通过 Deployment 来进行滚动更新的关键参数:

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

现在仍然使用上面的 app-v1.yaml 这个资源清单文件,新建一个定义滚动更新的资源清单文件 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

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

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

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

$ kubectl apply -f app-v1.yaml
service "my-app" created
deployment.apps "my-app" created

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

$ kubectl get pods -l app=my-app
NAME                      READY     STATUS    RESTARTS   AGE
my-app-7b4874cd75-h8c4d   1/1       Running   0          47s
my-app-7b4874cd75-p4l8f   1/1       Running   0          47s
my-app-7b4874cd75-qnt7p   1/1       Running   0          47s
$ kubectl get svc my-app
NAME      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
my-app    NodePort   10.109.99.184   <none>        80:30486/TCP   1m
$ curl http://127.0.0.1:30486
Host: my-app-7b4874cd75-qnt7p, Version: v1.0.0

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

$ watch kubectl get pod -l app=my-app

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

$ kubectl apply -f app-v2-rolling-update.yaml
deployment.apps "my-app" configured

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

$ while sleep 0.1; do curl http://127.0.0.1:30486; done
Host: my-app-7b4874cd75-vrlj7, Version: v1.0.0
......
Host: my-app-7b4874cd75-vrlj7, Version: v1.0.0
Host: my-app-6b5479d97f-2fk24, Version: v2.0.0
Host: my-app-7b4874cd75-p4l8f, Version: v1.0.0
......
Host: my-app-6b5479d97f-s5ctz, Version: v2.0.0
Host: my-app-7b4874cd75-5ldqx, Version: v1.0.0
......
Host: my-app-6b5479d97f-5z6ww, Version: v2.0.0

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

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

$ kubectl rollout undo deploy my-app
deployment.apps "my-app"

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

$ kubectl rollout pause deploy my-app
deployment.apps "my-app" paused

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

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

$ kubectl rollout resume deploy my-app
deployment.apps "my-app" resumed

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

$ kubectl delete all -l app=my-app

结论:

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

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

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

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

在 Kubernetes 中,我们可以用两种方法来实现蓝绿发布,通过单个 Service 对象或者 Ingress 控制器来实现蓝绿发布,实际操作都是类似的,都是通过 label 标签去控制。
实现蓝绿发布的关键点就在于 Service 对象中 label selector 标签的匹配方法,比如我们重新定义版本1 的资源清单文件 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

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

selector:
  app: my-app
  version: v1.0.0

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

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

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

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

首先,部署版本1 应用:

$ kubectl apply -f app-v1-single-svc.yaml
service "my-app" created
deployment.apps "my-app-v1" created

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

$ kubectl get pods -l app=my-app
NAME                         READY     STATUS    RESTARTS   AGE
my-app-v1-7b4874cd75-7xh6s   1/1       Running   0          41s
my-app-v1-7b4874cd75-dmq8f   1/1       Running   0          41s
my-app-v1-7b4874cd75-t64z7   1/1       Running   0          41s
$ kubectl get svc -l app=my-app
NAME      TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
my-app    NodePort   10.106.184.144   <none>        80:31539/TCP   50s
$ curl http://127.0.0.1:31539
Host: my-app-v1-7b4874cd75-7xh6s, Version: v1.0.0

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

$ watch kubectl get pod -l app=my-app

然后部署版本2 应用:

$ kubectl apply -f app-v2-single-svc.yaml
deployment.apps "my-app-v2" created

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

$ while sleep 0.1; do curl http://127.0.0.1:31539; done
Host: my-app-v1-7b4874cd75-dmq8f, Version: v1.0.0
Host: my-app-v1-7b4874cd75-dmq8f, Version: v1.0.0
......

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

$ kubectl patch service my-app -p '"spec":"selector":"version":"v2.0.0"'
service "my-app" patched

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

$ while sleep 0.1; do curl http://127.0.0.1:31539; done
Host: my-app-v2-f885c8d45-r5m6z, Version: v2.0.0
Host: my-app-v2-f885c8d45-r5m6z, Version: v2.0.0
......

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

$ kubectl patch service my-app -p '"spec":"selector":"version":"v1.0.0"'

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

$ kubectl delete deploy my-app-v1

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

$ kubectl delete all -l app=my-app

结论:

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

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

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

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

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

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

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

首先,创建版本1 的应用资源清单,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以上是关于Kubernetes 部署策略的主要内容,如果未能解决你的问题,请参考以下文章

18ReplicaSet手动蓝绿部署滚动发布回滚及Deployment自动滚动发布回滚及金丝雀发布回滚

蓝绿部署滚动部署灰度发布金丝雀发布

一文搞懂蓝绿部署和金丝雀发布

一文搞懂蓝绿部署和金丝雀发布

一文搞懂蓝绿部署和金丝雀发布

Kubernetes 实现灰度和蓝绿发布