控制器模式

控制循环

控制器模式最核心的就是控制循环,在控制循环中,包括控制器和被控制的系统以及能够观测系统的传感器,这三个逻辑组件。

控制循环

这三个组件都是逻辑的:

  1. 系统外界通过修改资源的spec来控制系统内的资源

  2. 控制器(controller)通过比较资源的spec和status,从而计算出一个diff

  3. diff会决定对系统进行什么样的控制操作,控制操作会使得系统(system)产生新的输出

  4. 传感器(sensor)接收到系统的输出,并以资源status形式上报

控制循环中的各个组件都是独立自主的运行,不断使系统向spec表示的终态趋近。

传感器 Sensor

控制循环中的逻辑组件传感器,由Reflector、Informer、Indexer三部分组成。

传感器
  1. Reflector通过List和Watch API Server来获取资源的数据:

    1. List:用来在Controller重启以及Watch中断的情况下,进行系统资源的全量更新

    2. Watch:在多次List之间进行增量的资源更新

  2. Reflector在获取新的资源数据后,会在Delta队列中加入一个包括资源对象信息本身以及资源对象事件类型的Delta记录

  3. Delta队列中可以保证同一个对象在队列中仅有一条记录,从而避免Reflector重新List和Watch时产生重复记录

  4. Informer不断从Delta队列中取出Delta记录,然后把资源对象交给Indexer

  5. Indexer把资源记录在一个缓存中(这个缓存在默认情况下,使用资源的Namespace作为索引),缓存中的资源可以被多个Controller共享

  6. Informer再将事件交给事件的回调函数

控制器 Controller

控制循环中的逻辑组件控制器,由事件处理函数和worker组成。

控制器
  1. 事件处理函数之间会相互关注资源的新增、更新和删除事件,并根据控制器的逻辑去决定是否要处理

  2. 对需要处理的事件,会把事件关联资源的Namespace和名字加入到一个队列中

  3. worker池从工作队列中取出事件关联的资源并进行处理(工作队列会对存储的对象进行去重,从而避免多个worker同时处理一个资源)

  4. worker在处理资源时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或更新资源对象,或者调用其他的外部服务

  5. worker如果处理失败,一般会把资源的名字重新加入到工作队列中,从而方便之后进行重试

应用编排与管理

Kubernetes项目最核心的功能---编排。Pod这个看似复杂的API对象,其实就是对容器的进一步抽象和封装。Pod对象就是容器的升级版,它对容器进行了组合,添加了更多的属性和字段。Kubernetes操作Pod的逻辑都是由控制器(controller)完成的。

创建一个nginx-deployment:

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=nignx标签的Pod的个数大于2,就会有旧的Pod被删除,反之,就会有新的Pod被创建。

kube-controller-manager,这个组件是一系列控制器的集合,查看kubernetes项目中pkg/controller目录:

cd kubernetes/pkg/controller/
ls -d */
deployment/             job/                    pod
scaler/
cloud/                  disruption/             namespace/
replicaset/             serviceaccount/         volume/
cronjob/                garbagecollector/       nodelifecycle/
replication/            statefulset/            daemon/
...

这个目录下的每一个控制器,都以独有的方式负责某种编排功能。这些控制器之所以都被统一放在pkg/controller目录下,是因为它们都遵循kubernetes项目中的一个通用编排模式,即:控制循环(controller loop)。

比如,现在有一种待编排的对象X,它有一个对应的控制器,那么就可以使用一段Go语言风格的伪代码,来描述这个控制循环

for {
  // 在具体实现中,实际状态往往来自Kubernetes集群本身
  // 比如:
  // 1. kubelet通过心跳汇报的容器状态和节点状态
  // 2. 监控系统中保存的应用监控数据
  // 3. 控制器主动收集的感兴趣的信息
  实际状态 := 获取集群中对象 X 的实际状态(Actual State)

  // 期望状态,一般来自于用户提交的YAML文件
  // 比如:Deployment对象中Replicas字段的值,这些信息都保存在Etcd中。
  期望状态 := 获取集群中对象 X 的期望状态(Desired State)
  if 实际状态 == 期望状态{
    什么都不做
  } else {
    执行编排动作,将实际状态调整为期望状态
  }
}

