《深入剖析Kubernetes》总结五:控制器与编排

在线任务编排

控制器(Controller):操作容器,比如Deployment,是k8s第一个重要的设计思想,叫作控制器模式

kube-controller-manager:一系列控制器的集合

$ ls -d kubernetes/pkg/controller/
deployment/ job/ podautoscaler/
cloud/ disruption/ namespace/
replicaset/ serviceaccount/ volume/
cronjob/ garbagecollector/ nodelifecycle/

该目录下的控制器遵循同一个通用编排模式:控制循环(control loop)。原理类似CAS操作,即如果对象X的实际状态等于期望状态,则可以什么都不做;如果不等于,则执行编排动作,将实际状态调整为期望状态

实际状态来源:kubelet 通过心跳汇报的容器状态和节点状态、监控系统中保存的应用监控数据,控制器主动收集的它自己感兴趣的信息等

期望状态:一般来自于用户提交的 YAML 文件

主要编排逻辑就是对比操作,被叫作调谐(Reconcile),这个调谐的过程,则被称作“Reconcile Loop”(调 谐循环)或者“Sync Loop”(同步循环);
而调谐的最终结果,往往都是对被控制对象的某种写操作。 比如,增加 Pod,删除已有的 Pod,或者更新 Pod 的某个字段,这也是 Kubernetes “面向 API 对象编程”的一个直观体现。

以Deployment为例:
在这里插入图片描述
如上图所示,类似 Deployment 这样的一个控制器,实际上都是由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的;
Deployment 里的 replicas=2 这个字段负责定义被管理对象的期望状态;
被控制对象的定义,则来自于一个“模板”,比如,Deployment 里的 template 字段, 这个 template 字段里的内容,跟一个标准的 Pod 对象的 API 定义是一样的,所有被这个 Deployment 管理的 Pod 实例,其实都是根据这个 template 字段的内容创建出来的。

在所有 API 对象的 Metadata 里,都有一个字段叫作 ownerReference,用于保存当前这个 API 对象的拥有者(Owner)的信息;
对于一个 Deployment 所管理的 Pod,其ownerReference为ReplicaSet,详情请继续往下看

控制器模式的实现介绍:Deployment

Deployment实现了Pod 的“水平扩展 / 收缩”(horizontal scaling out/in);
比如如果更新了 Deployment 的 Pod 模板(比如,修改了容器的镜像),那么 Deployment 就需要遵循一种叫作“滚动更新”(rolling update)的方式,来升级现有的容器。

实现方式:ReplicaSet(API对象),其结构为:

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

由副本数目的定义和一个Pod模板组成,其实是Deployment的一个子集,Deployment 控制器实际操纵的就是这样的 ReplicaSet 对象,而不是 Pod 对 象

对于一个 Deployment 所管理的 Pod,它的 ownerReference 是ReplicaSet

如下图是一个示例:
在这里插入图片描述
“水平扩展 / 收缩”的实现:Deployment Controller 修改它所控制的 ReplicaSet 的 Pod 副本个数

”滚动更新“的实现:修改了 Deployment 里的 Pod 定义之后,Deployment Controller 会使用这个修改后的 Pod 模板,创建一个新的 ReplicaSet,这个新的 ReplicaSet 的初始 Pod 副本数是:0;
然后,Deployment Controller 开始将这个新的 ReplicaSet 所控制的 Pod 副本数从 0 个变成 1 个,即:“水平扩展”出一个副本;
紧接着,Deployment Controller 又将旧的 ReplicaSet所控制的旧 Pod 副本数减少一个,即:“水平收缩”掉一个副本;
如此交替进行,新 ReplicaSet 管理的 Pod 副本数,从 0 个变成 1 个,再变成 2 个。最后变成x个,而旧的 ReplicaSet 管理的 Pod 副本数则从 x 个最后变成 0 个,就完成了这一组 Pod 的版本升级过程
在这里插入图片描述
Deployment 对应用进行版本控制的具体原理也是基于滚动更新来实现的,可以根据不同的版本号进行控制,有种git的感觉

Deployment缺点:无法处理有状态的应用。

因为 Deployment 对应用做了一个简单化假设,它认为一个应用的所有 Pod是完全一样的,所以它们互相之间没有顺序,也无所谓运行在哪台宿主机上;
需要的时候,Deployment 就可以通过 Pod 模板创建新的 Pod;
不需要的时候,Deployment 就可以“杀掉”任意一个 Pod

但是实际场景并非如此,比如分布式应用中,它的多个实例之间有依赖关系,比如:主从关系、主备关系,这种实例关系是不对等的,以及实例对外部数据有依赖关系的应用(数据存储类应用的多个实例往往都会在本地磁盘上保存一份数据,而这些实例一 旦被杀掉,即便重建出来,实例与数据之间的对应关系也已经丢失,从而导致应用失败),就被称为“有状态 应用”(Stateful Application)。

StatefulSet

拓扑状态

支持有状态应用,比如拓扑状态和存储状态,其核心功能就是通过某种方式记录这些状态,然后在 Pod 被重新创建时, 能够为新 Pod 恢复这些状态

Headless Service:

Service 是 Kubernetes 项目中用来将 一组 Pod 暴露给外界访问的一种机制,可以 定义一个 Service,然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod

Service被访问的方式:

1 虚拟ip:访问Service的ip,它会把请求转发到该 Service 所代理 的某一个 Pod 上

