目录

Deployment

Deployment 看似简单,但实际上,它实现了 Kubernetes 项目中一个非常重要的功能:Pod 的“水平扩展 / 收缩”(horizontal scaling out/in)。举个例子,如果你更新了 Deployment 的 Pod 模板(比如,修改了容器的镜像),那么 Deployment 就需要遵循一种叫作“滚动更新”(rolling update)的方式,来升级现有的容器。而这个能力的实现,依赖的是 Kubernetes 项目中的一个非常重要的概念(API 对象):ReplicaSet。

1. ReplicaSet

ReplicaSet 的结构非常简单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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.7.9

一个 ReplicaSet 对象,其实就是由副本数目的定义和一个 Pod 模板组成的。Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。

2. Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

上面的 nginx-deployment,在具体的实现上,Deployment,与 ReplicaSet,以及 Pod 的关系如下图所示:

/images/k8s/k8s_use/deployment.webp

其中

  1. ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数(比如,3 个)。这也正是 Deployment 只允许容器的 restartPolicy=Always 的主要原因:只有在容器能保证自己始终是 Running 状态的前提下,ReplicaSet 调整 Pod 的个数才有意义。
  2. Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作

2.1 水平扩展

“水平扩展 / 收缩”非常容易实现,Deployment Controller 只需要修改它所控制的 ReplicaSet 的 Pod 副本个数就可以了。

对应的指令是 kubectl scale

1
2
3

$ kubectl scale deployment nginx-deployment --replicas=4
deployment.apps/nginx-deployment scaled

2.2 滚动更新

第一步创建这个 nginx-deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# –record 参数的作用,是记录下你每次操作所执行的命令,以方便后面查看
$ kubectl create -f nginx-deployment.yaml --record

$ kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         0         0            0           1s

# 实时查看 Deployment 对象的状态变化
$ kubectl rollout status deployment/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.apps/nginx-deployment successfully rolled out

kubectl get deployments 的返回结果有四个字段:

  1. DESIRED:用户期望的 Pod 副本个数(spec.replicas 的值);
  2. CURRENT:当前处于 Running 状态的 Pod 的个数;
  3. UP-TO-DATE:当前处于最新版本的 Pod 的个数,所谓最新版本指的是 Pod 的 Spec 部分与 Deployment 里 Pod 模板里定义的完全一致;
  4. AVAILABLE:当前已经可用的 Pod 的个数,即:既是 Running 状态,又是最新版本,并且已经处于 Ready(健康检查正确)状态的 Pod 的个数

ReplicaSet 创建流程

1
2
3
$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-3167673210   3         3         3       20s

如上所示,在用户提交了一个 Deployment 对象后,Deployment Controller 就会立即创建一个 Pod 副本个数为 3 的 ReplicaSet。这个 ReplicaSet 的名字,则是由 Deployment 的名字和一个随机字符串共同组成。这个随机字符串叫作 pod-template-hash,ReplicaSet 会把这个随机字符串加在它所控制的所有 Pod 的标签里,从而保证这些 Pod 不会与集群里的其他 Pod 混淆。

而 ReplicaSet 的 DESIRED、CURRENT 和 READY 字段的含义,和 Deployment 中是一致的。所以,相比之下,Deployment 只是在 ReplicaSet 的基础上,添加了 UP-TO-DATE 这个跟版本有关的状态字段。

滚动更新出发

修改了 Deployment 的 Pod 模板,“滚动更新”就会被自动触发。比如,直接使用 kubectl edit 指令编辑 Etcd 里的 API 对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 修改方式一: kubectl edit
$ kubectl edit deployment/nginx-deployment
... 
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1 # 1.7.9 -> 1.9.1
        ports:
        - containerPort: 80
...
deployment.extensions/nginx-deployment edited

# 修改方式二: kubect set image
$ kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.extensions/nginx-deployment image updated

kubectl edit 并不神秘,它不过是把 API 对象的内容下载到了本地文件,让你修改完成后再提交上去。kubectl edit 指令编辑完成后,保存退出,Kubernetes 就会立刻触发“滚动更新”的过程。

滚动更新过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 通过查看 Deployment 的 Events,看到这个“滚动更新”的流程
$ kubectl describe deployment nginx-deployment
...
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
...
  Normal  ScalingReplicaSet  24s   deployment-controller  Scaled up replica set nginx-deployment-1764197365 to 1
  Normal  ScalingReplicaSet  22s   deployment-controller  Scaled down replica set nginx-deployment-3167673210 to 2
  Normal  ScalingReplicaSet  22s   deployment-controller  Scaled up replica set nginx-deployment-1764197365 to 2
  Normal  ScalingReplicaSet  19s   deployment-controller  Scaled down replica set nginx-deployment-3167673210 to 1
  Normal  ScalingReplicaSet  19s   deployment-controller  Scaled up replica set nginx-deployment-1764197365 to 3
  Normal  ScalingReplicaSet  14s   deployment-controller  Scaled down replica set nginx-deployment-3167673210 to 0

