前言

上篇笔记我们学习了管理有状态应用的对象 StatefulSet,再加上管理无状态应用的 Deployment 和 DaemonSet,我们就能在 Kubernetes 里部署任意形式的应用了。

只是把应用发布到集群里是远远不够的,要让应用稳定可靠地运行,还需要有持续的运维工作。

【k8s】Deployment让应用永不宕机(八) 里,我们学过 Deployment 的应用伸缩功能就是一种常见的运维操作,在 Kubernetes 里,使用命令 kubectl scale,我们就可以轻松调整 Deployment 下属的 Pod 数量,因为 StatefulSet 是 Deployment 的一种特例,所以它也可以使用 kubectl scale 来实现应用伸缩

除了应用伸缩,其他的运维操作比如应用更新、版本回退等工作,该怎么做呢?这些也是我们日常运维中经常会遇到的问题。

今天就以 Deployment 为例,来讲讲 Kubernetes 在应用管理方面的高级操作:滚动更新,使用 kubectl rollout 实现用户无感知的应用升级和降级。

一、Kubernetes 应用版本更新

应用的版本更新,大家都知道是怎么回事,比如我们发布了 V1 版,过了几天加了新功能,要发布V2 版。

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

Kubernetes 里的应用都是以 Pod 的形式运行的,而 Pod 通常又会被 Deployment 等对象来管理,所以应用的版本更新实际上更新的是整个 Pod。

Pod 是由 YAML 描述文件来确定的,更准确地说,是 Deployment 等对象里的字段 template。

所以,在 Kubernetes 里应用的版本变化就是 template 里 Pod 的变化,哪怕 template 里只变动了一个字段,那也会形成一个新的版本,也算是版本变化。

Kubernetes 使用了摘要功能,用摘要算法计算 template 的 Hash 值作为版本号,虽然不太方便识别,但是很实用。

来看一个例子:
在这里插入图片描述

Pod 名字里的那串随机数676475ddfd就是 Pod 模板的 Hash 值,也就是 Pod 的版本号

如果变动了 Pod YAML 描述,比如把镜像改成 nginx:stable-alpine,或者把容器名字改成 nginx-test,都会生成一个新的应用版本,kubectl apply 后就会重新创建 Pod
在这里插入图片描述
你可以看到,Pod 名字里的 Hash 值变成了56b6b4f4c9,这就表示 Pod 的版本更新了。

二、Kubernetes 实现应用更新

为了更仔细地研究 Kubernetes 的应用更新过程,这边略微改造一下 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';
      }
    }

然后我们修改 Pod 镜像,明确地指定版本号是 1.21-alpine,实例数设置为 4 个:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  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

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

kubectl apply -f ngx-v1.yml

我们还可以为它创建 Service 对象,利用NodePort类型进行转发:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: ngx-dep
  name: ngx-svc
spec:
  type: NodePort
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: ngx-dep

在这里插入图片描述

curl 127.1:30657

在这里插入图片描述

从 curl 命令的输出中可以看到,现在应用的版本是 1.21.6。现在,让我们编写一个新版本对象 ngx-v2.yml,把镜像升级到 nginx:1.22-alpine,其他的都不变。

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

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  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

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

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

在这里插入图片描述
更新完成后,你再执行 kubectl get pod,就会看到 Pod 已经全部替换成了新版本“7cf8……”,用 curl 访问 Nginx,输出信息也变成了“1.22.0”:
在这里插入图片描述
在这里插入图片描述
仔细查看 kubectl rollout status 的输出信息,你可以发现,Kubernetes 不是把旧 Pod 全部销毁再一次性创建出新 Pod,而是在逐个地创建新 Pod,同时也在销毁旧 Pod,保证系统里始终有足够数量的 Pod 在运行,不会有空窗期中断服务。

新 Pod 数量增加的过程有点像是滚雪球,从零开始,越滚越大,所以这就是所谓的滚动更新(rolling update)。

使用命令 kubectl describe 可以更清楚地看到 Pod 的变化情况:

kubectl describe deploy ngx-dep

在这里插入图片描述

  • 一开始的时候 V1 Pod(即 ngx-dep-676475ddfd)的数量是 4;
  • 当“滚动更新”开始的时候,Kubernetes 创建 1 个 V2 Pod(即 ngx-dep-7cf8546bf9),并且把 V1 Pod 数量减少到 3;
  • 接着再增加 V2 Pod 的数量到 2,同时 V1 Pod 的数量变成了 2 ;
  • 最后 V2 Pod 的数量达到预期值 4,V1 Pod 的数量变成了 0,整个更新过程就结束了

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

滚动更新的过程画了一张图,可以参考它来进一步体会:
在这里插入图片描述

三、Kubernetes如何管理应用更新