以Deployment为例:

  1. Deployment控制器从Etcd中获取所有携带“app:nginx”标签的Pod,然后统计它们的数量,这就是实际状态

  2. Deployment对象的Replicas字段的值就是期望状态

  3. Deployment控制器将两个状态做比较,然后根据比较结果,确定是创建Pod,还是删除已有的Pod

一个Kubernetes对象的主要编排逻辑,实际上是在第三步的比对阶段完成的。这个操作,通常被叫作调谐(Reconcile),调谐的过程被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。

调谐的结果往往是对被控制对象的某种写操作,如增加Pod或者删除已有Pod,或者增加Pod的某个字段。这是Kubernetes项目“面向API对象编程”的一个直观体现。

  • 控制器对象本身负责定义被管理对象的期望状态,比如Deployment里面replicas=2

  • 被控制对象的定义,来自于一个模板,比如Deployment里面的template字段

Deployment这个template字段里的内容,跟一个标准的Pod对象的API定义完全一样。所有被整个Deployment管理的Pod实例,都是根据这个template字段的内容创建的。Deployment定义的template字段,在kubernetes项目中称为PodTemplate(Pod模板)。大多数控制器,都会使用PodTemplate来统一定义它所要管理的Pod。也有其他类型的对象模板,比如Volume模板。

类似Deployment的控制器,实际上由上半部分的控制器定义(包括期望状态)和下半部分的被控制对象的模板组成:

控制器模型

