deployment如何使用不同的策略部署我们的程序?

随着kubernetes中服务数量的越来越多, 我们需要频繁的对服务进行升级发布。 那么对于服务可用性就存在了要求, 我们是可以容忍服务停机一段时间还是在不停机的情况下完成发布? 而随着发布回滚的进行需要进行如下的操作:

  • 新版本服务启动

  • 旧版本服务停止

  • 验证新服务是否启动成功

kubernetes中对与程序发布的策略是如何实现的呢? kubernets中已经考虑了这方面的诉求, 它允许我们使用不同的策略更新我们的应用程序. 根据具体的需求可选的方案如下:

  • 重新创建(recreate):终止旧版本并释放新版本;

  • 滚动更新(rolling-update):一个接一个地以滚动更新的方式发布新版本;

  • [蓝/绿(blue/green)](#蓝/绿(blue/green)):与旧版本一起发布新版本,然后切换流量;

  • [金丝雀(canary)](#金丝雀(canary)):向部分用户发布新版本,然后进行全面推出;

  • a/b 测试(a/b testing):以精确的方式向用户的子集发布新版本(HTTP标头,cookie,重量等)。

Kubernetes部署本质上只是ReplicaSets的包装。ReplicaSet管理正在运行的Pod的数量,Deployment在此之上实现功能,以允许滚动更新,对Pod的运行状况检查以及轻松回滚更新。

Kubernetes滚动更新

Kubernetes部署本质上只是ReplicaSets的包装。ReplicaSet管理正在运行的Pod的数量,Deployment在此之上实现功能,以允许滚动更新,对Pod的运行状况检查以及轻松回滚更新。

在正常操作期间,部署将仅管理一个ReplicaSet,以确保所需数量的Pod正在运行:

使用Deployment来控制Pod的主要好处之一是能够执行滚动更新。滚动更新允许您逐步更新Pod的配置,并且Deployments提供了许多选项来控制此过程。

配置滚动更新最重要的选项是更新策略。在您的部署清单中,spec.strategy.type具有两个可能的值:

  • RollingUpdate:逐渐添加新的Pod,逐渐终止旧的Pod

  • recreate: 在添加任何新吊舱之前,所有旧吊舱均已终止

在大多数情况下,RollingUpdate是部署的首选更新策略。如果您将Pod作为单例运行,并且重复的Pod持续几秒钟是不可接受的,则Recreate很有用。

Rolling Deployment

滚动更新的概念主要存在于k8s中的deployment资源对象, deployment创建基于label选择的Replicaset,使用新版本的应用程序创建辅助 ReplicaSet ,然后减少旧版副本的数量,并增加新版本,直到达到正确数量的副本。

参数如下:

 [...]
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
[...]

replicas: 3 定义运行几个副本

strategy.type 指定更新的策略, 主要包括 Rollingupdate和recreate 两种策略, 其中Rollingupdate为默认策略

strategy.rollingUpdate.maxSurge:1 除了relicas中指定的副本数更新时可以临时运行的副本数, 上面的例子中表示更新时最多可以运行4个副本

strategy.rollingupdate.maxUnavailable: 1 定义在更新的过程中不可用的副本数量, 这个数量是基于relicas配置的,上面的例子中总共定义三个副本也就是更新的时候只有1个副本不可用, 两个副本可用

maxSurge和maxUnavailable都可以指定为整数(例如2)或百分比(例如50%),并且不能都为零。当指定为整数时,它表示实际的容器数;当指定百分比时,将使用所需pods数量的百分比,四舍五入。例如,如果您对maxSurge和maxUnavailable使用默认值25%,并且将更新应用于具有8个容器的Deployment,则maxSurge将为2个容器,而maxUnavailable也将为2个容器。这意味着在更新过程中:

  • 更新期间准备10个pods(8个所需容器+ 2个maxSurge 容器)

  • 在更新期间,至少有6个容器(8个所需容器-2个最大不可用容器)将始终处于就绪状态

重要的是要注意,在考虑部署应在更新期间运行的Pod数量时,它将使用在部署的更新版本中指定的副本数,而不是现有版本 部署horizontal-pod-autoscaling时,才可以基于百分比的值而非 maxSurge和 maxUnavailable 的数字 除了上面说到的几个参数以外,Health Probe也是重要的组成部分, 这样可以保证程序的0停机时间更新应用程序 请参考阅读Kubernetes使用什么方法方法来检查应用程序的运行状况?

优点:

  • 版本在实例间缓慢发布

  • 有状态应用程序可以很方便的处理数据得到平衡

缺点:

  • 首次发布/回滚可能需要一些时间

  • 支持多种 API 很难

  • 无法控制流量

1.定义yaml

假设k8s集群中当前运行的v1版本的程序, 我们需要更新到v2版本

v1.yaml

apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  type: ClusterIP
  selector:
    app: backend
  ports:
    - name: http
      port: 9898
      protocol: TCP
      targetPort: http
    - port: 9999
      targetPort: grpc
      protocol: TCP
      name: grpc

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: stefanprodan/podinfo:3.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9898
          protocol: TCP
        - name: http-metrics
          containerPort: 9797
          protocol: TCP
        - name: grpc
          containerPort: 9999
          protocol: TCP
        command:
        - ./podinfo
        - --port=9898
        - --port-metrics=9797
        - --grpc-port=9999
        - --grpc-service-name=backend
        - --level=info
        env:
        - name: PODINFO_UI_COLOR
          value: "#34577c"
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/healthz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/readyz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        resources:
          limits:
            cpu: 2000m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 32Mi

v2.yaml

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: app
  labels:
    app.kubernetes.io/name: app
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: app
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app
    spec:
      containers:
        - name: app
          image: "nginx:1.7.9"
          imagePullPolicy: IfNotPresent
          env:
          - name: VERSION
            value: v2.0.0
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {}

上面两个资源清单文件中的 Deployment 定义几乎是一直的,唯一不同的是定义的环境变量VERSION值不同

2.首先部署v1.0.0应用

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

测试v1 是否部署成功

➜  kubectl get pods    -l app.kubernetes.io/name=app
NAME                   READY   STATUS    RESTARTS   AGE
app-7cfcdf4b77-bt6nk   1/1     Running   0          72s
app-7cfcdf4b77-h9mzs   1/1     Running   0          72s
app-7cfcdf4b77-nr44j   1/1     Running   0          72s

➜ kubectl get svc  -l app.kubernetes.io/name=app
NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
app    ClusterIP   10.222.46.182   <none>        80/TCP    99s

➜ kubectl get ep  -l app.kubernetes.io/name=app
NAME   ENDPOINTS                                            AGE
app    10.41.206.150:80,10.41.206.164:80,10.41.206.185:80   2m54s

确认当前v1.0.0提供服务

watch -n 1 curl -s backend:9898/env |grep VERSION

3.部署v2.0.0应用

➜ kubectl apply -f v2.yaml
deployment.apps/backend configured

验证

 ➜ watch -n 1 curl -s 10.222.50.39:9898/env |grep VERSION
  ..........
  "VERSION=v3.0.0",
  "VERSION=v3.0.0",
  "VERSION=v4.0.0",
  "VERSION=v4.0.0",
  "VERSION=v4.0.0",
  "VERSION=v4.0.0",
  "VERSION=v3.0.0",
  "VERSION=v3.0.0",
  "VERSION=v3.0.0",
  "VERSION=v3.0.0",
  "VERSION=v4.0.0",
  "VERSION=v4.0.0",
  "VERSION=v4.0.0",
  .......

我们可以看到在部署的过程中不是基于版本控制的而是基于副本数控制的

recreate

滚动更新策略对于确保更新过程中的零停机时间。但是,这种方法的副作用是在更新过程中,容器的两个版本同时运行。这可能会给服务消费者带来问题,特别是当更新过程在服务api中引入了向后不兼容的更改,而客户端无法处理这些更改时。对于这种场景, 策略定义为Recreate的Deployment,会终止所有正在运行的实例,然后用较新的版本来重新创建它们。

recreate策略的本质是将maxUnavailable设置为replicas中的的副本数量。

spec:
  replicas: 3
  strategy:
    type: Recreate

蓝/绿(blue/green)

蓝绿部署是最常见的一种0 downtime部署的方式,是一种以可预测的方式发布应用的技术,目的是减少发布过程中服务停止的时间。 蓝绿部署原理上很简单,就是通过冗余来解决问题。通常生产环境需要两组配置(蓝绿配置),一组是active的生产环境的配置(绿配置),一组是inactive的配置(蓝绿配置) 用户访问的时候,只会让用户访问active的服务器集群。在绿色环境(active)运行当前生产环境中的应用,也就是旧版本应用version1。当你想要升级到version2 ,在蓝色环境(inactive)中进行操作,即部署新版本应用,并进行测试。如果测试没问题,就可以把负载均衡器/反向代理/路由指向蓝色环境了。随后需要监测新版本应用,也就是version2 是否有故障和异常。如果运行良好,就可以删除version1 使用的资源。如果运行出现了问题,可以通过负载均衡器指向快速回滚到绿色环境。

蓝绿部署的简短流程如下:

  • v1对外提供服务

  • 部署v2

  • 验证v2版本可用性

  • 测试完成后切换v1上的流量到v2上

  • 关闭v1的服务

关于蓝绿部署的细节请阅读BlueGreenDeployment

1.部署v1

v1.yaml

apiVersion: v1
kind: Service
metadata:
  name: backend
  labels:
    app: backend
spec:
  type: ClusterIP
  selector:
    app: backend
    version: v1.0.0
  ports:
    - name: http
      port: 9898
      protocol: TCP
      targetPort: http
    - port: 9999
      targetPort: grpc
      protocol: TCP
      name: grpc

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-v1
  labels:
    app: backend
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app: backend
      version: v1.0.0
  template:
    metadata:
      labels:
        app: backend
        version: v1.0.0
    spec:
      containers:
      - name: backend-v1
        image: stefanprodan/podinfo:3.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9898
          protocol: TCP
        - name: http-metrics
          containerPort: 9797
          protocol: TCP
        - name: grpc
          containerPort: 9999
          protocol: TCP
        command:
        - ./podinfo
        - --port=9898
        - --port-metrics=9797
        - --grpc-port=9999
        - --grpc-service-name=backend
        - --level=info
        env:
        - name: PODINFO_UI_COLOR
          value: "#34577c"
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/healthz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/readyz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        resources:
          limits:
            cpu: 2000m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 32Mi

执行部署

$ kubectl apply -f v1.yaml
service/backend unchanged
deployment.apps/backend configured

查看部署是否成功

 $ curl -s 10.222.50.39:9898/env |grep VERSION
  "VERSION=v1.0.0",

2.部署v2

v2.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-v2
  labels:
    app: backend
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3
      maxUnavailable: 3
  selector:
    matchLabels:
      app: backend
      version: v2.0.0
  template:
    metadata:
      labels:
        app: backend
        version: v2.0.0
    spec:
      containers:
      - name: backend-v2
        image: stefanprodan/podinfo:3.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9898
          protocol: TCP
        - name: http-metrics
          containerPort: 9797
          protocol: TCP
        - name: grpc
          containerPort: 9999
          protocol: TCP
        command:
        - ./podinfo
        - --port=9898
        - --port-metrics=9797
        - --grpc-port=9999
        - --grpc-service-name=backend
        - --level=info
        env:
        - name: PODINFO_UI_COLOR
          value: "#34577c"
        - name: VERSION
          value: v2.0.0
        livenessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/healthz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/readyz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        resources:
          limits:
            cpu: 2000m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 32Mi

执行部署

$ kubectl apply -f v2.yaml 
deployment.apps/backend-v2 created

查看是否部署成功

$ kubectl get pods  -l app=backend
NAME                          READY   STATUS    RESTARTS   AGE
backend-v1-76bdc697d7-4nc89   1/1     Running   0          148m
backend-v1-76bdc697d7-lvn6n   1/1     Running   0          148m
backend-v1-76bdc697d7-vkf2z   1/1     Running   0          148m
backend-v2-78f8b89f4-hl4qr    1/1     Running   0          79s
backend-v2-78f8b89f4-kshrg    1/1     Running   0          79s
backend-v2-78f8b89f4-s2w77    1/1     Running   0          79s

3.流量切换

通过上面的步骤我们已经将v1版本和v2版本的pod全部部署到主机上但是service仍然发送流量到V1的版本, 修改资源让service将所有的流量发送到label=v2.0.0上

$ kubectl patch service backend -p '{"spec":{"selector": {"version":"v2.0.0"}}}'

测试流量是否切换成功

 # watch -n 1 curl -s 10.222.50.39:9898/env |grep VERSION
  "VERSION=v2.0.0",
  "VERSION=v2.0.0",
  "VERSION=v2.0.0",
  [...]

查看pod现在v1版本的pod和v2版本的pod是同时存在的, 清理v1的pod

$ kubectl get pods  -l app=backend
NAME                          READY   STATUS    RESTARTS   AGE
backend-v1-76bdc697d7-4nc89   1/1     Running   0          156m
backend-v1-76bdc697d7-lvn6n   1/1     Running   0          156m
backend-v1-76bdc697d7-vkf2z   1/1     Running   0          156m
backend-v2-78f8b89f4-hl4qr    1/1     Running   0          9m28s
backend-v2-78f8b89f4-kshrg    1/1     Running   0          9m28s
backend-v2-78f8b89f4-s2w77    1/1     Running   0          9m28s

$ kubect delete deployment backend-v1

4.Cleanup

$ kubectl delete all -l app=backend

金丝雀(canary)

金丝雀发布还有一种说法叫灰度发布. 是指在原有版本可用的情况下,同时部署一个新版本做为金丝雀, 测试新版本的性能和表现,以保障系统稳定的前提下尽早的发现问题和处理问题

金丝雀发布的简短流程如下:

  • v1对外提供服务

  • 部署v2

  • 创建对应canary的ingress规则, 分部分流量到canary的实例上

  • 确定canary可以正常处理请求流量并没有抛异常

  • 将所有流量切到v2版本上

  • 关闭v1的服务

比较主流的ingress controller目前都支持canary也可以使用istio本文使用的traefik

1.部署v1

v1.yaml

apiVersion: v1
kind: Service
metadata:
  name: backend-v1
  labels:
    app: backend
spec:
  type: ClusterIP
  selector:
    app: backend
    version: v1.0.0
  ports:
    - name: http
      port: 9898
      protocol: TCP
      targetPort: http
    - port: 9999
      targetPort: grpc
      protocol: TCP
      name: grpc
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-v1
  labels:
    app: backend
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app: backend
      version: v1.0.0
  template:
    metadata:
      labels:
        app: backend
        version: v1.0.0
    spec:
      containers:
      - name: backend-v1
        image: stefanprodan/podinfo:3.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9898
          protocol: TCP
        - name: http-metrics
          containerPort: 9797
          protocol: TCP
        - name: grpc
          containerPort: 9999
          protocol: TCP
        command:
        - ./podinfo
        - --port=9898
        - --port-metrics=9797
        - --grpc-port=9999
        - --grpc-service-name=backend
        - --level=info
        env:
        - name: PODINFO_UI_COLOR
          value: "#34577c"
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/healthz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/readyz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        resources:
          limits:
            cpu: 2000m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 32Mi

执行部署

$ kubectl apply -f v1.yaml
service/backend-v1 configured
deployment.apps/backend-v1 configured

2.部署v2

v2.yaml

apiVersion: v1
kind: Service
metadata:
  name: backend-v2
  labels:
    app: backend
spec:
  type: ClusterIP
  selector:
    app: backend
    version: v2.0.0
  ports:
    - name: http
      port: 9898
      protocol: TCP
      targetPort: http
    - port: 9999
      targetPort: grpc
      protocol: TCP
      name: grpc
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-v2
  labels:
    app: backend
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app: backend
      version: v2.0.0
  template:
    metadata:
      labels:
        app: backend
        version: v2.0.0
    spec:
      containers:
      - name: backend-v2
        image: stefanprodan/podinfo:3.2.3
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9898
          protocol: TCP
        - name: http-metrics
          containerPort: 9797
          protocol: TCP
        - name: grpc
          containerPort: 9999
          protocol: TCP
        command:
        - ./podinfo
        - --port=9898
        - --port-metrics=9797
        - --grpc-port=9999
        - --grpc-service-name=backend
        - --level=info
        env:
        - name: PODINFO_UI_COLOR
          value: "#34577c"
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/healthz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/readyz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        resources:
          limits:
            cpu: 2000m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 32Mi

执行部署

$ kubectl apply -f v2.yaml
service/backend-v1 configured
deployment.apps/backend-v1 configured

3.创建canary ingress

因为v2版本打入的流量太少不好测试所以权重设置为v1和v2各50%

ingress-canary.yaml

---
    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
      name: bakcend
      labels:
        app: backend
        version: v2.0.0
        traffic-type: devops
      annotations:
        kubernetes.io/ingress.class: "traefik"
        traefik.ingress.kubernetes.io/service-weights: |
          backend-v2: 50%
          backend-v1: 50%
    spec:
      rules:
      - host: canary-test.com
        http:
          paths:
          - backend:
              serviceName: backend-v2
              servicePort: http
            path: /
          - backend:
              serviceName: backend-v1
              servicePort: http
            path: /

测试

$ while sleep 0.1
do
curl -s http://canary-test.com/env  |grep VERSION
done
  [...]
  "VERSION=v1.0.0",
  "VERSION=v2.0.0",
  "VERSION=v2.0.0",
  "VERSION=v2.0.0",
  "VERSION=v1.0.0",
  "VERSION=v1.0.0",
  [...]

4.切换流量到v2版本

ingress.yaml

---
    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
      name: bakcend
      labels:
        app: backend
        version: v2.0.0
        traffic-type: devops
      annotations:
        kubernetes.io/ingress.class: "traefik"
        traefik.ingress.kubernetes.io/service-weights: |
          backend-v2: 100%
          backend-v1: 0%
    spec:
      rules:
      - host: canary-test.com
        http:
          paths:
          - backend:
              serviceName: backend-v2
              servicePort: http
            path: /
          - backend:
              serviceName: backend-v1
              servicePort: http
            path: /

部署

➜ kubectl apply -f ingress.yaml
ingress.extensions/bakcend configured

测试

➜  canary while sleep 0.1
do
curl -s http://canary-test.com/env  |grep VERSION
done
  [...]
  "VERSION=v2.0.0",
  "VERSION=v2.0.0",
  "VERSION=v2.0.0",
  [...]

5.Cleanup

$ kubectl delete all -l app=backend

参考

kubernetes-deployment-strategies

最后更新于