📑 官方文档

Controller Model

控制器模型

Control Loop

Kubernetes的pkg/controller目录下存放着控制器的集合,每一个控制器,都以独有的方式负责某种编排功能。

现在有一个待编排的对象X,它有一个对应的控制器。用伪代码描述这个控制循环(control loop):

1
2
3
4
5
6
7
8
9
for {
实际状态 := 获取集群中对象X的实际状态
期望状态 := 获取集群中对象X的期望状态
if 实际状态 == 期望状态 {
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}

在具体实现中,实际状态往往来自于Kubernetes集群本身。而期望状态,一般来自于用户提交的YAML文件。

以Deployment为例,它对控制器模型的实现如下:

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

这个操作,通常被叫做调谐(Reconcile)。这个调谐的过程,则被称作“Reconsilr Loop”(调谐循环)或者“Sync Looop”(同步循环)。调谐的最终结果,往往是对被控制对象的某种写操作,比如多Pod的CRUD。

PodTemplate

下面的Deployment对象,它定义的编排动作:确保携带了app=nginx标签的Pod的个数,永远等于spec.replicas指定的个数2。

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

这就意味着,如果在这个集群中,携带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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
ports:

一个ReplicaSet对象,其实就是由副本数目的定义和一个PodTemplate组成的。

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

Practice-1

分析下面的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.14.2
ports:
- containerPort: 80

在具体的实现上,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
2
3
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 0/3 3 0 18s

返回结果的三个状态字段的含义如下:

  1. READY:AVAILABLE/DESIRED,即当前可用的Pod的个数 / 用户期望的Pod的个数;
  2. UP-TO-DATE:当前处于最新版本的Pod的个数,最新版本指的是Pod的Spec部分与Deployment里Pod模板里定义的完全一致;
  3. AVAILABLE:当前已经可用的Pod的个数,即:即是Running状态,又是最新版本并且已经处于Ready状态的Pod的个数。

AVAILABLE字段,描述的才是用户所期望的最终状态。

通过命令kubectl rollout status可以实时查看Deployment对象的状态变化:

1
2
3
4
5
$ kubectl rollout status deployment/nginx-deployment
deployment "nginx-deployment" successfully rolled out
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 3/3 3 3 96s

Deployment的3个Pod全部进入AVAILABLE状态后,查看这个Deployment所控制的ReplicaSet:

1
2
3
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-5d59d67564 3 3 3 52m

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

这时候,修改Deployment的Pod模板,“滚动更新”就会被自动触发:

1
2
3
4
5
6
7
8
9
$ kubectl edit deployment/nginx-deployment
...
spec:
containers:
- image: nginx:1.14.2
...
deployment.apps/nginx-deployment edited
$ kubectl rollout status deployment/nginx-deployment
deployment "nginx-deployment" successfully rolled out

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

1
2
3
4
5
6
7
8
9
10
11
$ kubectl describe deployment nginx-deployment
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 94s deployment-controller Scaled up replica set nginx-deployment-66b6c48dd5 to 1
Normal ScalingReplicaSet 93s deployment-controller Scaled down replica set nginx-deployment-5d59d67564 to 2
Normal ScalingReplicaSet 93s deployment-controller Scaled up replica set nginx-deployment-66b6c48dd5 to 2
Normal ScalingReplicaSet 92s deployment-controller Scaled down replica set nginx-deployment-5d59d67564 to 1
Normal ScalingReplicaSet 92s deployment-controller Scaled up replica set nginx-deployment-66b6c48dd5 to 3
Normal ScalingReplicaSet 90s deployment-controller Scaled down replica set nginx-deployment-5d59d67564 to 0

首先,修改了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
2
3
4
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-5d59d67564 0 0 0 122m
nginx-deployment-66b6c48dd5 3 3 3 13m

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
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。

综上,扩展一下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
2
$ kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.apps/nginx-deployment image updated

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

这时,检查ReplicaSet的状态,如下所示:

1
2
3
4
5
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-5d59d67564 0 0 0 174m
nginx-deployment-66b6c48dd5 3 3 3 64m
nginx-deployment-d645d84b6 1 1 0 2m40s

可以看到,新版本的ReplicaSet(hash=d645d84b6)的“水平扩展”已经停止。此时,它创建了一个Pod,但是它们都没有进入READY状态。

与此同时,旧版本的ReplicaSet(hash=66b6c48dd5)的“水平收缩”也自动停止了。三个旧的Pod均为被删除。

那么问题来了,如何让这个Deployment的3个Pod都会滚到以前的版本呢?

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

1
2
$ kubectl rollout undo deployment/nginx-deployment
deployment.apps/nginx-deployment rolled back

使用kubectl rollout history命令查看每次Deployment变更对应的版本:

1
2
3
4
5
6
$ kubectl rollout history deployment/nginx-deployment
deployment.apps/nginx-deployment
REVISION CHANGE-CAUSE
1 kubectl create --filename=nginx-deployment.yaml --record=true
3 kubectl create --filename=nginx-deployment.yaml --record=true
4 kubectl create --filename=nginx-deployment.yaml --record=true

上述命令后面可以指定revision看到每个版本对应的Deployment的API对象的细节,具体命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kubectl rollout history deployment/nginx-deployment --revision=4
deployment.apps/nginx-deployment with revision #4
Pod Template:
Labels: app=nginx
pod-template-hash=66b6c48dd5
Annotations: kubernetes.io/change-cause: kubectl create --filename=nginx-deployment.yaml --record=true
Containers:
nginx:
Image: nginx:1.14.2
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>

可以在kubectl rollout undo命令后,加上要回滚到的指定版本号,就可以回滚到指定版本:

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

这时检查ReplicaSet的状态,如下所示:

1
2
3
4
5
$ kubectl get rs                                                  
NAME DESIRED CURRENT READY AGE
nginx-deployment-5d59d67564 0 0 0 7h29m
nginx-deployment-66b6c48dd5 3 3 3 5h40m
nginx-deployment-d645d84b6 0 0 0 4h37m

可以看到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
2
3
4
5
6
$ kubectl rollout pause deployment/nginx-deployment
deployment.apps/nginx-deployment paused
$ kubectl set image deployment/nginx-deployment nginx=nginx:1.21.6
deployment.apps/nginx-deployment image updated
$ kubectl rollout resume deployment/nginx-deployment
deployment.apps/nginx-deployment resumed

检查ReplicaSet的状态:

1
2
3
4
5
6
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-5d59d67564 0 0 0 8h
nginx-deployment-66b6c48dd5 0 0 0 6h47m
nginx-deployment-85dd6f4cc 3 3 3 10s
nginx-deployment-d645d84b6 0 0 0 5h45m

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

不过,即使这样小心控制ReplicaSet的生成数量,Kubernetes还是会保存历史ReplicaSet。Deployment对象有一个字段,叫作spec.revisionHistryLimit,就是Kubernetes为Deployment保留的历史版本的个数。所以,如果将它设置为0,就再也不能做回滚操作了。