在所有的API对象的Metadata里,都要有一个字段叫作ownerReference(GC的时候会用到这个字段),用于保存这个API对象的拥有者的信息。Kubernetes使用的“控制器模式”与“事件驱动模式”的区别与联系:(控制器视角

控制器模式

事件驱动模式

主动

被动

循环不断的尝试,最终达到申明一致

一次性的操作,失败后难以处理

Deployment的三种状态
  • Processing:Deployment 正在处于扩容和发布中

    • Processing 状态的 deployment,所有的 replicas 及 Pod 副本全部达到最新版本,而且是 available,就进入 complete 状态

  • Complete: 如果发生了一些扩缩容,会进入 processing 状态

  • Failed:

    • 如果在处理过程中遇到一些问题:比如拉镜像失败,或者 readiness probe 检查失败,就进入 failed 状态

    • 如果在运行过程中即 complete 状态,发生了一些 pod readiness probe 检查失败,deployment 也会进入 failed 状态

    • 进入 failed 状态后,除非所有 replicas 均变成 available,而且是 updated 最新版本,deployment 才会重新进入 complete 状态

Deployment控制器

Deployment控制器
  1. 首先,所有的控制器都是通过 Informer 中的 Event 做一些 Handler 和 Watch。

  2. Deployment 控制器,关注 Deployment 和 ReplicaSet 中的 Event,收到事件后会加入到队列中。

  3. Deployment 控制器从队列中取出来后,会判断 Check Paused(Deployment 是否需要新的发布,即创建新的replicaset)

    1. 如果 Paused 为true,就表示这个 Deployment 只会做一个数量上的维持(Sync replicas,把 replicas sync 同步到对应的 ReplicaSet 中,最后再 Update Deployment status),不会做新的发布, controller 这一次的 ReplicaSet 就结束了

    2. 如果 paused 为false,就会 Rollout,通过 Create 或者是 Rolling 的方式来做更新,更新的方式也是通过 Create/Update/Delete 这种 ReplicaSet 来实现

spec中字段解析

  • MinReadySeconds:Deployment 会根据 Pod ready 来看 Pod 是否可用,但是如果设置了 MinReadySeconds之后,比如设置为30秒,那 Deployment 就一定会等到 Pod ready 超过 30 秒之后才认为 Pod 是 available 的。

Pod available 的前提条件是 Pod ready,但是 ready 的 Pod 不一定是 available 的,它一定要超过 MinReadySeconds 之后,才会判断为 available;

  • revisionHistoryLimit:保留历史 revision,即保留历史 ReplicaSet 的数量,默认值为 10 个,如果回滚可能性比较大的话,可以设置数量超过 10;

  • paused:是一个标识,Deployment 只做数量维持,不做新的发布,这里在 Debug 场景可能会用到;

  • progressDeadlineSeconds:Deployment 处于扩容或者发布状态时,它的 condition 会处于一个 processing 的状态,processing 可以设置一个超时时间。如果超过超时时间还处于 processing,那么 controller 将认为这个 Pod 会进入 failed 的状态。

Deployment 在 RollingUpdate 中提供两个策略,默认都是25%:

  • MaxUnavailable:滚动过程中最多有多少个 Pod 不可用;

  • MaxSurge:滚动过程中最多存在多少个 Pod 超过预期 replicas 数量。

用户的资源足够,且更注重发布过程中的可用性,可设置 MaxUnavailable 较小、MaxSurge 较大。用户的资源比较紧张,可以设置 MaxSurge 较小,甚至设置为 0,注意 MaxSurge 和 MaxUnavailable 不能同时为 0。

ReplicaSet控制器

ReplicaSet控制器
  1. 当 Deployment 分配 ReplicaSet 之后,ReplicaSet 控制器本身也是从 Informer 中 watch 一些事件,这些事件包含了 ReplicaSet 和 Pod 的事件

  2. 从队列中取出之后,ReplicaSet控制器只管理副本数:

    1. 如果 controller 发现 replicas 比 Pod 数量大的话,就会扩容

    2. 如果发现实际数量超过期望数量的话,就会删除 Pod

Deployment 控制器做了更复杂的事情,包含了版本管理,而它把每一个版本下的数量维持工作交给 ReplicaSet 来做。

作业副本与水平扩展

Deployment实现了Kubernetes项目中一个非常重要的功能:Pod的水平扩展/收缩(Horizontal scaling out/in)。如果修改了Deployment的Pod模板,那么Deployment就需要遵循滚动更新(rolling update)的方式来升级现有容器。这个能力的实现依赖的是kubernetes项目中的另一个API 对象 : ReplicaSet。

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的一个子集。Deployment控制器实际操纵的,就是ReplicaSet对象,而不是Pod对象。即,Pod的ownerReference是ReplicaSet。

Deployment、ReplicaSet和Pod的关系

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

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

水平扩展/收缩:Deployment Controller只需要修改它所控制的ReplicaSet的Pod副本个数就可以了,通过kubectl scale指令实现。

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

滚动更新:创建一个Deployment --record参数的含义是记录每次操作所执行的命令。

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

# 查看创建的deployment
kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         0         0            0           1s

返回结果中的四个状态字段的含义:

  • DESIRED:用户期望的Pod副本个数(spec.replicas的值)

  • CURRENT:当前处于Running状态的Pod的个数

  • UP-TO-DATE:当前处于最新版的Pod的个数,所谓最新版指的是Pod的Spec部分与Deployment里Pod模板的定义完全一致

  • AVAILABLE:当前已经可用的Pod的个数,即,既是Running又是最新版,并且已经处于Ready(健康检查正确)状态的Pod个数(AVAILABLE字段,描述的才是用户所期望的最终状态)

# 实时查看Deployment对象的状态变化
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

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


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只是多加了UP-TO-DATE这个与版本有关的状态字段。这个时候修改Deployment的Pod模板,滚动更新会自动触发。

修改Deployment

  1. 修改yaml文件

  2. 直接修改Etcd里的API对象,使用kubectl edit指令

# kubectl edit 指令直接打开nginx-deployment的API对象,然后就可以修改Pod模板部分
# kubectl edit 只不过是将API对象的内容下载到本地文件,修改完成后再提交上去
kubectl edit deployment/nginx-deployment
...
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1 # 1.7.9 -> 1.9.1
        ports:
        - containerPort: 80
...

# 编辑完成后,保存退出,kubernetes会立刻触发滚动更新的过程
deployment.extensions/nginx-deployment edited

kubectl rollout status        //查看deployment的状态变化 deployment/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.extensions/nginx-deployment successfully rolled out

# 通过查看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
  1. 修改Deployment里的Pod定义后,Deployment Controller会使用这个修改后的Pod模板,创建一个新的ReplicaSet(hash=1764197365),这个新的ReplicaSet的初始Pod副本数是0

  2. 在Age=24S的位置,Deployment Controller开始将新的ReplicaSet所控制的Pod副本数从0变成1,即水平扩展出1个副本

  3. 在Age=22S的位置,Deployment Controller又将旧的ReplicaSet(hash=316763210)所控制的旧Pod副本数减少1个,即水平收缩成为2个副本

  4. 如此交替,直到新ReplicaSet管理的副本数从0变到3,旧ReplicaSet管理的Pod副本数从3变到0

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

# 滚动更新完成后,查看ReplicaSet的最终状态
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 的例子中,它有 3 个 Pod 副本,那么控制器在“滚动更新”的过程中永远都会确保至少有 2 个 Pod 处于可用状态,至多只有 4 个 Pod 同时存在于集群中。这个策略,是 Deployment 对象的一个字段,名叫 RollingUpdateStrategy,如下所示:

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 的关系图。

滚动更新

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

Deployment控制应用版本

# 修改Deployment所使用的镜像
# kubectl set image直接修改 nginx-deployment 所使用的镜像
# 不用像 kubectl edit 那样需要打开编辑器
kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.extensions/nginx-deployment image updated

# 把整个Deployment回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment
deployment.extensions/nginx-deployment

在具体操作上,Deployment的控制器,让旧的ReplicaSet再次扩展,让新的ReplicaSet重新收缩。

# 查看每次Deployment变更对应的版本
# 在kubectl create的时候使用--record参数配合
# 记录所有版本被创建时的kubectl命令
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对象,浪费资源,使用如下指令,在多次更新后只生成一个ReplicaSet对象:

# 让Deployment进入暂停状态
kubectl rollout pause deployment/nginx-deployment
deployment.extensions/nginx-deployment paused
  1. 目前Deployment处于暂停状态

  2. 使用kubectl edit 或者 kubectl set image指令修改的Deployment的内容,并不会触发滚动更新,也不会创建新的ReplicaSet

  3. 对Deployment的修改完成后执行如下指令,把Deployment恢复回来

kubectl rollout resume deploy/nginx-deployment
deployment.extensions/nginx-deployment resumed

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

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

Deployment是一个两层控制器(Deployment-->ReplicaSet版本-->Pod副本数):

  1. 通过ReplicaSet的个数来描述应用的版本

  2. 通过ReplicaSet的属性(比如replicas的值)来保证Pod的副本数量

Kubernetes通过对Deployment的设计,完成了对应用的抽象,可以使用Deployment来描述应用,使用kubectl rollout 命令控制用应用的版本。有了Deployment的能力,可以轻松实现金丝雀发布,蓝绿发布,A/B测试等很多应用发布模式。参考这个GitHub库

滚动更新就是一个自动化更新的金丝雀发布。

  • 金丝雀部署:优先发布一台或少量机器升级,等验证无误后再更新其他机器。

    • 优点是用户影响范围小,

    • 不足之处是要额外控制如何做自动更新。

  • 蓝绿部署:2组机器,

    • 蓝代表当前的V1版本,

    • 绿代表已经升级完成的V2版本。

通过LB(Load Balancer)将流量全部导入V2完成升级部署。

  • 优点是切换快速

  • 缺点是影响全部用户

最后更新于

这有帮助吗?