Kubernetes-控制器(Controller)模型
简介
Pod 这个看似复杂的 API 对象,实际上就是对容器的进一步抽象和封装而已。
Pod 对象,其实就是容器的升级版。它对容器进行了组合,添加了更多的属性和字段。这就好比给容器集装箱四面安装了吊环,使得 Kubernetes 这架“吊车”,可以更轻松地操作它。
而操作这些“集装箱”的逻辑,都由控制器(Controller)完成
控制器模式
控制器集合
在Kubernetes 架构中,有一个叫作kube-controller-manager 的组件。实际上,这个组件,就是一系列控制器的集合。我们可以查看一下 Kubernetes 项目的 pkg/controller 目录:
1 | $ cd kubernetes/pkg/controller/ |
控制器通用编排模式
这个目录下面的每一个控制器,都以独有的方式负责某种编排功能。实际上,这些控制器之所以被统一放在 pkg/controller 目录下,就是因为它们都遵循 Kubernetes 项目中的一个通用编排模式,即:控制循环(control loop)。
看一个nginx-deployment 例子:
1 | apiVersion: apps/v1 |
这个 Deployment 定义的编排动作非常简单,即:确保携带了 app=nginx 标签的 Pod 的个数,永远等于 spec.replicas 指定的个数,即 2 个。
这就意味着,如果在这个集群中,携带 app=nginx 标签的 Pod 的个数大于 2 的时候,就会有旧的 Pod 被删除;反之,就会有新的 Pod 被创建。
控制循环的实现步骤:
- Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
- Deployment 对象的 Replicas 字段的值就是期望状态;
- 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 | apiVersion: apps/v1 |
从这个 YAML 文件中,可以看到,一个 ReplicaSet 对象,其实就是由副本数目的定义和一个 Pod 模板组成的。不难发现,它的定义其实是 Deployment 的一个子集。
Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。
实现方法
明白了这个原理,再来分析一个如下所示的 Deployment:
1 | apiVersion: apps/v1 |
可以看到,这就是一个常用的 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 | $ kubectl scale deployment nginx-deployment --replicas=4 |
滚动更新
定义
将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是“滚动更新”。
更新流程
下面用实际例子来解释一下:
首先,来创建这个 nginx-deployment:
1 | $ kubectl create -f nginx-deployment.yaml --record |
--record
:记录下每次操作所执行的命令,以方便后面查看。
检查一下 nginx-deployment 创建后的状态信息:
1 | $ kubectl get deployments |
在返回结果中,可以看到四个状态字段,它们的含义如下所示。
- DESIRED:用户期望的 Pod 副本个数(spec.replicas 的值);
- CURRENT:当前处于 Running 状态的 Pod 的个数;
- UP-TO-DATE:当前处于最新版本的 Pod 的个数,所谓最新版本指的是 Pod 的 Spec 部分与 Deployment 里 Pod 模板里定义的完全一致;
- AVAILABLE:当前已经可用的 Pod 的个数,即:既是 Running 状态,又是最新版本,并且已经处于 Ready(健康检查正确)状态的 Pod 的个数。
可以看到,只有这个 AVAILABLE 字段,描述的才是用户所期望的最终状态。vs
还可以实时查看 Deployment 对象的状态变化。
命令:kubectl rollout status
1 | $ kubectl rollout status deployment/nginx-deployment |
继续等待一会儿,就能看到这个 Deployment 的 3 个 Pod,就进入到了 AVAILABLE 状态:
1 | $ kubectl get deployments |
查看一下这个 Deployment 所控制的 ReplicaSet:
命令:kubectl get rs
1 | $ kubectl get rs |
如上所示,在用户提交了一个 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 | $ kubectl edit deployment/nginx-deployment |
这个 kubectl edit 指令,会帮你直接打开 nginx-deployment 的 API 对象。然后,你就可以修改这里的 Pod 模板部分了。比如,在这里,我将 nginx 镜像的版本升级到了 1.9.1。
备注:kubectl edit 命令实际上是把 API 对象的内容下载到了本地文件,等修改完成后再提交上去。
通过 kubectl rollout status
指令查看 nginx-deployment 的状态变化:
1 | $ kubectl rollout status deployment/nginx-deployment |
通过查看 Deployment 的 Events,看到这个“滚动更新”的流程:
1 | $ kubectl describe deployment nginx-deployment |
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 | $ kubectl get rs |
其中,旧 ReplicaSet(hash=3167673210)已经被“水平收缩”成了 0 个副本。
更新原则
为了保证服务的连续性,Deployment Controller 还会确保,在任何时间窗口内,只有指定比例的 Pod 处于离线状态。同时,它也会确保,在任何时间窗口内,只有指定比例的新 Pod 被创建出来。这两个比例的值都是可以配置的,默认都是 DESIRED 值的 25%。
所以,在上面这个 Deployment 的例子中,它有 3 个 Pod 副本,那么控制器在“滚动更新”的过程中永远都会确保至少有 2 个 Pod 处于可用状态,至多只有 4 个 Pod 同时存在于集群中。这个策略,是 Deployment 对象的一个字段,名叫 RollingUpdateStrategy,如下所示:
1 | apiVersion: apps/v1 |
maxSurge
:除了 DESIRED 数量之外,在一次“滚动”中,Deployment 控制器还可以创建多少个新 Pod;maxUnavailable
:在一次“滚动”中,Deployment 控制器可以删除多少个旧 Pod。
同时,这两个配置还可以用前面我们介绍的百分比形式来表示,比如:maxUnavailable=50%,指的是我们最多可以一次删除“50%*DESIRED 数量”个 Pod。
滚动回滚(降级)
先把镜像名字修改成为了一个错误的名字,比如:nginx:1.91,这样,这个 Deployment 就会出现一个升级失败的版本。
这次,使用 kubectl set image
的指令,直接修改 nginx-deployment 所使用的镜像。这个命令的好处就是,你可以不用像 kubectl edit 那样需要打开编辑器。
1 | $ kubectl set image deployment/nginx-deployment nginx=nginx:1.91 |
查一下 ReplicaSet 的状态,如下所示:
1 | $ kubectl get rs |
通过这个返回结果,可以看到,新版本的 ReplicaSet(hash=2156724341)的“水平扩展”已经停止。而且此时,它已经创建了两个 Pod,但是它们都没有进入 READY 状态。这当然是因为这两个 Pod 都拉取不到有效的镜像。
与此同时,旧版本的 ReplicaSet(hash=1764197365)的“水平收缩”,也自动停止了。此时,已经有一个旧 Pod 被删除,还剩下两个旧 Pod。
回滚到上一个版本
执行kubectl rollout undo
命令,就能把整个 Deployment 回滚到上一个版本:
1 | $ kubectl rollout undo deployment/nginx-deployment |
在具体操作上,Deployment 的控制器,其实就是让这个旧 ReplicaSet(hash=1764197365)再次“扩展”成 3 个 Pod,而让新的 ReplicaSet(hash=2156724341)重新“收缩”到 0 个 Pod。
回滚到任意一个版本
使用kubectl rollout history
命令,查看每次 Deployment 变更对应的版本。由于在创建这个 Deployment 的时候,指定了–record 参数,所以我们创建这些版本时执行的 kubectl 命令,都会被记录下来。还可以通过这个指令,看到每个版本对应的 Deployment 的 API 对象的细节,最后加上具体的版本即可,这个操作的输出如下所示:
1 | $ kubectl rollout history deployment/nginx-deployment [--revision=2] |
可以看到,前面执行的创建和更新操作,分别对应了版本 1 和版本 2,而那次失败的更新操作,则对应的是版本 3。
最后使用kubectl rollout undo
命令,再加上要回滚到的指定版本的版本号,就可以回滚到指定版本了。这个指令的用法如下:
1 | $ kubectl rollout undo deployment/nginx-deployment --to-revision=2 |
这样,Deployment Controller 会按照“滚动更新”的方式,完成对 Deployment 的降级操作。
控制多余版本的生成
控制多次编辑版本的数量
Kubernetes 项目还提供了一个指令,使得对 Deployment 的多次更新操作,最后 只生成一个 ReplicaSet。
具体的做法是,在更新 Deployment 前,你要先执行一条 kubectl rollout pause
指令。它的用法如下所示:
1 | $ kubectl rollout pause deployment/nginx-deployment |
这个 kubectl rollout pause 的作用,是让这个 Deployment 进入了一个“暂停”状态。
所以接下来,就可以随意使用 kubectl edit 或者 kubectl set image 指令,修改这个 Deployment 的内容了。
由于此时 Deployment 正处于“暂停”状态,所以对 Deployment 的所有修改,都不会触发新的“滚动更新”,也不会创建新的 ReplicaSet。
而等到对 Deployment 修改操作都完成之后,只需要再执行一条 kubectl rollout resume
指令,就可以把这个 Deployment“恢复”回来,如下所示:
1 | $ kubectl rollout resume deployment/nginx-deployment |
而在这个 kubectl rollout resume 指令执行之前,在 kubectl rollout pause 指令之后的这段时间里,对 Deployment 进行的所有修改,最后只会触发一次“滚动更新”。
可以通过检查 ReplicaSet 状态的变化,来验证一下 kubectl rollout pause 和 kubectl rollout resume 指令的执行效果,如下所示:
1 | $ kubectl get rs |
通过返回结果,可以看到,只有一个 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(副本数)。
Kubernetes-控制器(Controller)模型