2 Service 的 DNS:访问DNS记录,就可以访问到指定的Service所代理的某一个Pod
A Normal Service:访问DNS记录,解析到的是Service的ip,后面的流程就跟1一样
B Headless Service:访问DNS记录,解析到的是某一个Pod的ip,不需要再经过Service的代理了

StatefulSet 通过 Headless Service 的方式,为每个 Pod 创建了一个固定并且稳定 的 DNS 记录,来作为它的访问入口(ip可能不同,但是经过这个DNS记录一定可以找到对应的Pod就行);
在部署“有状态应用”的时候,应用的每个实例拥有唯一并且稳定的“网络标识”,是 一个非常重要的假设,用ip的话,是不稳定的

StatefulSet 的主要作用之一就是使用 Pod 模板创建 Pod 的时候, 对它们进行编号,并且按照编号顺序逐一完成创建工作;
当 StatefulSet 的“控 制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作

存储状态

先讲讲Persistent Volume Claim(PVC)和Persistent Volume(PV)

要在一个 Pod 里声明 Volume,只要在 Pod 里加上 spec.volumes 字段,然后就可以在这个字段里定义一个具体类型的 Volume 了,比 如:hostPath;
但是如果不知道有哪些Volume可以使用,那么就可以使用PVC,会使用PV对象里配置的Volume;
PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想,开发者只要知道并会使用“接口”,即:PVC;而运维人员则负责给“接口”绑定具体的实现,即: PV

存储状态实现:为 StatefulSet 额外添加一个 volumeClaimTemplates 字段,该字段表示凡是被这 个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC,PVC的名字会被分配一个与对应Pod完全一致的编号,等于是将PVC和Pod绑定起来,这样就可以实现存储状态了

总结

首先,StatefulSet 的控制器直接管理的是 Pod;
因为StatefulSet 里的不同 Pod 实例, 不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的;
比如,每个 Pod 的 hostname、名字等都是不同的、携带了编号的;
而 StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。

其次,Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录,只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里的 DNS 记录也就不会变;
记录解 析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新,这是 Service 机制本身的能力,不需要 StatefulSet 操心。

最后,StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC;
这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有 一个独立的 Volume

DaemonSet

作用:在 Kubernetes 集群里运行一个 Daemon Pod

特征:

  1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上
  2. 每个节点上只有一个这样的 Pod 实例
  3. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧 节点被删除后,它上面的 Pod 也相应地会被回收掉

场景:

  1. 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络
  2. 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录
  3. 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志 搜集

DaemonSet 开始运行的时机,很多时候比整个 Kubernetes 集群出现的时机都要早,原因:

DaemonSet 会给 Pod 自动加上一个与调度相关的字段,叫作 tolerations,这个字段意味着这个 Pod,会“容忍”(Toleration)某些 Node 的“污点”(Taint),“容忍”所有被标记为 unschedulable“污点”的 Node;“容忍”的效果是允许调度;
在正常情况下,被标记了 unschedulable“污点”的 Node,是不会有任何 Pod 被调度上去 的(effect: NoSchedule),可是,DaemonSet 自动地给被管理的 Pod 加上了这个特殊的 Toleration,就使得这些 Pod 可以忽略这个限制,继而保证每个节点上都会被调度一个 Pod

DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node,然后进行相应的操作:
1 删除节点(Node)上多余的 Pod:直接调用 Kubernetes API
2 在指定的 Node 上创建新 Pod:使用nodeSelector,选择 Node 的名字(nodeAffinity用来替代nodeSelector,可以支持更丰富的语法)

原理总结:
在控制循环中遍历所有节点,然后根据节点上是否有被管理 Pod 的情况,来决定是否要创建或者删除一个 Pod;
在创建每个 Pod 的时候,DaemonSet 会自动给这个 Pod 加上一个 nodeAffinity,从 而保证这个 Pod 只会在指定节点上启动;
同时还会自动给这个 Pod 加上一个 Toleration,从而忽略节点的 unschedulable“污点”,也可以在Pod模板里加上更多种类的Toleration

DaemonSet 控制器操作的是 Pod,不想Deployment有 ReplicaSet 这样的对象参与其中,所以根据“面向API对象”思路,就设计出了ControllerRevision,专门用来记录某种 Controller 对象的版本;
其Data 字段保存了该版本对应的完整的 DaemonSet 的 API 对象,Annotation 字段保存了创建这个对象所使用的 kubectl 命令。

离线任务编排

Deployment、StatefulSet、DaemonSet主要编排的对象是在线业务,即Long Running Task(长作业)

Job用来编排离线业务

Job 对象在创建后,它的 Pod 模板被自动加上了一个 ”controller-uid=< 一 个随机字符串 > “这样的 Label;
而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而保证了 Job 与它所管理的 Pod 之间的匹配关系;
而 Job Controller 之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理 的 Pod 发生重合。需

restartPolicy字段用来定义离线任务失败后的操作,Never表示重新创建Pod, Job 对象的 spec.backoffLimit 字段里定义了重试次数

Job Controller也可以用并行的方式去运行。控制的参数为:

  1. spec.parallelism:一个 Job 在任意时间最多可以启动多少个 Pod 同时运行;
  2. spec.completions: Job 至少要完成的 Pod 数目,即 Job 的最小完成数。

Cronjob:定时任务,CronJob 是一个 Job 对象的控制器(Controller),CronJob 与 Job 的关系,正如同 Deployment 与 Pod 的关系一样,CronJob 是一个专门用来管理 Job 对象的控制器;
Cronjob会定义一个时间,每到这个时间,就会产生一个Job来执行任务

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章