Deployment是什么?

Deployment是kube-controller-manager 这个组件下其中的一种控制器,我们可以看一下:

$ cd kubernetes/pkg/controller/
$ ls -d */
deployment/             job/                    podautoscaler/
cloud/                  disruption/             namespace/
replicaset/             serviceaccount/         volume/
cronjob/                garbagecollector/       nodelifecycle/          replication/            statefulset/            daemon/
...

在上面目录下面的每一个控制器,都以独有的方式负责某种编排功能。这些控制器之所以被统一放在 pkg/controller 目录下,就是因为它们都遵循 Kubernetes 项目中的一个通用编排模式,即:控制循环(control loop)。

以之前的一个例子为例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.18.0
        ports:
        - containerPort: 80

这个 Deployment 定义的编排动作非常简单,即:确保携带了 app=nginx 标签的 Pod 的个数,永远等于 spec.replicas 指定的个数,即 3 个。

这就意味着,如果在这个集群中,携带 app=nginx 标签的 Pod 的个数大于 3 的时候,就会有旧的 Pod 被删除;反之,就会有新的 Pod 被创建。

简单描述一下它对控制器模型的实现:

  1. Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;

  2. Deployment 对象的 Replicas 字段的值就是期望状态;

  3. Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod

这种操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。其实也就是控制循环。

其中,这个控制器对象本身,负责定义被管理对象的期望状态。比如,Deployment 里的 replicas=2 这个字段。

而被控制对象的定义,则来自于一个“模板”。比如,Deployment 里的 template 字段。字段里的内容,跟一个标准的 Pod 对象的 API 定义,丝毫不差。而所有被这个 Deployment 管理的 Pod 实例,其实都是根据这个 template 字段的内容创建出来的。所以像 Deployment 定义的 template 字段,在 Kubernetes 项目中有一个专有的名字,叫作 PodTemplate(Pod 模板)。

至此,我们就可以对 Deployment 以及其他类似的控制器,做一个简单总结了:

Deployment能做什么?

它能实现了 Kubernetes 项目中一个非常重要的功能:Pod 的“水平扩展 / 收缩”(horizontal scaling out/in)。这个功能,是从 PaaS 时代开始,一个平台级项目就必须具备的编排能力。

举个例子,如果你更新了 Deployment 的 Pod 模板(比如,修改了容器的镜像),那么 Deployment 就需要遵循一种叫作“滚动更新”(rolling update)的方式,来升级现有的容器。

而这个能力的实现,依赖的是 Kubernetes 项目中的一个非常重要的概念(API 对象):ReplicaSet。

ReplicaSet 的结构非常简单,我们可以通过这个 YAML 文件查看一下:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-set
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.18.0

从这个 YAML 文件中,我们可以看到,一个 ReplicaSet 对象,其实就是由副本数目的定义和一个 Pod 模板组成的。不难发现,它的定义其实是 Deployment 的一个子集。

更重要的是,Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。

所以 Deployment,与 ReplicaSet,以及 Pod 的关系可以用一张图来说明:

其中,ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数(比如,3 个)。这也正是 Deployment 只允许容器的 restartPolicy=Always 的主要原因:只有在容器能保证自己始终是 Running 状态的前提下,ReplicaSet 调整 Pod 的个数才有意义。

而在此基础上,Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。

我们先创建这个 nginx-deployment:

# --record的作用,是记录下你每次操作所执行的命令,以方便后面查看。
[root@kubeadm ~]# kubectl create -f nginx-deployment.yaml --record
[root@kubeadm ~]# kubectl get deployments
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           3m5s

在返回结果中,我们可以看到三个状态字段,它们的含义如下所示。

  • READY:准备状态的 Pod 副本个数;

  • UP-TO-DATE:当前处于最新版本的 Pod 的个数,所谓最新版本指的是 Pod 的 Spec 部分与 Deployment 里 Pod 模板里定义的完全一致;

  • AVAILABLE:当前已经可用的 Pod 的个数,即:既是 Running 状态,又是最新版本,并且已经处于 Ready(健康检查正确)状态的 Pod 的个数。这个字段,描述的才是用户所期望的最终状态。

