深入掌握Pod(中)
深入掌握Pod
Pod调度
在Kubernetes平台上,我们很少会直接创建一个Pod,在大多数情况下会通过RC、Deployment、DaemonSet、Job等控制器完成对一组Pod副本的创建、调度及全生命周期的自动控制任务。
在最早的Kubernetes版本里是没有这么多Pod副本控制器的,只有一个Pod副本控制器RC(Replication Controller),这个控制器是这样设计实现的:RC独立于所控制的Pod,并通过Label标签这个松耦合关联关系控制目标Pod实例的创建和销毁,随着Kubernetes的发展,RC也出现了新的继任者——Deployment,用于更加自动地完成Pod副本的部署、版本更新、回滚等功能。
RC的继任者其实并不是Deployment,而是ReplicaSet,因为 ReplicaSet进一步增强了 RC标签选择器的灵活性。之前RC的标签选择器只能选择一个标签,而ReplicaSet拥有集合式的标签选择器,可以选择多个Pod标签,如下所示:
与RC不同,ReplicaSet被设计成能控制多个不同标签的Pod副本。一种常见的应用场景是,应用MyApp目前发布了v1与v2两个版本,用户希望MyApp的Pod副本数保持为3个,可以同时包含v1和v2版本的Pod,就可以用ReplicaSet来实现这种控制,写法如下:.
Deployment或RC
Deployment或RC:全自动调度
Deployment或RC的主要功能之一就是自动部署一个容器应用的多份副本,以及持续监控副本的数量,在集群内始终维持用户指定的副本数量。
下面是一个Deployment配置的例子,使用这个配置文件可以创建一个ReplicaSet,这个ReplicaSet会创建3个Nginx应用的Pod:
$ kubectl create deployment nginx-deployment --image=nginx -o yaml --dry-run > nginx-deployment.yaml
[root@master ~]# cat nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-deployment
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx-deployment
template:
metadata:
labels:
app: nginx-deployment
spec:
containers:
- image: nginx
name: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
[root@master ~]# kubectl create -f nginx-deployment.yaml
deployment.apps/nginx-deployment created
"查看Deployment的状态"
[root@master ~]# kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 3/3 3 3 8s
[root@master ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-b5f4f78d8-dlc8h 1/1 Running 0 45s
nginx-deployment-b5f4f78d8-nq5zh 1/1 Running 0 45s
nginx-deployment-b5f4f78d8-tngkc 1/1 Running 0 45s
总结
- 该状态说明Deployment已创建好所有3个副本,并且所有副本都是最新的可用的。通过运行kubectl get rs和kubectl get pods可以查看已创建的ReplicaSet(RS)和Pod的信息。
- 从调度策略上来说,这3个Nginx Pod由系统全自动完成调度。它们各自最终运行在哪个节点上,完全由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。
NodeSelector
NodeSelector:定向调度
Kubernetes Master上的Scheduler服务(kube-scheduler进程)负责实现Pod的调度,整个调度过程通过执行一系列复杂的算法,最终为每个Pod都计算出一个最佳的目标节点,这一过程是自动完成的,通常我们无法知道Pod最终会被调度到哪个节点上。在实际情况下,也可能需要将Pod调度到指定的一些Node上,可以通过Node的标签(Label)和Pod的nodeSelector属性相匹配,来达到上述目的。
首先通过kubectl label命令给目标Node打上一些标签:
$ kubectl label nodes <node-name> <label-key>=<label-key>
这里,我们为k8s-node-1节点打上一个zone=north标签,表明它是“北方”的一个节点:
[root@master ~]# kubectl label nodes master zone=north
node/master labeled
在Pod的定义中加上nodeSelector的设置,以redis-master-controller.yaml为例:
[root@master ~]# vi replicaset.yaml
kind: ReplicationController
apiVersion: v1
metadata:
name: nginx
namespace: default
spec:
replicas: 3
selector:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
nodeSelector:
zone: north
[root@master ~]# kubectl create -f replicaset.yaml
replicationcontroller/nginx created
[root@master ~]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-b5f4f78d8-dlc8h 1/1 Running 0 16m 10.244.0.9 master <none> <none>
nginx-deployment-b5f4f78d8-nq5zh 1/1 Running 0 16m 10.244.0.11 master <none> <none>
nginx-deployment-b5f4f78d8-tngkc 1/1 Running 0 16m 10.244.0.10 master <none> <none>
如果我们给多个Node都定义了相同的标签(例如zone=north),则scheduler会根据调度算法从这组Node中挑选一个可用的Node进行Pod调度。
通过基于Node标签的调度方式,我们可以把集群中具有不同特点的Node都贴上不同的标签,例如“role=frontend”“role=backend”“role=database”等标签,在部署应用时就可以根据应用的需求设置NodeSelector来进行指定Node范围的调度。
NodeSelector通过标签的方式,简单实现了限制Pod所在节点的方法。亲和性调度机制则极大扩展了Pod的调度能力,主要的增强功能如下。
- 更具表达力(不仅仅是“符合全部”的简单情况)。
- 可以使用软限制、优先采用等限制方式,代替之前的硬限制,这样调度器在无法满足优先需求的情况下,会退而求其次,继续运行该Pod。
- 可以依据节点上正在运行的其他Pod的标签来进行限制,而非节点本身的标签。这样就可以定义一种规则来描述Pod之间的亲和或互斥关系。
- 亲和性调度功能包括节点亲和性(NodeAffinity)和Pod亲和性(PodAffinity)两个维度的设置。节点亲和性与NodeSelector类似,增强了上述前两点优势;Pod的亲和与互斥限制则通过Pod标签而不是节点标签来实现,也就是上面第4点内容所陈述的方式,同时具有前两点提到的优点。
NodeAffinity
NodeAffinity:Node亲和性调度
NodeAffinity意为Node亲和性的调度策略,是用于替换NodeSelector的全新调度策略。目前有两种节点亲和性表达。
-
- RequiredDuringSchedulingIgnoredDuringExecution:必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于硬限制。
- PreferredDuringSchedulingIgnoredDuringExecution:强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制。多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序.
- IgnoredDuringExecution的意思是:如果一个Pod所在的节点在Pod运行期间标签发生了变更,不再符合该Pod的节点亲和性需求,则系统将忽略Node上Label的变化,该Pod能继续在该节点运行。
下面的例子设置了NodeAffinity调度的如下规则。
-
- requiredDuringSchedulingIgnoredDuringExecution要求只运行在amd64的节点上(beta.kubernetes.io/arch In amd64)。
- preferredDuringSchedulingIgnoredDuringExecution的要求是尽量运行在磁盘类型为ssd(disk-type In ssd)的节点上。
从上面的配置中可以看到In操作符,NodeAffinity语法支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt。虽然没有节点排斥功能,但是用NotIn和DoesNotExist就可以实现排斥的功能了。
NodeAffinity规则设置的注意事项如下。
- 如果同时定义了nodeSelector和nodeAffinity,那么必须两个条件都得到满足,Pod才能最终运行在指定的Node上。
- 如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能够匹配成功即可。
- 如果在nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。
PodAffinity
PodAffinity:Pod亲和与互斥调度策略
Pod间的亲和与互斥从Kubernetes 1.4版本开始引入。这一功能让用户从另一个角度来限制Pod所能运行的节点:根据在节点上正在运行的Pod的标签而不是节点的标签进行判断和调度,要求对节点和Pod两个条件进行匹配。这种规则可以描述为:如果在具有标签X的Node上运行了一个或者多个符合条件Y的Pod,那么Pod应该(如果是互斥的情况,那么就变成拒绝)运行在这个Node上。
这里X指的是一个集群中的节点、机架、区域等概念,通过Kubernetes内置节点标签中的key来进行声明。这个key的名字为topologyKey,意为表达节点所属的topology范围。
-
- kubernetes.io/hostname
- failure-domain.beta.kubernetes.io/zone
- failure-domain.beta.kubernetes.io/region
与节点不同的是,Pod是属于某个命名空间的,所以条件Y表达的是一个或者全部命名空间中的一个Label Selector。
和节点亲和相同,Pod亲和与互斥的条件设置也是requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution。Pod的亲和性被定义于PodSpec的affinity字段下的podAffinity子字段中。Pod间的互斥性则被定义于同一层次的podAntiAffinity子字段中。
参照目标Pod
首先,创建一个名为pod-flag的Pod,带有标签security=S1和app=nginx,后面的例子将使用pod-flag作为Pod亲和与互斥的目标Pod:
Pod的亲和性调度
下面创建第2个Pod来说明Pod的亲和性调度,这里定义的亲和标签是security=S1,对应上面的Pod“pod-flag”,topologyKey的值被设置为“kubernetes.io/hostname”:
创建Pod之后,使用kubectl get pods -o wide命令可以看到,这两个Pod在同一个Node上运行。
Pod的互斥性调度
创建第3个Pod,我们希望它不与目标Pod运行在同一个Node上:
这里要求这个新Pod与security=S1的Pod为同一个zone,但是不与app=nginx的Pod为同一个Node。创建Pod之后,同样用kubectl get pods -o wide来查看,会看到新的Pod被调度到了同一Zone内的不同Node上。
Taints和Tolerations
Taints和Tolerations(污点和容忍)
Taint需要和Toleration配合使用,让Pod避开那些不合适的Node。在Node上设置一个或多个Taint之后,除非Pod明确声明能够容忍这些污点,否则无法在这些Node上运行。Toleration是Pod的属性,让Pod能够(注意,只是能够,而非必须)运行在标注了Taint的Node上。
Pod的Toleration声明中的key和effect需要与Taint的设置保持一致,并且满足以下条件之一。
- operator的值是Exists(无须指定value)。
- operator的值是Equal并且value相等。
如果不指定operator,则默认值为Equal。
另外,有如下两个特例。
- 空的key配合Exists操作符能够匹配所有的键和值。
- 空的effect匹配所有的effect。
系统允许在同一个Node上设置多个Taint,也可以在Pod上设置多个Toleration。Kubernetes调度器处理多个Taint和Toleration的逻辑顺序为:首先列出节点中所有的Taint,然后忽略Pod的Toleration能够匹配的部分,剩下的没有忽略的Taint就是对Pod的效果了。下面是几种特殊情况。
- 如果在剩余的Taint中存在effect=NoSchedule,则调度器不会把该Pod调度到这一节点上。
- 如果在剩余的Taint中没有NoSchedule效果,但是有PreferNoSchedule效果,则调度器会尝试不把这个Pod指派给这个节点。
- 如果在剩余的Taint中有NoExecute效果,并且这个Pod已经在该节点上运行,则会被驱逐;如果没有在该节点上运行,则也不会再被调度到该节点上。
查看当前存在的节点
[root@master flannel]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 5h54m v1.18.1
增加一个污点
$ kubectl taint node k8s-node2 node-role.kubernetes.io=k8s-node2:NoSchedule
若要移除上述命令所添加的污点,你可以执行:
kubectl taint node k8s-node2 node-role.kubernetes.io:NoSchedule-
创建可以容忍k8s-node2的污点的Pod
$ kubectl create deployment noschedule --image=nginx:latest -o yaml --dry-run=client > noschedule.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: noschedule
name: noschedule
spec:
replicas: 1
selector:
matchLabels:
app: noschedule
template:
metadata:
labels:
app: noschedule
spec:
affinity:
nodeAffinity: #这里强制将pod调度到k8s-node2节点上进行测试
requiredDuringSchedulingIgnoredDuringExecution: # 定义硬亲和性
nodeSelectorTerms:
- matchExpressions: #集合选择器
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node2
containers:
- image: nginx:latest
name: nginx
创建并且查看Pod调度情况
$ kubectl apply -f noschedule.yaml && kubectl get pod -o wide
为Pod添加容忍点
生成Pod文件
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: noschedule
name: noschedule
spec:
replicas: 1
selector:
matchLabels:
app: noschedule
template:
metadata:
labels:
app: noschedule
spec:
affinity:
nodeAffinity: #这里强制将pod调度到k8s-node2节点上进行测试
requiredDuringSchedulingIgnoredDuringExecution: # 定义硬亲和性
nodeSelectorTerms:
- matchExpressions: #集合选择器
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node2
containers:
- image: nginx:latest
name: nginx
tolerations:
- key: "node-role.kubernetes.io" #污点的key
operator: "Equal"
value: "k8s-node2" #污点的键
effect: "NoSchedule" #污点策略
更新Pod文件
$ kubectl apply -f noschedule.yaml && kubectl get po
使用污点NoExecute策略进行测试
k8s-node2添加NoExecute污点
$ kubectl taint node k8s-node2 node-role.kubernetes.io=k8s-node2:NoExecute
更新yaml文件并且更新Pod
$ vim noschedule.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: noschedule
name: noschedule
spec:
replicas: 1
selector:
matchLabels:
app: noschedule
template:
metadata:
labels:
app: noschedule
spec:
affinity:
nodeAffinity: #这里强制将pod调度到k8s-node2节点上进行测试
requiredDuringSchedulingIgnoredDuringExecution: # 定义硬亲和性
nodeSelectorTerms:
- matchExpressions: #集合选择器
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node2
containers:
- image: nginx:latest
name: nginx
tolerations:
- key: "node-role.kubernetes.io" #污点的key
operator: "Equal"
value: "k8s-node2" #污点的键
effect: "NoSchedule" #污点策略
- key: "node-role.kubernetes.io" #污点的key
operator: "Equal"
value: "k8s-node2" #污点的键
effect: "NoExecute" #污点策略
tolerationSeconds: 60 #Pod在节点上运行60秒
$ kubectl apply -f noschedule.yaml && kubectl get pod -w
Pod优先级调度
Pod Priority Preemption:Pod优先级调度
在用户提交新的Pod创建请求后,该Pod会一直处于Pending状态,即使这个Pod是一个很重要(很有身份)的Pod,也只能被动等待其他Pod被删除并释放资源,才能有机会被调度成功。Kubernetes 1.8版本引入了基于Pod优先级抢占(Pod Priority Preemption)的调度策略,此时Kubernetes会尝试释放目标节点上低优先级的Pod,以腾出空间(资源)安置高优先级的Pod,这种调度方式被称为“抢占式调度”。
我们可以通过以下几个维度来定义:
- Priority,优先级;
- QoS,服务质量等级;
- 系统定义的其他度量指标。
Pod优先级调度示例如下。
上述YAML文件定义了一个名为high-priority的优先级类别,优先级为100000,数字越大,优先级越高,超过一亿的数字被系统保留,用于指派给系统组件。
我们可以在任意Pod中引用上述Pod优先级类别:
DaemonSet
DaemonSet:在每个Node上都调度一个Pod
DaemonSet是Kubernetes 1.2版本新增的一种资源对象,用于管理在集群中每个Node上仅运行一份Pod的副本实例.
图3.3 DaemonSet示例
这种用法适合有这种需求的应用。
- 💨 在每个Node上都运行一个GlusterFS存储或者Ceph存储的Daemon进程。
- 💨 在每个Node上都运行一个日志采集程序,例如Fluentd或者Logstach。
- 💨 在每个Node上都运行一个性能监控程序,采集该Node的运行性能数据,例如Prometheus Node Exporter、collectd、New Relic agent或者Ganglia gmond等。
DaemonSet的Pod调度策略与RC类似,除了使用系统内置的算法在每个Node上进行调度,也可以在Pod的定义中使用NodeSelector或NodeAffinity来指定满足条件的Node范围进行调度。
[root@master ~]# vi daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: default
spec:
selector:
matchLabels:
name: fluentd
template:
metadata:
labels:
name: fluentd
spec:
containers:
- name: fluentd-container01
image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
[root@master ~]# kubectl create -f daemonset.yaml
daemonset.apps/fluentd created
Job
Job:批处理调度
Kubernetes从1.2版本开始支持批处理类型的应用,我们可以通过Kubernetes Job资源对象来定义并启动一个批处理任务。批处理任务通常并行(或者串行)启动多个计算进程去处理一批工作项(work item),处理完成后,整个批处理任务结束。
- Job Template Expansion模式:一个Job对象对应一个待处理的Work item,有几个Work item就产生几个独立的Job,通常适合Work item数量少、每个Work item要处理的数据量比较大的场景,比如有一个100GB的文件作为一个Work item,总共有10个文件需要处理。
- Queue with Pod Per Work Item模式:采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,在这种模式下,Job会启动N个Pod,每个Pod都对应一个Work item。
- Queue with Variable Pod Count模式:也是采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,但与上面的模式不同,Job启动的Pod数量是可变的。
考虑到批处理的并行问题,Kubernetes将Job分以下三种类型。
1.Non-parallel Jobs
通常一个Job只启动一个Pod,除非Pod异常,才会重启该Pod,一旦此Pod正常结束,Job将结束。
2.Parallel Jobs with a fixed completion count
并行Job会启动多个Pod,此时需要设定Job的.spec.completions参数为一个正数,当正常结束的Pod数量达至此参数设定的值后,Job结束。此外,Job的.spec.parallelism参数用来控制并行度,即同时启动几个Job来处理Work Item。
3.Parallel Jobs with a work queue
任务队列方式的并行Job需要一个独立的Queue,Work item都在一个Queue中存放,不能设置Job的.spec.completions参数,此时Job有以下特性。
- 🧀 每个Pod都能独立判断和决定是否还有任务项需要处理。
- 🧀 如果某个Pod正常结束,则Job不会再启动新的Pod。
- 🧀 如果一个Pod成功结束,则此时应该不存在其他Pod还在工作的情况,它们应该都处于即将结束、退出的状态。
- 🧀 如果所有Pod都结束了,且至少有一个Pod成功结束,则整个Job成功结束。
利用job的粗并发管理输出圆周率小数点后2000位。Pods运行成功次数:10;Pod最大失败次数:4次;并行运行的Pod的个数:2。
kubectl create job myjob --image=perl -o yaml --dry-run > perl-job.yaml
[root@master ~]# vi perl-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
completions: 10
parallelism: 2
template:
spec:
containers:
- name: myjob
image: perl
imagePullPolicy: IfNotPresent
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4
[root@master ~]# kubectl create -f perl-job.yaml
job.batch/myjob created
[root@master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 0/10 5m29s 5m29s
Cronjob:定时任务
Cronjob:定时任务
Kubernetes从1.5版本开始增加了一种新类型的Job,即类似Linux Cron的定时任务Cron Job,下面看看如何定义和使用这种类型的Job。
首先,确保Kubernetes的版本为1.8及以上。
其次,需要掌握Cron Job的定时表达式,它基本上照搬了Linux Cron的表达式。
是第1位是分钟而不是秒,格式如下:
其中每个域都可出现的字符如下。
- Minutes:可出现“,”“-”“*”“/”这4个字符,有效范围为0~59的整数。
- Hours:可出现“,”“-”“*”“/”这4个字符,有效范围为0~23的整数。
- DayofMonth:可出现“,”“-”“*”“/”“?”“L”“W”“C”这8个字符,有效范围为0~31的整数。
- Month:可出现“,”“-”“*”“/”这4个字符,有效范围为1~12的整数或JAN~DEC。
- DayofWeek:可出现“,”“-”“*”“/”“?”“L”“C”“#”这8个字符,有效范围为1~7的整数或SUN~SAT。1表示星期天,2表示星期一,以此类推。
- 表达式中的特殊字符“*”与“/”的含义如下。
- *:表示匹配该域的任意值,假如在Minutes域使用“*”,则表示每分钟都会触发事件。
- /:表示从起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域设置为5/20,则意味着第1次触发在第5min时,接下来每20min触发一次,将在第25min、第45min等时刻分别触发。