Controller
Pod 是 Kubernetes 项目里最核心的编排对象,而编排功能则是由 Controller(kube-controller-manager) 完成。Pod 和 Controller 之间通过 label 标签建立关系。
Deployment 是控制器的一种,控制器要么就是创建、更新一些 Pod(或者其他的 API 对象、资源),要么就是删除一些已经存在的 Pod(或者其他的 API 对象、资源)。
Deployment
控制器,是用一种对象来管理另一种对象。

控制器对象本身,负责定义被管理对象的期望状态。比如,Deployment 里的 replicas=2 这个字段。而被控制对象的定义,则来自于一个“模板”。比如,Deployment 里的 template 字段,所有被这个 Deployment 管理的 Pod 实例,都是根据这个 template 字段的内容创建出来的。

Deployment 的设计思想是应用版本和 ReplicaSet 一一对应。Deployment 实际上是一个两层控制器,Deployment 控制 ReplicaSet(版本),ReplicaSet 控制 Pod(副本数)。
Deployment 所管理的 Pod,互相之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment 就可以通过 Pod 模板创建新的 Pod;不需要的时候,Deployment 就可以“杀掉”任意一个 Pod。
Deployment 典型应用场景是:Web服务。
StatefulSet
尤其是分布式应用,它的多个实例之间,往往有依赖关系,比如:主从关系、主备关系。还有就是数据存储类应用,它的多个实例,往往都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉,即便重建出来,实例与数据之间的对应关系也已经丢失,从而导致应用启动失败。所以,这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)。
StatefulSet 可以认为是对 Deployment 的改良。StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候,对它们进行编号,编号与 Pod 的名字和 hostname 等标识信息绑定上,并且按照编号顺序逐一完成创建工作。当需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。
Service 是如何被访问的?
第一种方式,是以 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 记录也就不会变,网络标识和数据的绑定关系与之前都是相同的。
例子
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
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
。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web
和 deployment 的 yaml 唯一区别,就是多了一个 serviceName=nginx 字段。这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
StatefulSet 典型应用场景是:按顺序启动的主从关系,或者一个数据库应用的多个存储实例。
存储状态的维持
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-volume
labels:
type: local
spec:
capacity:
storage: 10Gi
rbd:
monitors:
- '10.16.154.78:6789'
- '10.16.154.82:6789'
- '10.16.154.83:6789'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
keyring: /etc/ceph/keyring
imageformat: "2"
imagefeatures: "layering"
PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。
自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。
$ kubectl create -f statefulset.yaml
$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s
www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
可以看到,这些 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
DaemonSet 的主要作用,是在 Kubernetes 集群里,运行一个 Daemon Pod:
这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;
每个节点上只有一个这样的 Pod 实例;
当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。
例子
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
labels:
k8s-app: fluentd-logging
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd-elasticsearch
image: k8s.gcr.io/fluentd-elasticsearch:1.20
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts: # 挂载了两个 hostPath 类型的 Volume, fluentd 启动之后,它会从这两个目录里搜集日志信息,并转发给 ElasticSearch 保存
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod,作用是通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch。
可以看到,DaemonSet 跟 Deployment 其实非常相似,只不过是没有 replicas 字段。
DaemonSet是如何保证每个 Node 上有且只有一个被管理的 Pod 呢?
DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时,它就可以很容易地去检查,当前这个 Node 上是不是有一个携带了 name=fluentd-elasticsearch 标签的 Pod 在运行。
而检查的结果,可能有这么三种情况:
没有这种 Pod,那么就意味着要在这个 Node 上创建这样一个 Pod;
有这种 Pod,但是数量大于 1,那就说明要把多余的 Pod 从这个 Node 上删除掉;
正好只有一个这种 Pod,那说明这个节点是正常的。
其中,删除节点(Node)上多余的 Pod 非常简单,直接调用 Kubernetes API 就可以了。
DaemonSet是如何在指定的 Node 上创建新 Pod 的呢?
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: metadata.name
operator: In # operator: In 含义是部分匹配;operator: Equal,就是完全匹配
values:
- node-special
如上,定义的 nodeAffinity 的含义是:
requiredDuringSchedulingIgnoredDuringExecution:它的意思是说,这个 nodeAffinity 必须在每次调度的时候予以考虑。同时,这也意味着你可以设置在某些情况下不考虑这个 nodeAffinity;
这个 Pod,将来只允许运行在“
metadata.name
”是“node-special”的节点上。
DaemonSet 会在向 Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象,加上这样一个 nodeAffinity 定义。此外,DaemonSet 还会给这个 Pod 自动加上另外一个与调度相关的字段,叫作 tolerations。
apiVersion: v1
kind: Pod
metadata:
name: with-toleration
spec:
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule
被标记为 unschedulable “污点”的 Node,一般是不允许把 Pod 调度到它上面的。但是给 Pod 加上这样一个 tolerations 定义,“容忍”这个“污点”,就使得定义的这个 Pod 可以忽略这个限制。
注:“污点”可以理解为一种特殊的 Label。
总结:在创建每个 Pod 的时候,DaemonSet 会自动给这个 Pod 加上一个 nodeAffinity,从而保证这个 Pod 只会在指定节点上启动。同时,它还会自动给这个 Pod 加上一个 Toleration,从而忽略节点的 unschedulable“污点”。
DaemonSet的版本管理
Deployment 的版本管理,靠的是“一个版本对应一个 ReplicaSet 对象”。可是,DaemonSet 控制器操作的直接就是 Pod,不可能有 ReplicaSet 这样的对象参与其中。那么,它的这些版本又是如何维护的呢?
ControllerRevision,专门用来记录某种 Controller 对象的版本。
$ kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch
NAME CONTROLLER REVISION AGE
fluentd-elasticsearch-64dc6799c9 daemonset.apps/fluentd-elasticsearch 2 1h
$ kubectl describe controllerrevision fluentd-elasticsearch-64dc6799c9 -n kube-system
Name: fluentd-elasticsearch-64dc6799c9
Namespace: kube-system
Labels: controller-revision-hash=2087235575
name=fluentd-elasticsearch
Annotations: deprecated.daemonset.template.generation=2
kubernetes.io/change-cause=kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record=true --namespace=kube-system
API Version: apps/v1
Data:
Spec:
Template:
$ Patch: replace
Metadata:
Creation Timestamp: <nil>
Labels:
Name: fluentd-elasticsearch
Spec:
Containers:
Image: k8s.gcr.io/fluentd-elasticsearch:v2.2.0
Image Pull Policy: IfNotPresent
Name: fluentd-elasticsearch
...
Revision: 2
Events: <none>
$ kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system
daemonset.extensions/fluentd-elasticsearch rolled back
通过使用 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 来进行版本管理的。
Job
Deployment、StatefulSet、DaemonSet 它们主要编排的对象,都是“在线业务”,即:Long Running Task(长作业)。但是,有一类作业显然不满足这样的条件,这就是“离线业务”,或者叫作 Batch Job(计算业务),这种业务在计算完成后就直接退出了。
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
parallelism: 2 # 最多可以启动的同时运行的 Pod 数量
completions: 4 # Job 的最小完成数
template:
spec:
containers:
- name: pi
image: resouer/ubuntu-bc
command: ["sh", "-c", "echo 'scale=5000; 4*a(1)' | bc -l "]
restartPolicy: Never
backoffLimit: 4 # 当作业失败时的重试次数
activeDeadlineSeconds: 600 # 最长执行时间,单位s
如果定义的 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。
$ kubectl create -f job.yaml
$ kubectl describe jobs/pi
Name: pi
Namespace: default
Selector: controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
Labels: controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
job-name=pi
Annotations: <none>
Parallelism: 1
Completions: 1
..
Pods Statuses: 0 Running / 1 Succeeded / 0 Failed
Pod Template:
Labels: controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
job-name=pi
Containers:
...
Volumes: <none>
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1m 1m 1 {job-controller } Normal SuccessfulCreate Created pod: pi-rq5rl
可以看到,这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。
使用方法之外部管理器
apiVersion: batch/v1
kind: Job
metadata:
name: process-item-$ITEM
labels:
jobgroup: jobexample
spec:
template:
metadata:
name: jobexample
labels:
jobgroup: jobexample
spec:
containers:
- name: c
image: busybox
command: ["sh", "-c", "echo Processing item $ITEM && sleep 5"]
restartPolicy: Never
可以看到,在这个 Job 的 YAML 里,定义了 $ITEM 这样的“变量”。
创建 Job 时,替换掉 $ITEM 这样的变量;
所有来自于同一个模板的 Job,都有一个 jobgroup: jobexample 标签,也就是说这一组 Job 使用这样一个相同的标识。
可以通过这样一句 shell 把 $ITEM 替换掉:
$ mkdir ./jobs
$ for i in apple banana cherry
do
cat job-tmpl.yaml | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
done
接下来,创建这些 Job 即可:
$ kubectl create -f ./jobs
$ kubectl get pods -l jobgroup=jobexample
NAME READY STATUS RESTARTS AGE
process-item-apple-kixwv 0/1 Completed 0 4m
process-item-banana-wrsf7 0/1 Completed 0 4m
process-item-cherry-dnfu9 0/1 Completed 0 4m
使用场景:当已经有了一套自己的方案,需要做的往往就是集成工作。这时候,Kubernetes 项目对这些方案来说最有价值的,就是 Job 这个 API 对象。所以,编写一个外部工具(等同于我们这里的 for 循环)来管理这些 Job 即可。
使用方法之Operator
在实际的应用中,需要处理的条件往往会非常复杂,比如任务 Pod 之间的关系不那么“单纯”,是“有状态”Job。在这种情况下,使用Operator(即自定义资源和控制器),加上 Job 对象一起,可能才能更好的满足实际离线任务的编排需求。
CronJob
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
startingDeadlineSeconds: 200
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
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 就不会被创建执行了。
Last updated
Was this helpful?