Kubernetes-控制器(Controller)模型

简介

Pod 这个看似复杂的 API 对象,实际上就是对容器的进一步抽象和封装而已。

Pod 对象,其实就是容器的升级版。它对容器进行了组合,添加了更多的属性和字段。这就好比给容器集装箱四面安装了吊环,使得 Kubernetes 这架“吊车”,可以更轻松地操作它。

而操作这些“集装箱”的逻辑,都由控制器(Controller)完成

控制器模式

控制器集合

在Kubernetes 架构中,有一个叫作kube-controller-manager 的组件。实际上,这个组件,就是一系列控制器的集合。我们可以查看一下 Kubernetes 项目的 pkg/controller 目录:

1
2
3
4
5
6
7
$ 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)

看一个nginx-deployment 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

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

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

控制循环的实现步骤:

  1. Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
  2. Deployment 对象的 Replicas 字段的值就是期望状态;
  3. Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod

Kubernetes 对象的主要编排逻辑,实际上是在第三步的“对比”阶段完成的。

这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。它们其实指的都是同一个东西:控制循环。

而调谐的最终结果,往往都是对被控制对象的某种写操作。

控制器组成

如上图所示,类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。

可以看到,Deployment 这个 template 字段里的内容,跟一个标准的 Pod 对象的 API 定义,丝毫不差。而所有被这个 Deployment 管理的 Pod 实例,其实都是根据这个 template 字段的内容创建出来的。

像 Deployment 定义的 template 字段,在 Kubernetes 项目中有一个专有的名字,叫作 PodTemplate(Pod 模板)

Deployment

实现原理

Deployment 实现了 Kubernetes 项目中一个非常重要的功能:Pod 的“水平扩展 / 收缩”(horizontal scaling out/in)

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

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

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

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

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

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

实现方法

明白了这个原理,再来分析一个如下所示的 Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,它定义的 Pod 副本个数是 3(spec.replicas=3)。

Deployment与 ReplicaSet的关系是怎样的呢?

可以用一张图描述出来:

清楚地看到,一个定义了 replicas=3 的 Deployment,与它的 ReplicaSet,以及 Pod 的关系,实际上是一种“层层控制”的关系。

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

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

水平扩展/收缩

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

比如,把这个值从 3 改成 4,那么 Deployment 所对应的 ReplicaSet,就会根据修改后的值自动创建一个新的 Pod。这就是“水平扩展”了;“水平收缩”则反之。

命令如下:kubectl scale

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

滚动更新

定义

将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

更新流程

下面用实际例子来解释一下:

首先,来创建这个 nginx-deployment:

1
$ kubectl create -f nginx-deployment.yaml --record

--record:记录下每次操作所执行的命令,以方便后面查看。

检查一下 nginx-deployment 创建后的状态信息:

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

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

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

可以看到,只有这个 AVAILABLE 字段,描述的才是用户所期望的最终状态。vs

还可以实时查看 Deployment 对象的状态变化。

命令:kubectl rollout status

1
2
3
4
$ kubectl rollout status deployment/nginx-deployment
# 意味着已经有 2 个 Pod 进入了 UP-TO-DATE 状态。
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.apps/nginx-deployment successfully rolled out

继续等待一会儿,就能看到这个 Deployment 的 3 个 Pod,就进入到了 AVAILABLE 状态:

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

查看一下这个 Deployment 所控制的 ReplicaSet:

命令:kubectl get rs

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,在这个例子里就是:3167673210。ReplicaSet 会把这个随机字符串加在它所控制的所有 Pod 的标签里,从而保证这些 Pod 不会与集群里的其他 Pod 混淆

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

编辑Pod模版,“滚动更新”就会被自动触发,编辑有很多方式

  • 直接编辑原来的yaml文件

  • kubectl set image命令(下面会写到)

  • 直接使用kubectl edit指令编辑 Etcd 里的 API 对象。

1
2
3
4
5
6
7
8
9
10
$ 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

这个 kubectl edit 指令,会帮你直接打开 nginx-deployment 的 API 对象。然后,你就可以修改这里的 Pod 模板部分了。比如,在这里,我将 nginx 镜像的版本升级到了 1.9.1。

备注:kubectl edit 命令实际上是把 API 对象的内容下载到了本地文件,等修改完成后再提交上去。

