2.16 滚动更新

2.16 滚动更新 #

在实际生产环境中,只是把应用发布到集群里是远远不够的,要让应用稳定可靠地运行,还需要有持续的运维工作。比如 Deployment 的 “应用伸缩” 功能就是一种常见的运维操作,在 Kubernetes 里,使用命令 kubectl scale,可以轻松调整 Deployment 下属的 Pod 数量。除了 “应用伸缩”,其他的运维操作比如应用更新、版本回退等工作也是日常运维中经常会遇到的问题。

2.16.1 应用版本 #

版本更新实际做起来是一个相当棘手的事。因为系统已经上线运行,必须要保证不间断地对外提供服务。尤其在特殊时候可能需要开发、测试、运维、监控、网络等各个部门的一大堆人来协同工作,费时又费力。

在 Kubernetes 里,版本更新使用的不是 API 对象,而是两个命令:kubectl apply 和 kubectl rollout,需要搭配部署应用所需要的 Deployment、DaemonSet 等 YAML 文件。

在 Kubernetes 里应用都是以 Pod 的形式运行的,而 Pod 通常又会被 Deployment 等对象来管理,所以应用的 “版本更新” 实际上更新的是整个 Pod。Pod 是由 YAML 描述文件来确定的,是 Deployment 等对象里的字段 template。所以,在 Kubernetes 里应用的版本变化就是 template 里 Pod 的变化,哪怕 template 里只变动了一个字段,那也会形成一个新的版本,也算是版本变化。但在 template 里的内容太多了,拿这么长的字符串来当做 “版本号” 不太现实,所以 Kubernetes 就使用了 “摘要” 功能,用摘要算法计算 template 的 Hash 值作为 “版本号”。

比如通过以下的 YAML 描述文件生成的 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  name: ngx-dep

spec:
  replicas: 2
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      containers:
        - image: nginx:alpine
          name: nginx

Pod 名字里的那串随机数 “bfbb5f64b” 就是 Pod 模板的 Hash 值,也就是 Pod 的 “版本号”。

如果变动了 Pod YAML 描述,比如把镜像改成 nginx:stable-alpine,或者把容器名字改成 nginx-test,都会生成一个新的应用版本,kubectl apply 后就会重新创建 Pod:

可以看到,Pod 名字里的 Hash 值变成了 “c98cdf864”,这就表示 Pod 的版本更新了。

2.16.2 如何实现应用更新 #

可以用一个 Nginx Deployment 对象,看看 Kubernetes 到底是怎么实现版本更新的。

以下是一个 ConfigMap,让它输出 Nginx 的版本号,方便用 curl 查看版本:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ngx-conf

data:
  default.conf: |
    server {
      listen 80;
      location / {
        default_type text/plain;
        return 200
          'ver : $nginx_version\nsrv : $server_addr:$server_port\nhost: $hostname\n';
      }
    }    

以下是一个 Deployment YAML 描述,指定 Nginx 版本号是 1.21-alpine,实例数设置为 4 个:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep

spec:
  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
        - name: ngx-conf-vol
          configMap:
            name: ngx-conf

      containers:
        - image: nginx:1.21-alpine
          name: nginx
          ports:
            - containerPort: 80

          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: ngx-conf-vol

把这个 YAML 命名为 ngx-v1.yml,然后执行命令 kubectl apply 部署这个应用:

为这个 Deployment 创建 Service 对象,用 kubectl port-forward 转发请求来查看状态,以下是 Service 的 YAML 描述:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  selector:
    app: ngx-dep

  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
kubectl port-forward svc/ngx-svc 8080:80 &
curl 127.1:8080

从 curl 命令的输出中可以看到,现在应用的版本是 1.21.x。

现在,编写一个新版本对象 ngx-v2.yml,把镜像升级到 nginx:1.22-alpine,其他的都不变。

因为 Kubernetes 的动作太快了,为了能够观察到应用更新的过程,还需要添加一个字段 minReadySeconds,让 Kubernetes 在更新过程中等待一点时间,确认 Pod 没问题才继续其余 Pod 的创建工作。以下是新版本的 YAML 描述:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep

spec:
  minReadySeconds: 15 # 确认Pod就绪的等待时间
  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
        - name: ngx-conf-vol
          configMap:
            name: ngx-conf

      containers:
        - image: nginx:1.22-alpine
          name: nginx
          ports:
            - containerPort: 80

          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: ngx-conf-vol

现在执行命令 kubectl apply 来更新应用,因为改动了镜像名,Pod 模板变了,就会触发 “版本更新”,然后用一个新命令:kubectl rollout status,来查看应用更新的状态:

kubectl apply -f ngx-v2.yml
kubectl rollout status deployment ngx-dep

