Kubernetes-ReplicaSet
📑 官方文档
Controller Model
Control Loop
Kubernetes的pkg/controller目录下存放着控制器的集合,每一个控制器,都以独有的方式负责某种编排功能。
现在有一个待编排的对象X,它有一个对应的控制器。用伪代码描述这个控制循环(control loop):
1 | for { |
在具体实现中,实际状态往往来自于Kubernetes集群本身。而期望状态,一般来自于用户提交的YAML文件。
以Deployment为例,它对控制器模型的实现如下:
- Deployment控制器从etcd中获取到所有携带了“agg:nginx”标签的Pod,然后统计它们的数量,这就是实际状态;
- Deployment对象的Replicas字段的值就是期望状态;
- Deployment控制器将两个状态做比较,然后根据比较结果,确定是创建Pod,还是删除已有的Pod。
这个操作,通常被叫做调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。调谐的最终结果,往往是对被控制对象的某种写操作,比如多Pod的CRUD。
PodTemplate
下面的Deployment对象,它定义的编排动作:确保携带了app=nginx标签的Pod的个数,永远等于spec.replicas指定的个数2。
1 | apiVersion: apps/v1 |
这就意味着,如果在这个集群中,携带app=nginx标签的 Pod 的个数大于 2 的时候,就会有旧的Pod被删除;反之,就会有新的Pod被创建。
Deployment这个template字段,叫做PodTemplate,其内容跟一个标准的Pod对象的API定义,丝毫不差。而所有被这个Deployment管理的Pod实例,其实都是根据这个template字段的内容创建出来的。
Deployment以及其他类似的控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。

Horizaontal Expansion
ReplicaSet
如果更新了Deployment的Pod模板(比如,修改了容器的镜像),那么Deployment就需要遵循一种叫做”滚动更新“的方式,来升级现有的容器。
这个能力的实现,依赖的是Kubernetes项目中的一个非常重要的概念(API对象):ReplicaSet。
ReplicaSet的结构非常简单,看一下这个YAML:
1 | apiVersion: apps/v1 |
一个ReplicaSet对象,其实就是由副本数目的定义和一个PodTemplate组成的。
Deployment控制器实际操纵的,就是ReplicaSet对象,而不是Pod对象。
Practice-1
分析下面的Deployment:
1 | apiVersion: apps/v1 |
在具体的实现上,Deployment、ReplicaSet和Pod的关系如下:

一个定义了replicas=3的Deployment,与它的ReplicaSet,以及Pod的关系,实际上是一种层层控制的关系。
ReplicaSet负责通过“控制器模式”,保证系统中的Pod的个数永远等于指定的个数。这也正是Deployment只允许容器的restartPolicy=Always的主要原因:只有在容器能保证自己始终是Running状态的前提下,ReplicaSet调整Pod的个数才有意义。
Deployment同样通过“控制器模式”,来操作ReplicaSet的个数和属性,进而实现“水平扩展/收缩”和“滚动更新”这两个编排动作。
实操一下,创建这个nginx-deployment:
1 | kubectl create -f nginx-deployment.yaml --record |
检查nginx-deployment创建后的状态信息:
1 | kubectl get deployments |
返回结果的三个状态字段的含义如下:
- READY:AVAILABLE/DESIRED,即当前可用的Pod的个数 / 用户期望的Pod的个数;
- UP-TO-DATE:当前处于最新版本的Pod的个数,最新版本指的是Pod的Spec部分与Deployment里Pod模板里定义的完全一致;
- AVAILABLE:当前已经可用的Pod的个数,即:即是Running状态,又是最新版本并且已经处于Ready状态的Pod的个数。
AVAILABLE字段,描述的才是用户所期望的最终状态。
通过命令kubectl rollout status
可以实时查看Deployment对象的状态变化:
1 | kubectl rollout status deployment/nginx-deployment |
Deployment的3个Pod全部进入AVAILABLE状态后,查看这个Deployment所控制的ReplicaSet:
1 | kubectl get rs |
如上所示,在用户提交了一个Deployment对象后,Deployment Controller就会立即创建一个Pod副本个数为3的ReplicaSet。这个ReplicaSet的名字,则是由Deployment的名字和一个随机字符串共同组成。随机字符串叫做pod-template-hash,ReplicaSet会把这个随机字符串加在它所控制的所有Pod的标签里,保证这些Pod不会与集群里的其他Pod混淆。
这时候,修改Deployment的Pod模板,“滚动更新”就会被自动触发:
1 | kubectl edit deployment/nginx-deployment |
这时,可以通过查看Deployment的Events,看到这个“滚动更新”的流程:
1 | kubectl describe deployment nginx-deployment |
首先,修改了Deployment里的Pod定义之后,Deployment Controller会使用这个修改后的Pod模板,创建一个新的ReplicaSet(hash=66b6c48dd5),这个新的ReplicaSet的初始Pod副本数是:0。
然后,在Age=94s的位置,Deployment Controller开始将这个新的ReplicaSet所控制的Pod副本数从0个变成1个,即:“水平扩展”出一个副本。
紧接着,在Age=22s的位置,Deployment Controller又将旧的ReplicaSet(hash=5d59d67564)所控制的旧Pod副本数减少一个,即:“水平收缩”成两个副本。
如此交替进行,新ReplicaSet管理的Pod副本数,从0个变成1个,再变成2个,最后变成3个。而旧的ReplicaSet管理的Pod副本数则从3个变成2个,再变成1个,最后变成0个。这样,就完成了这一组Pod的版本升级过程。
像这样,将一个集群中正在运行的多个Pod版本,交替地逐一升级的过程,就是“滚动更新”。
“滚动更新”完成之后,查看一下新、旧两个ReplicaSet的最终状态:
1 | kubectl get rs |
RollingUpdateStrategy
滚动更新的好处:
比如,在升级刚开始的时候,集群里只有1个新版本的Pod。如果这时,新版本Pod有问题启动不起来,那么“滚动更新”就会停止,从而允许开发和韵味人员介入。而在这个过程中,由于应用本身还有两个旧版本的Pod在线,所以服务并不会受到太大的影响。当然,这也要求使用Pod的Health Check机制检查应用的运行状态,而不是简单地依赖容器的Running状态。否则,虽然容器已经变成Running,但是服务很有可能尚未启动。
为了进一步保证服务的连续性,Deployment Controller还会确保,在任何时间窗口内,只有指定比例的Pod处于离线状态。同时,它也会确保,在任何时间窗口内,只有指定比例的新Pod被创建出来。这两个比例的值都是可用配置的,默认都是DESIRED值的25%。
在上面Practice的Deployment例子中,它有3个Pod副本,那么控制器在“滚动更新”的过程中永远都会确保至少有2个Pod处于可用状态,至多有4个Pod同时存在于集群中。这个策略,是Deployment对象的一个字段,名叫RollingUpdateStategy,如下所示:
1 | apiVersion: apps/v1 |
在上面这个RollingUpdateStrategy的配置中:
- maxSurge:是除了DESIRED数量之外,在一次“滚动”中,Deployment控制器还可以创建多少新Pod;
- maxUnavailable:在一次“滚动”中,Deployment控制器还可以删除多少个旧Pod。
综上,扩展一下Deployment、ReplicaSet和Pod的关系图。

Deployment的控制器,实际上控制的是ReplicaSet的数目,以及每个ReplicaSet的属性。
而一个应用的版本,对应的正是一个ReplicaSet;这个版本应用的Pod数量,则由ReplicaSet通过它自己的控制器(ReplicaSet Controller)来保证。通过这样的多个ReplicaSet对象,Kubernetes就实现了对多个“应用版本”的描述。
Practice-2
使用kubectl set image
的指令可以直接修改nginx-deployment所使用的镜像。
这一次,将镜像名字修改成一个错误的名字:nginx:1.91,这样这个Deployment就会出现一个升级失败的版本。
1 | kubectl set image deployment/nginx-deployment nginx=nginx:1.91 |
由于这个nginx:1.91镜像在Docker Hub中并不存在,所以这个Deployment的“滚动更新”被触发后,会立刻报错并停止。
这时,检查ReplicaSet的状态,如下所示:
1 | kubectl get rs |
可以看到,新版本的ReplicaSet(hash=d645d84b6)的“水平扩展”已经停止。此时,它创建了一个Pod,但是它们都没有进入READY状态。
与此同时,旧版本的ReplicaSet(hash=66b6c48dd5)的“水平收缩”也自动停止了。三个旧的Pod均为被删除。
那么问题来了,如何让这个Deployment的3个Pod都会滚到以前的版本呢?
只需要执行kubectl rollout undo
命令,就能把整个Deployment会滚到上一个版本。
1 | kubectl rollout undo deployment/nginx-deployment |
使用kubectl rollout history
命令查看每次Deployment变更对应的版本:
1 | kubectl rollout history deployment/nginx-deployment |
上述命令后面可以指定revision
看到每个版本对应的Deployment的API对象的细节,具体命令如下:
1 | kubectl rollout history deployment/nginx-deployment --revision=4 |
可以在kubectl rollout undo
命令后,加上要回滚到的指定版本号,就可以回滚到指定版本:
1 | kubectl rollout undo deployment/nginx-deployment --to-revision=4 |
这时检查ReplicaSet的状态,如下所示:
1 | kubectl get rs |
可以看到3个Pod都回滚到了hash=66b6c48dd5的版本。
不过,这里有一个问题,每次对Deployment的更新操作,都生成了一个新的ReplicaSet对象,这样有些多余,甚至浪费资源。
因此,Kubernetes项目还提供了一个指令,使得我们对Deployment的多次更新操作,最后只生成一个ReplicaSet。
具体的做法是,在更新Deployment前,要先执行一条kubectl rollout pause
指令。这个命令的作用,是让这个Deployment进入一个“暂停”状态。
接下来,就可以随意使用kubectl edit
或者kubectl set image
指令,修改这个Deployment的内容。由于此时,Deployment正处于暂停状态,所以我们对Deployment的所有修改,都不会触发新的“滚动更新”,也不会创建新的ReplicaSet。
而等到我们对Deployment操作修改都完成之后,只需要再执行kubectl rollout resume
指令,就可以把这个Deployment恢复回来。
实践测试一下,先暂停Deployment,更新镜像至正确版本,再恢复Deployment:
1 | kubectl rollout pause deployment/nginx-deployment |
检查ReplicaSet的状态:
1 | kubectl get rs |
通过返回结果,可以看到,只有一个hash=85dd6f4cc的ReplicaSet被创建了出来。
不过,即使这样小心控制ReplicaSet的生成数量,Kubernetes还是会保存历史ReplicaSet。Deployment对象有一个字段,叫作spec.revisionHistryLimit
,就是Kubernetes为Deployment保留的历史版本的个数。所以,如果将它设置为0,就再也不能做回滚操作了。