Controller
Last updated
Was this helpful?
Last updated
Was this helpful?
Pod 是 Kubernetes 项目里最核心的编排对象,而编排功能则是由 Controller(kube-controller-manager) 完成。Pod 和 Controller 之间通过 label 标签建立关系。
Deployment 是控制器的一种,控制器要么就是创建、更新一些 Pod(或者其他的 API 对象、资源),要么就是删除一些已经存在的 Pod(或者其他的 API 对象、资源)。
控制器,是用一种对象来管理另一种对象。
控制器对象本身,负责定义被管理对象的期望状态。比如,Deployment 里的 replicas=2 这个字段。而被控制对象的定义,则来自于一个“模板”。比如,Deployment 里的 template 字段,所有被这个 Deployment 管理的 Pod 实例,都是根据这个 template 字段的内容创建出来的。
Deployment 的设计思想是应用版本和 ReplicaSet 一一对应。Deployment 实际上是一个两层控制器,Deployment 控制 ReplicaSet(版本),ReplicaSet 控制 Pod(副本数)。
Deployment 所管理的 Pod,互相之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment 就可以通过 Pod 模板创建新的 Pod;不需要的时候,Deployment 就可以“杀掉”任意一个 Pod。
Deployment 典型应用场景是:Web服务。
尤其是分布式应用,它的多个实例之间,往往有依赖关系,比如:主从关系、主备关系。还有就是数据存储类应用,它的多个实例,往往都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉,即便重建出来,实例与数据之间的对应关系也已经丢失,从而导致应用启动失败。所以,这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)。
StatefulSet 可以认为是对 Deployment 的改良。StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候,对它们进行编号,编号与 Pod 的名字和 hostname 等标识信息绑定上,并且按照编号顺序逐一完成创建工作。当需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。
第一种方式,是以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式。比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上。
第二种方式,就是以 Service 的 DNS 方式。比如:这时候,只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。
在第二种 Service DNS 的方式下,具体还可以分为两种处理方法:
第一种处理方法,是 Normal Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致了。
第二种处理方法,是 Headless Service。这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。
这样的设计有什么作用呢?
因为<pod-name>.<svc-name>.<namespace>.svc.cluster.local
这样的DNS 记录,是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。
Pod每次重建,它的IP都会发生变化,如果通过IP来定位Pod,拓扑状态就不稳定了。
应用的多个实例分别绑定了不同的存储数据,一对一绑定,即使 Pod 被重新创建,也会按照之前的编号顺序和绑定关系重新创建出新的 Pod,虽然新的 Pod 的集群ip发生了变化,由于这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,网络标识和数据的绑定关系与之前都是相同的。
clusterIP 字段的值为None
的就是 Headless Service。这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。也是用 Label Selector 机制选择出来的,所有携带了 app=nginx 标签的 Pod,都会被这个 Service 代理起来。创建的 Headless Service 会使它所代理的所有 Pod 的 IP 地址再绑定一个这样格式的 DNS 记录<pod-name>.<svc-name>.<namespace>.svc.cluster.local
。
和 deployment 的 yaml 唯一区别,就是多了一个 serviceName=nginx 字段。这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
StatefulSet 典型应用场景是:按顺序启动的主从关系,或者一个数据库应用的多个存储实例。
PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。
自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。
可以看到,这些 PVC,都以“--< 编号 >”的方式命名,并且处于 Bound 状态。
Pod 的编号与 PVC 的编号一一对应,比如,在名叫 web-0 的 Pod 的 volumes 字段,它会声明使用名叫 www-web-0 的 PVC。
如果在此时删除 web-0 这个 Pod。
当把一个 Pod 删除之后,这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里。
当把一个 Pod 删除之后,StatefulSet 控制器会创建一个新的 Pod。在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字与之前相同,还是叫作:www-web-0。
所以,在这个新的 web-0 Pod 被创建出来之后,Kubernetes 为它查找名叫 www-web-0 的 PVC 时,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。
这样,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。
DaemonSet 的主要作用,是在 Kubernetes 集群里,运行一个 Daemon Pod:
这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
每个节点上只有一个这样的 Pod 实例;
当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。
这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod,作用是通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch。
可以看到,DaemonSet 跟 Deployment 其实非常相似,只不过是没有 replicas 字段。
DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时,它就可以很容易地去检查,当前这个 Node 上是不是有一个携带了 name=fluentd-elasticsearch 标签的 Pod 在运行。
而检查的结果,可能有这么三种情况:
没有这种 Pod,那么就意味着要在这个 Node 上创建这样一个 Pod;
有这种 Pod,但是数量大于 1,那就说明要把多余的 Pod 从这个 Node 上删除掉;
正好只有一个这种 Pod,那说明这个节点是正常的。
其中,删除节点(Node)上多余的 Pod 非常简单,直接调用 Kubernetes API 就可以了。
如上,定义的 nodeAffinity 的含义是:
requiredDuringSchedulingIgnoredDuringExecution:它的意思是说,这个 nodeAffinity 必须在每次调度的时候予以考虑。同时,这也意味着你可以设置在某些情况下不考虑这个 nodeAffinity;
这个 Pod,将来只允许运行在“metadata.name
”是“node-special”的节点上。
DaemonSet 会在向 Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象,加上这样一个 nodeAffinity 定义。此外,DaemonSet 还会给这个 Pod 自动加上另外一个与调度相关的字段,叫作 tolerations。
被标记为 unschedulable “污点”的 Node,一般是不允许把 Pod 调度到它上面的。但是给 Pod 加上这样一个 tolerations 定义,“容忍”这个“污点”,就使得定义的这个 Pod 可以忽略这个限制。
注:“污点”可以理解为一种特殊的 Label。
总结:在创建每个 Pod 的时候,DaemonSet 会自动给这个 Pod 加上一个 nodeAffinity,从而保证这个 Pod 只会在指定节点上启动。同时,它还会自动给这个 Pod 加上一个 Toleration,从而忽略节点的 unschedulable“污点”。
Deployment 的版本管理,靠的是“一个版本对应一个 ReplicaSet 对象”。可是,DaemonSet 控制器操作的直接就是 Pod,不可能有 ReplicaSet 这样的对象参与其中。那么,它的这些版本又是如何维护的呢?
ControllerRevision,专门用来记录某种 Controller 对象的版本。
通过使用 kubectl describe 查看 ControllerRevision 对象可知,这个 ControllerRevision 对象,实际上是在 Data 字段保存了该版本对应的完整的 DaemonSet 的 API 对象,并且,在 Annotation 字段保存了创建这个对象所使用的 kubectl 命令。
kubectl rollout undo 操作,实际上相当于读取到了 Revision=1 的 ControllerRevision 对象保存的 Data 字段。而这个 Data 字段里保存的信息,就是 Revision=1 时这个 DaemonSet 的完整 API 对象。
所以,现在 DaemonSet Controller 就可以使用这个历史 API 对象,对现有的 DaemonSet 做一次 PATCH 操作(等价于执行一次 kubectl apply -f “旧的 DaemonSet 对象”),从而把这个 DaemonSet“更新”到一个旧版本。
这也是为什么,在执行完这次回滚完成后,DaemonSet 的 Revision 并不会从 Revision=2 退回到 1,而是会增加成 Revision=3。这是因为,一个新的 ControllerRevision 被创建了出来。
注:ControllerRevision 是一个通用的版本管理对象,StatefulSet 也是直接控制 Pod 对象的,也是使用 ControllerRevision 来进行版本管理的。
Deployment、StatefulSet、DaemonSet 它们主要编排的对象,都是“在线业务”,即:Long Running Task(长作业)。但是,有一类作业显然不满足这样的条件,这就是“离线业务”,或者叫作 Batch Job(计算业务),这种业务在计算完成后就直接退出了。
如果定义的 restartPolicy=OnFailure,那么离线作业失败后,Job Controller 就不会去尝试创建新的 Pod。但是,它会不断地尝试重启 Pod 里的容器。
在这个 Pod 模板中,定义了一个安装了 bc 命令的 Ubuntu 镜像,它运行的程序是:echo "scale=10000; 4*a(1)" | bc -l
。其中,bc 命令是 Linux 里的“计算器”;-l 表示,我现在要使用标准数学库;而 a(1),则是调用数学库中的 arctangent 函数。tan(π/4) = 1
。所以,4*atan(1)
正好就是π,也就是 3.1415926…。通过 scale=10000,即是指定了输出的小数点后的位数是 10000。在我的计算机上,这个计算大概用时 1 分 54 秒。
Job 跟其他控制器不同的是,Job 对象并不要求定义一个 spec.selector 来描述要控制哪些 Pod。
可以看到,这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。
可以看到,在这个 Job 的 YAML 里,定义了 $ITEM 这样的“变量”。
创建 Job 时,替换掉 $ITEM 这样的变量;
所有来自于同一个模板的 Job,都有一个 jobgroup: jobexample 标签,也就是说这一组 Job 使用这样一个相同的标识。
可以通过这样一句 shell 把 $ITEM 替换掉:
接下来,创建这些 Job 即可:
使用场景:当已经有了一套自己的方案,需要做的往往就是集成工作。这时候,Kubernetes 项目对这些方案来说最有价值的,就是 Job 这个 API 对象。所以,编写一个外部工具(等同于我们这里的 for 循环)来管理这些 Job 即可。
在实际的应用中,需要处理的条件往往会非常复杂,比如任务 Pod 之间的关系不那么“单纯”,是“有状态”Job。在这种情况下,使用Operator(即自定义资源和控制器),加上 Job 对象一起,可能才能更好的满足实际离线任务的编排需求。
CronJob 与 Job 的关系,正如同 Deployment 与 Pod 的关系一样。CronJob 是一个专门用来管理 Job 对象的控制器。它创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的 Unix Cron 格式的表达式。
由于定时任务的特殊性,很可能某个 Job 还没有执行完,另外一个新 Job 就产生了。可以通过 spec.concurrencyPolicy 字段来定义具体的处理策略:
concurrencyPolicy=Allow,这也是默认情况,这意味着这些 Job 可以同时存在;
concurrencyPolicy=Forbid,这意味着不会创建新的 Pod,该创建周期被跳过;
concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的 Job。
如果某一次 Job 创建失败,这次创建就会被标记为“miss”。spec.startingDeadlineSeconds=200 的含义是:在过去 200 s 里,如果 miss 的数目达到了 100 次,那么这个 Job 就不会被创建执行了。