可以看到,首先,当你修改了 Deployment 里的 Pod 定义之后,Deployment Controller 会使用这个修改后的 Pod 模板,创建一个新的 ReplicaSet(hash=1764197365),这个新的 ReplicaSet 的初始 Pod 副本数是:0。

然后,在 Age=24 s 的位置,Deployment Controller 开始将这个新的 ReplicaSet 所控制的 Pod 副本数从 0 个变成 1 个,即:“水平扩展”出一个副本。紧接着,在 Age=22 s 的位置,Deployment Controller 又将旧的 ReplicaSet(hash=3167673210)所控制的旧 Pod 副本数减少一个,即:“水平收缩”成两个副本。

如此交替进行,像这样,将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。在这个“滚动更新”过程完成之后,你可以查看一下新、旧两个 ReplicaSet 的最终状态

1
2
3
4
5

$ kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-1764197365   3         3         3       6s
nginx-deployment-3167673210   0         0         0       30s

这种“滚动更新”的好处是显而易见的。在升级刚开始的时候,集群里只有 1 个新版本的 Pod。如果这时,新版本 Pod 有问题启动不起来,那么“滚动更新”就会停止,从而允许开发和运维人员介入。而在这个过程中,由于应用本身还有两个旧版本的 Pod 在线,所以服务并不会受到太大的影响。当然,这也就要求你一定要使用 Pod 的 Health Check 机制检查应用的运行状态,而不是简单地依赖于容器的 Running 状态。要不然的话,虽然容器已经变成 Running 了,但服务很有可能尚未启动,“滚动更新”的效果也就达不到了。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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。

这两个配置还可以用前面我们介绍的百分比形式来表示,比如:maxUnavailable=50%,指的是我们最多可以一次删除“50%*DESIRED 数量”个 Pod。

现在我们可以扩展一下 Deployment、ReplicaSet 和 Pod 的关系图了:

/images/k8s/k8s_use/deployment_1.webp

如上图所示,

  1. Deployment 的控制器,实际上控制的是 ReplicaSet 的数目,以及每个 ReplicaSet 的属性
  2. 一个应用的版本,对应的正是一个 ReplicaSet;这个版本应用的 Pod 数量,则由 ReplicaSet 通过它自己的控制器(ReplicaSet Controller)来保证
  3. 通过这样的多个 ReplicaSet 对象,Kubernetes 项目就实现了对多个“应用版本”的描述

Deployment 实际上是一个两层控制器。首先,它通过 ReplicaSet 的个数来描述应用的版本;然后,它再通过 ReplicaSet 的属性(比如 replicas 的值),来保证 Pod 的副本数量。

滚动升级撤销

如上所述,我们可以使用下面的命令修改 Deployment 的定义,这样可以自动出发 Deployment 的滚动更新:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 修改方式一: kubectl edit
$ kubectl edit deployment/nginx-deployment
... 
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1 # 1.7.9 -> 1.9.1
        ports:
        - containerPort: 80
...
deployment.extensions/nginx-deployment edited

# 修改方式二: kubect set image
$ kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.extensions/nginx-deployment image updated

如果想让 Deployment Pod 回滚到以前的旧版本,只需要执行 kubectl rollout undo:

1
2
$ kubectl rollout undo deployment/nginx-deployment
deployment.extensions/nginx-deployment

如果想回滚到更早之前的版本,需要使用 kubectl rollout history 命令,查看每次 Deployment 变更对应的版本。而由于我们在创建这个 Deployment 的时候,指定了–record 参数,所以我们创建这些版本时执行的 kubectl 命令,都会被记录下来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 查看所有版本
$ 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.91


# 查看版本对应的 Deployment 的 API 对象的细节
$ kubectl rollout history deployment/nginx-deployment --revision=2

# 回滚到指定版本
$ kubectl rollout undo deployment/nginx-deployment --to-revision=2
deployment.extensions/nginx-deployment

我们对 Deployment 进行的每一次更新操作,都会生成一个新的 ReplicaSet 对象,是不是有些多余,甚至浪费资源呢?没错。所以,Kubernetes 项目还提供了一个指令,使得我们对 Deployment 的多次更新操作,最后 只生成一个 ReplicaSet。具体的做法是

1
2
3
4
5
6
7
8
9
# 在更新 Deployment 前执行,让这个 Deployment 进入了一个“暂停”状态
$ kubectl rollout pause deployment/nginx-deployment
deployment.extensions/nginx-deployment paused

#  kubectl edit 或者 kubectl set image 指令所有的修改,都不会触发新的“滚动更新”,也不会创建新的 ReplicaSet。

# 修改操作都完成之后,恢复滚动更新
$ kubectl rollout resume deployment/nginx-deployment
deployment.extensions/nginx-deployment resumed

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