水平扩展 & 水平收缩
[root@kubeadm ~]# kubectl scale deployment nginx-deployment --replicas 4
deployment.apps/nginx-deployment scaled
[root@kubeadm ~]# kubectl rollout status deployment/nginx-deployment   #实时查看 Deployment 对象的状态变化
deployment "nginx-deployment" successfully rolled out
[root@kubeadm ~]# kubectl get deployments
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   4/4     4            4           17m

可以尝试查看一下这个 Deployment 所控制的 ReplicaSet:

[root@kubeadm ~]# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-75ddd4d4b4   4         4         4       20m
  • NAME:是由 Deployment 的名字和一个随机字符串共同组成。这个随机字符串叫作 pod-template-hash,ReplicaSet 会把这个随机字符串加在它所控制的所有 Pod 的标签里,从而保证这些 Pod 不会与集群里的其他 Pod 混淆。

  • DESIRED:用户期望的 Pod 副本个数(spec.replicas 的值);

  • CURRENT:当前处于 Running 状态的 Pod 的个数;

滚动更新

如果我们修改了 Deployment 的 Pod 模板,“滚动更新”就会被自动触发。这里我们可以直接使用 kubectl edit 指令编辑 Etcd 里的 API 对象。(kubectl edit 原理:把 API 对象的内容下载到了本地文件,让你修改完成后再提交上去。)

[root@kubeadm ~]# kubectl edit deployment/nginx-deployment
... 
    spec:
      containers:
      - name: nginx
        image: nginx:1.19.0          # 1.18.0 -> 1.19.0
        ports:
        - containerPort: 80
...
deployment.extensions/nginx-deployment edited

kubectl edit 指令编辑完成后,保存退出,Kubernetes 就会立刻触发“滚动更新”的过程。可以通过 kubectl rollout status 指令查看 nginx-deployment 的状态变化:

[root@kubeadm ~]# kubectl rollout status deployment/nginx-deployment
Waiting for deployment "nginx-deployment" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "nginx-deployment" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "nginx-deployment" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "nginx-deployment" rollout to finish: 3 of 4 updated replicas are available...
deployment "nginx-deployment" successfully rolled out

这时,你可以通过查看 Deployment 的 Events,看到这个“滚动更新”的流程:

[root@kubeadm ~]# kubectl describe deployment nginx-deployment
.....
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  7m49s  deployment-controller  Scaled up replica set nginx-deployment-7b446869f to 1
  Normal  ScalingReplicaSet  7m49s  deployment-controller  Scaled down replica set nginx-deployment-75ddd4d4b4 to 3
  Normal  ScalingReplicaSet  7m49s  deployment-controller  Scaled up replica set nginx-deployment-7b446869f to 2
  Normal  ScalingReplicaSet  7m47s  deployment-controller  Scaled down replica set nginx-deployment-75ddd4d4b4 to 2
  Normal  ScalingReplicaSet  7m47s  deployment-controller  Scaled up replica set nginx-deployment-7b446869f to 3
  Normal  ScalingReplicaSet  7m45s  deployment-controller  Scaled down replica set nginx-deployment-75ddd4d4b4 to 1
  Normal  ScalingReplicaSet  7m45s  deployment-controller  Scaled up replica set nginx-deployment-7b446869f to 4
  Normal  ScalingReplicaSet  6m49s  deployment-controller  Scaled down replica set nginx-deployment-75ddd4d4b4 to 0

新 ReplicaSet 管理的 Pod 副本数,从 0 个变成 1 个,再变成 2 个,最后变成 4 个。而旧的 ReplicaSet 管理的 Pod 副本数则从 4 个变成 3 个,再变成 2 个,最后变成 0 个。这样,就完成了这一组 Pod 的版本升级过程。

在这个“滚动更新”过程完成之后,你可以查看一下新、旧两个 ReplicaSet 的最终状态:

[root@kubeadm ~]# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-75ddd4d4b4   0         0         0       47m
nginx-deployment-7b446869f    4         4         4       12m

当然,你也可以把文件下载下来修改,例如:

#下载
[root@kubeadm ~]# kubectl get deployment nginx-deployment -o yaml >nginx-deployment.yaml
#修改后更新
[root@kubeadm ~]# kubectl replace -f nginx-deployment.yaml
这种“滚动更新”的有什么好处?

比如,在升级刚开始的时候,集群里只有 1 个新版本的 Pod。如果这时,新版本 Pod 有问题启动不起来,那么“滚动更新”就会停止,从而允许开发和运维人员介入。而在这个过程中,由于应用本身还有两个旧版本的 Pod 在线,所以服务并不会受到太大的影响。

当然,这也就要求你一定要使用 Pod 的 Health Check 机制检查应用的运行状态,而不是简单地依赖于容器的 Running 状态。要不然的话,虽然容器已经变成 Running 了,但服务很有可能尚未启动,“滚动更新”的效果也就达不到了。

而为了进一步保证服务的连续性,Deployment Controller 还会确保,在任何时间窗口内,只有指定比例的 Pod 处于离线状态。同时,它也会确保,在任何时间窗口内,只有指定比例的新 Pod 被创建出来。这两个比例的值都是可以配置的,默认都是 DESIRED 值的 25%。

所以,在上面这个 Deployment 的例子中,它有 3 个 Pod 副本,那么控制器在“滚动更新”的过程中永远都会确保至少有 2 个 Pod 处于可用状态,至多只有 4 个 Pod 同时存在于集群中。这个策略,是 Deployment 对象的一个字段,名叫 RollingUpdateStrategy,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
...
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1

在上面这个 RollingUpdateStrategy 的配置中,maxSurge 指定的是除了 DESIRED 数量之外,在一次“滚动”中,Deployment 控制器还可以创建多少个新 Pod;而 maxUnavailable 指的是,在一次“滚动”中,Deployment 控制器可以删除多少个旧 Pod。

结合以上讲述,现在我们可以扩展一下 Deployment、ReplicaSet 和 Pod 的关系图了。

如上所示,Deployment 的控制器,实际上控制的是 ReplicaSet 的数目,以及每个 ReplicaSet 的属性。而一个应用的版本,对应的正是一个 ReplicaSet;

这个版本应用的 Pod 数量,则由 ReplicaSet 通过它自己的控制器(ReplicaSet Controller)来保证。

通过这样的多个 ReplicaSet 对象,Kubernetes 项目就实现了对多个“应用版本”的描述。

通过kubectl set image 的指令,直接修改 nginx-deployment 所使用的镜像。

[root@kubeadm ~]# kubectl set image deployment/nginx-deployment nginx=nginx:1.25.0
deployment.apps/nginx-deployment image updated
[root@kubeadm ~]# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-75ddd4d4b4   0         0         0       65m
nginx-deployment-789f4df7fb   2         2         0       6s
nginx-deployment-7b446869f    3         3         3       31m

由于这个 nginx:1.25.0 镜像在 Docker Hub 中并不存在,所以这个 Deployment 的“滚动更新”被触发后,会立刻报错并停止。

通过 get rs 返回结果,我们可以看到,新版本的 ReplicaSet(hash=789f4df7fb)的“水平扩展”已经停止。而且此时,它已经创建了两个 Pod,但是它们都没有进入 READY 状态。这当然是因为这两个 Pod 都拉取不到有效的镜像。

与此同时,旧版本的 ReplicaSet(hash=7b446869f)的“水平收缩”,也自动停止了。此时,已经有 1 个旧 Pod 被删除,还剩下 3 个旧 Pod。

那么问题来了, 我们如何让这个 Deployment 的 4 个 Pod,都回滚到以前的旧版本呢?

只需要执行一条 kubectl rollout undo 命令,就能把整个 Deployment 回滚到上一个版本:

[root@kubeadm ~]# kubectl rollout undo deployment/nginx-deployment
deployment.apps/nginx-deployment rolled back
[root@kubeadm ~]# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-75ddd4d4b4   0         0         0       71m
nginx-deployment-789f4df7fb   0         0         0       5m49s
nginx-deployment-7b446869f    4         4         4       36m
更进一步地,如果我想回滚到更早之前的版本,要怎么办呢?

首先,我需要使用 kubectl rollout history 命令,查看每次 Deployment 变更对应的版本。而由于我们在创建这个 Deployment 的时候,指定了–record 参数,所以我们创建这些版本时执行的 kubectl 命令,都会被记录下来。这个操作的输出如下所示:

[root@kubeadm ~]# kubectl rollout history deployment/nginx-deployment
deployments "nginx-deployment"
REVISION    CHANGE-CAUSE
1           kubectl create -f nginx-deployment.yaml --record
2           kubectl edit deployment/nginx-deployment
3           kubectl set image deployment/nginx-deployment nginx=nginx:1.25.0

然后,我们就可以在 kubectl rollout undo 命令行最后,加上要回滚到的指定版本的版本号,就可以回滚到指定版本了。这个指令的用法如下:

[root@kubeadm ~]# kubectl rollout undo deployment/nginx-deployment --to-revision=2
deployment.extensions/nginx-deployment
不过,我们对 Deployment 进行的每一次更新操作,都会生成一个新的 ReplicaSet 对象,是不是有些多余,甚至浪费资源呢?

所以,Kubernetes 项目还提供了一个指令,使得我们对 Deployment 的多次更新操作,最后 只生成一个 ReplicaSet。

具体的做法是,在更新 Deployment 前,你要先执行一条 kubectl rollout pause 指令。它的用法如下所示:

[root@kubeadm ~]# kubectl rollout pause deployment/nginx-deployment
deployment.apps/nginx-deployment paused

这个 kubectl rollout pause 的作用,是让这个 Deployment 进入了一个“暂停”状态。

所以接下来,你就可以随意使用 kubectl edit 或者 kubectl set image 指令,修改这个 Deployment 的内容了。

由于此时 Deployment 正处于“暂停”状态,所以我们对 Deployment 的所有修改,都不会触发新的“滚动更新”,也不会创建新的 ReplicaSet。

而等到我们对 Deployment 修改操作都完成之后,只需要再执行一条 kubectl rollout resume 指令,就可以把这个 Deployment“恢复”回来,如下所示:

[root@kubeadm ~]# kubectl set image deployment/nginx-deployment nginx=nginx:1.17.0   
deployment.apps/nginx-deployment image updated
[root@kubeadm ~]# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-75ddd4d4b4   0         0         0       82m
nginx-deployment-789f4df7fb   0         0         0       17m
nginx-deployment-7b446869f    4         4         4       48m
[root@kubeadm ~]# kubectl set image deployment/nginx-deployment nginx=nginx:1.18.0
deployment.apps/nginx-deployment image updated
[root@kubeadm ~]# kubectl rollout resume deployment/nginx-deployment
deployment.apps/nginx-deployment resumed
[root@kubeadm ~]# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-75ddd4d4b4   3         3         1       84m
nginx-deployment-789f4df7fb   0         0         0       18m
nginx-deployment-7b446869f    2         2         2       49m

不过,即使你像上面这样小心翼翼地控制了 ReplicaSet 的生成数量,随着应用版本的不断增加,Kubernetes 中还是会为同一个 Deployment 保存很多很多不同的 ReplicaSet。

那么,我们又该如何控制这些“历史”ReplicaSet 的数量呢?

很简单,Deployment 对象有一个字段,叫作 spec.revisionHistoryLimit,就是 Kubernetes 为 Deployment 保留的“历史版本”个数。所以,如果把它设置为 0,你就再也不能做回滚操作了。

文档更新时间: 2020-08-06 18:53   作者:子木