从 kubectl rollout status 的输出信息,可以发现,Kubernetes 不是把旧 Pod 全部销毁再一次性创建出新 Pod,而是在逐个地创建新 Pod,同时也在销毁旧 Pod,保证系统里始终有足够数量的 Pod 在运行。新 Pod 数量增加的过程有点像是 “滚雪球”,从零开始,越滚越大,也就是所谓的 “滚动更新”(rolling update)。

其实 “滚动更新” 就是由 Deployment 控制的两个同步进行的 “应用伸缩” 操作,老版本缩容到 0,同时新版本扩容到指定值,是一个 “此消彼长” 的过程。

更新完成后,再执行 kubectl get pod,就会看到 Pod 已经全部替换成了新版本 “d575d5776”,用 curl 访问 Nginx,输出的版本信息也变成了 1.22.x:

滚动更新的过程可以用下面的这张图体下:

2.16.3 管理应用更新 #

如果更新过程中发生了错误或者更新后发现有 Bug,可以使用 kubectl rollout 命令。

在应用更新的过程中,可以随时使用 kubectl rollout pause 来暂停更新,检查、修改 Pod,或者测试验证,如果确认没问题,再用 kubectl rollout resume 来继续更新。

对于更新后出现的问题,可以查看之前的每次更新记录,并且回退到任何位置,和 Git 等版本控制软件非常类似。查看更新历史使用的命令是 kubectl rollout history,如:

kubectl rollout history deploy ngx-dep

kubectl rollout history 的列表输出的有用信息太少,可以在命令后加上参数 --revision 来查看每个版本的详细信息,包括标签、镜像名、环境变量、存储卷等等,通过这些就可以大致了解每次都变动了哪些关键字段:

kubectl rollout history deploy ngx-dep --revision=1

假设认为刚刚更新的 nginx:1.22-alpine 不好,想要回退到上一个版本,可以使用命令 kubectl rollout undo,也可以加上参数 --to-revision 回退到任意一个历史版本:

kubectl rollout undo 的操作过程其实和 kubectl apply 是一样的,执行的仍然是 “滚动更新”,只不过使用的是旧版本 Pod 模板,把新版本 Pod 数量收缩到 0,同时把老版本 Pod 扩展到指定值。

这个 V2 到 V1 的 “版本降级” 的变化过程如下图所示:

1.16.4 添加更新描述 #

kubectl rollout history 的版本列表 CHANGE-CAUSE 列可以添加说明信息,当没有添加说明信息时显示

当需要添加说明信息时,需要在 Deployment 的 metadata 里加上一个新的字段 annotations。annotations 字段的含义是 “注解” “注释”,形式上和 labels 一样,都是 Key-Value,也都是给 API 对象附加一些额外的信息,但是用途上区别很大。

  • annotations 添加的信息一般是给 Kubernetes 内部的各种对象使用的,有点像是 “扩展属性”;

  • labels 主要面对的是 Kubernetes 外部的用户,用来筛选、过滤对象的。

借助 annotations,Kubernetes 既不破坏对象的结构,也不用新增字段,就能够给 API 对象添加任意的附加信息,这就是面向对象设计中典型的 OCP “开闭原则”,让对象更具扩展性和灵活性。

annotations 里的值可以任意写,Kubernetes 会自动忽略不理解的 Key-Value,但要编写更新说明就需要使用特定的字段 kubernetes.io/change-cause

这里有 3 个版本的 Nginx 应用,同时添加更新说明:

ngx1.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: v1, ngx=1.21

spec:
  minReadySeconds: 15 # 确认Pod就绪的等待时间
  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
        - name: ngx-conf-vol
          configMap:
            name: ngx-conf

      containers:
        - image: nginx:1.21-alpine
          name: nginx
          ports:
            - containerPort: 80

          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: ngx-conf-vol

ngx2.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: update to v2, ngx=1.22

spec:
  minReadySeconds: 15 # 确认Pod就绪的等待时间
  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
        - name: ngx-conf-vol
          configMap:
            name: ngx-conf

      containers:
        - image: nginx:1.22-alpine
          name: nginx
          ports:
            - containerPort: 80

          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: ngx-conf-vol

ngx3.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: update to v3, change name

spec:
  minReadySeconds: 15 # 确认Pod就绪的等待时间
  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
        - name: ngx-conf-vol
          configMap:
            name: ngx-conf

      containers:
        - image: nginx:1.22-alpine
          name: nginx
          ports:
            - containerPort: 80

          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: ngx-conf-vol

依次使用 kubectl apply 创建并更新对象之后,再用 kubectl rollout history 来看一下更新历史:

这次显示的列表信息,每个版本的主要变动情况列得非常清楚,和 Git 版本管理的感觉很像。

Kubernetes 不会记录所有的更新历史,那样太浪费资源,默认它只会保留最近的 10 次操作,但这个值可以用字段 “revisionHistoryLimit” 调整。

参考 #