通过 kubectl rollout status 指令查看 nginx-deployment 的状态变化:

1
2
3
$ kubectl rollout status deployment/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.extensions/nginx-deployment successfully rolled out

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

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

Pod 的版本升级过程:

  • 首先,当你修改了 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 副本数减少一个,即:“水平收缩”成两个副本。
  • 如此交替进行,新 ReplicaSet 管理的 Pod 副本数,从 0 个变成 1 个,再变成 2 个,最后变成 3 个。而旧的 ReplicaSet 管理的 Pod 副本数则从 3 个变成 2 个,再变成 1 个,最后变成 0 个。

像这样,将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。

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

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

其中,旧 ReplicaSet(hash=3167673210)已经被“水平收缩”成了 0 个副本。

更新原则

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

所以,在上面这个 Deployment 的例子中,它有 3 个 Pod 副本,那么控制器在“滚动更新”的过程中永远都会确保至少有 2 个 Pod 处于可用状态,至多只有 4 个 Pod 同时存在于集群中。这个策略,是 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
  • maxSurge:除了 DESIRED 数量之外,在一次“滚动”中,Deployment 控制器还可以创建多少个新 Pod;
  • maxUnavailable:在一次“滚动”中,Deployment 控制器可以删除多少个旧 Pod。

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

滚动回滚(降级)

先把镜像名字修改成为了一个错误的名字,比如:nginx:1.91,这样,这个 Deployment 就会出现一个升级失败的版本。

这次,使用 kubectl set image 的指令,直接修改 nginx-deployment 所使用的镜像。这个命令的好处就是,你可以不用像 kubectl edit 那样需要打开编辑器。

1
2
$ kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.extensions/nginx-deployment image updated

查一下 ReplicaSet 的状态,如下所示:

1
2
3
4
5
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-1764197365 2 2 2 24s
nginx-deployment-3167673210 0 0 0 35s
nginx-deployment-2156724341 2 2 0 7s

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

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

回滚到上一个版本

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

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

在具体操作上,Deployment 的控制器,其实就是让这个旧 ReplicaSet(hash=1764197365)再次“扩展”成 3 个 Pod,而让新的 ReplicaSet(hash=2156724341)重新“收缩”到 0 个 Pod。

回滚到任意一个版本

使用kubectl rollout history命令,查看每次 Deployment 变更对应的版本。由于在创建这个 Deployment 的时候,指定了–record 参数,所以我们创建这些版本时执行的 kubectl 命令,都会被记录下来。还可以通过这个指令,看到每个版本对应的 Deployment 的 API 对象的细节,最后加上具体的版本即可,这个操作的输出如下所示:

1
2
3
4
5
6
$ kubectl rollout history deployment/nginx-deployment [--revision=2]
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

可以看到,前面执行的创建和更新操作,分别对应了版本 1 和版本 2,而那次失败的更新操作,则对应的是版本 3。

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

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

这样,Deployment Controller 会按照“滚动更新”的方式,完成对 Deployment 的降级操作。

控制多余版本的生成
控制多次编辑版本的数量

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

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

1
2
$ kubectl rollout pause deployment/nginx-deployment
deployment.extensions/nginx-deployment paused

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

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

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

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

1
2
$ kubectl rollout resume deployment/nginx-deployment
deployment.extensions/nginx-deployment resumed

而在这个 kubectl rollout resume 指令执行之前,在 kubectl rollout pause 指令之后的这段时间里,对 Deployment 进行的所有修改,最后只会触发一次“滚动更新”。

可以通过检查 ReplicaSet 状态的变化,来验证一下 kubectl rollout pause 和 kubectl rollout resume 指令的执行效果,如下所示:

1
2
3
4
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-1764197365 0 0 0 2m
nginx-3196763511 3 3 3 28s

通过返回结果,可以看到,只有一个 hash=3196763511 的 ReplicaSet 被创建了出来。

控制ReplicaSet 的数量

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

Deployment、ReplicaSet 和 Pod 的关系

如上所示,Deployment 的控制器,实际上控制的是 ReplicaSet 的数目,以及每个 ReplicaSet 的属性。

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

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

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

Deployment 控制 ReplicaSet(版本),ReplicaSet 控制 Pod(副本数)。

作者

buubiu

发布于

2021-05-29

更新于

2024-01-25

许可协议