Kubernetes 的滚动更新功能确实非常方便,不需要任何人工干预就能简单地把应用升级到新版本,也不会中断服务,不过如果更新过程中发生了错误或者更新后发现有 Bug 该怎么办呢?

要解决这两个问题,我们还是要用 kubectl rollout 命令。

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

注意的是它们只支持 Deployment,不能用在 DaemonSet、StatefulSet 上(最新的 1.24 支持了 StatefulSet 的滚动更新)。

对于更新后出现的问题,Kubernetes 为我们提供了后悔药,也就是更新历史,你可以查看之前的每次更新记录,并且回退到任何位置,和我们开发常用的 Git 等版本控制软件非常类似。

查看更新历史使用的命令是 kubectl rollout history

kubectl rollout history deploy ngx-dep
[root@k8s-console nginx]# kubectl rollout history deploy ngx-dep
deployment.apps/ngx-dep 
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

它会输出一个版本列表,因为我们创建 Nginx Deployment 是一个版本,更新又是一个版本,所以这里就会有两条历史记录。

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

kubectl rollout history deploy --revision=2
[root@k8s-console nginx]# kubectl rollout history deploy --revision=2
deployment.apps/ngx-dep with revision #2
Pod Template:
  Labels:	app=ngx-dep
	pod-template-hash=7cf8546bf9
  Containers:
   nginx:
    Image:	nginx:1.22-alpine
    Port:	80/TCP
    Host Port:	0/TCP
    Environment:	<none>
    Mounts:
      /etc/nginx/conf.d/ from ngx-conf-vol (rw)
  Volumes:
   ngx-conf-vol:
    Type:	ConfigMap (a volume populated by a ConfigMap)
    Name:	ngx-conf
    Optional:	false

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

[root@k8s-console nginx]# kubectl rollout history deploy ngx-dep
deployment.apps/ngx-dep 
REVISION  CHANGE-CAUSE
2         <none>
3         <none>

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

这个 V2 到 V1 的版本降级的过程同样画了一张图,它和从 V1 到 V2 的版本升级过程是完全一样的,不同的只是版本号的变化方向:

在这里插入图片描述

四、Kubernetes 添加更新描述

kubectl rollout history 的版本列表好像有点太简单了呢?只有一个版本更新序号,而另一列 CHANGE-CAUSE 为什么总是显示成 呢?能不能像 Git 一样,每次更新也加上说明信息呢?

这当然是可以的,做法也很简单,我们只需要在 Deployment 的 metadata 里加上一个新的字段 annotations

annotations 字段的含义是注解``注释,形式上和 labels 一样,都是 Key-Value,也都是给 API 对象附加一些额外的信息,但是用途上区别很大。

  • annotations 添加的信息一般是给 Kubernetes 内部的各种对象使用的,有点像是扩展属性
  • labels 主要面对的是 Kubernetes 外部的用户,用来筛选、过滤对象的。

如果用一个简单的比喻来说呢,annotations 就是包装盒里的产品说明书,而 labels 是包装盒外的标签贴纸。

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

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

下面来操作一下,我们创建 3 个版本的 Nginx 应用,同时添加更新说明:
ngx-v1

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: v1, nginx=1.21
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

ngx-v2

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: update to v2, ngx=1.22
spec:
  minReadySeconds: 15
  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

ngx-v3

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: update to v3, change name
spec:
  minReadySeconds: 15
  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-3
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx/conf.d/
          name: ngx-conf-vol

你需要注意 YAML 里的 metadata 部分,使用 annotations.kubernetes.io/change-cause 描述了版本更新的情况,相比 kubectl rollout history --revision 的罗列大量信息更容易理解。

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

kubectl rollout history deployment ngx-dep

在这里插入图片描述
这次显示的列表信息就好看多了,每个版本的主要变动情况列得非常清楚,和 Git 版本管理的感觉很像。

总结

  1. 在 Kubernetes 里应用的版本不仅仅是容器镜像,而是整个 Pod 模板,为了便于处理使用了摘要算法,计算模板的 Hash 值作为版本号。
  2. Kubernetes 更新应用采用的是滚动更新策略,减少旧版本 Pod 的同时增加新版本 Pod,保证在更新过程中服务始终可用。
  3. 管理应用更新使用的命令是 kubectl rollout,子命令有 status、history、undo 等。
  4. Kubernetes 会记录应用的更新历史,可以使用 history --revision 查看每个版本的详细信息,也可以在每次更新时添加注解 kubernetes.io/change-cause。

另外,在 Deployment 里还有其他一些字段可以对滚动更新的过程做更细致的控制,它们都在 spec.strategy.rollingUpdate 里,比如 maxSurge、maxUnavailable 等字段,分别控制最多新增 Pod 数和最多不可用 Pod 数,一般用默认值就足够了,你如果感兴趣也可以查看 Kubernetes 文档进一步研究

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