k8s之pod调度
在Kubernetes平台上,我们很少会直接创建一个Pod,在大多数情况下会通过RC、Deployment、DaemonSet、Job等控制器完成对一组Pod副本的创建、调度及全生命周期的自动控制任务。
真实生产环境可能存在如下需求:
(1) :不同Pod之间的亲和性(Affinity)。比如MySQL数据库与Redis中间件不能被调度到同一个目标节点上,或者两种不同的Pod必须被调度到同一个Node上,以实现本地文件共享或本地网络通信等特殊需求,这就是PodAffinity要解决的问题。
(2):有 状 态 集 群 的 调 度 。 对 于 ZooKeeper 、 Elasticsearch 、MongoDB、Kafka等有状态集群,虽然集群中的每个Worker节点看起来都是相同的,但每个Worker节点都必须有明确的、不变的唯一ID(主机名或IP地址),这些节点的启动和停止次序通常有严格的顺序。此外,由于集群需持久化保存状态数据,所以集群中的Worker节点对应的Pod不管在哪个Node上恢复,都需要挂载原来的Volume,因此这些Pod还需要捆绑具体的PV。针对这种复杂的需求,Kubernetes提供了StatefulSet这种特殊的副本控制器来解决问题,在Kubernetes 1.9版本发布后,StatefulSet才可用于正式生产环境中。
(3):在每个Node上调度并且仅仅创建一个Pod副本。这种调度通常用于系统监控相关的Pod,比如主机上的日志采集、主机性能采集等进程需要被部署到集群中的每个节点,并且只能部署一个副本,这就是DaemonSet这种特殊Pod副本控制器所解决的问题
(4):对于批处理作业,需要创建多个Pod副本来协同工作,当这些Pod副本都完成自己的任务时,整个批处理作业就结束了。这种Pod运行且仅运行一次的特殊调度,用常规的RC或者Deployment都无法解决,所以Kubernetes引入了新的Pod调度控制器Job来解决问题,并继续延伸了定时作业的调度控制器CronJob。
与 单 独 的 Pod 实 例 不 同 , 由 RC 、 ReplicaSet 、 Deployment 、DaemonSet等控制器创建的Pod副本实例都是归属于这些控制器的,这就产生了一个问题:控制器被删除后,归属于控制器的Pod副本该何去何从?在Kubernetes 1.9之前,在RC等对象被删除后,它们所创建的Pod副本都不会被删除;在Kubernetes 1.9以后,这些Pod副本会被一并删除。如果不希望这样做,则可以通过kubectl命令的 –cascade=orphan 参数来取消这一默认特性:kubectl delete deploy deploy名称 –cascade=orphan
默认情况下,通过deployment定义pod后,系统会根据自动调度算法将pod随机调度到某个节点上,除了自动调度外, Kubernetes也提供了多种丰富的调度策略,用户只需在Pod的定义中使用NodeSelector、 NodeAffinity、PodAffinity、Pod驱逐等更加细粒度的调度策略设置,就能完成对Pod的精准调度。下面对这些策略进行说明。
一、 NodeSelector:定向调度:
Kubernetes Master上的Scheduler服务(kube-scheduler进程)负责实现Pod的调度,整个调度过程通过执行一系列复杂的算法,最终为每个Pod都计算出一个最佳的目标节点,这一过程是自动完成的,通常我们无法知道Pod最终会被调度到哪个节点上。在实际情况下,也可能需要将Pod调度到指定的一些Node上,可以通过Node的标签(Label)和Pod 的nodeSelector属性相匹配,来达到上述目的。
1、 首先通过kubectl label命令给目标Node打上一些标签
例如:给k8s-node2节点打上标签hello=world ,执行命令如下:
kubectl label nodes k8s-node2 hello=world

上述命令行操作也可以通过修改资源定义文件的方式,并执行 kubectl replace -f xxx.yaml命令来完成
删除标签可以通过命令” kubectl label node k8s-node2 hello- “来完成,注意,hello表示标签的键,注意hello后面还有一个减号,表示删除这个 标签
2、在pod定义文件中,添加节点选择条件,如图:

执行命令kubectl create -f nginx.yaml创建pod后,查看pod所在节点,已经成功调度到k8s-node2节点上,如图:

注意:上图中的Pod创建到命令空间gongguan中的
如果给多个Node都定义了相同的标签(例如hello=world),则 scheduler会根据调度算法从这组Node中挑选一个可用的Node进行Pod调度。
如果我们指定了Pod的nodeSelector条件,且在集群中不存在包含相应标签的Node,则即使在集群中还有其他可供使用的 Node,这个Pod也无法被成功调度,会一直显示pending状态。
除了用户可以自行给Node添加标签,Kubernetes也会给Node预定义 一些标签,执行命令kubectl get node show-labels=true 可以查看,如图:

用户也可以使用这些系统标签进行Pod的定向调度
NodeSelector通过标签的方式,简单实现了限制Pod所在节点的方 法。亲和性调度机制则极大扩展了Pod的调度能力,主要的增强功能如下:
- 更具表达力(不仅仅是“符合全部”的简单情况)
- 可以使用软限制、优先采用等限制方式,代替之前的硬限制, 这样调度器在无法满足优先需求的情况下,会退而求其次,继续运行该 Pod。
- 可以依据节点上正在运行的其他Pod的标签来进行限制,而非节点本身的标签。这样就可以定义一种规则来描述Pod之间的亲和或互斥关系。
亲和性调度功能包括节点亲和性(NodeAffinity)和Pod亲和性 (PodAffinity)两个维度的设置。节点亲和性与NodeSelector类似,增强了上述前两点优势;Pod的亲和与互斥限制则通过Pod标签而不是节点标签来实现
本小节代码内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx #deployment名字
namespace: gongguan #命名空间
labels:
name: deployment-nginx
spec:
selector:
matchLabels:
app: nginx-pod #管理标签为nginx-pod的pod
replicas: 2 #副本个数为2
template: #创建pod的模板
metadata:
labels:
app: nginx-pod #pod标签,标签选择器就是选择这个标签
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
nodeSelector: #节点选择标签
hello: world
二、 NodeAffinity:节点亲和性调度:
NodeAffinity意为Node亲和性的调度策略,是用于替换NodeSelector 的全新调度策略。目前有两种节点亲和性表达。
- RequiredDuringSchedulingIgnoredDuringExecution:必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于硬限制。
- PreferredDuringSchedulingIgnoredDuringExecution:强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制。多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序。
IgnoredDuringExecution的意思是:如果一个Pod所在的节点在Pod运行期间标签发生了变更,不再符合该Pod的节点亲和性需求,则系统将忽略Node上Label的变化,该Pod能继续在该节点运行。
1、首先在k8s-node1创建标签ni=hao,在k8s-node2创建标签hello=world
kubectl label node k8s-node1 ni=hao/kubectl label node k8s-node2 hello=world
例如: 下面例子设置了NodeAffinity调度的如下规则, requiredDuringSchedulingIgnoredDuringExecution要求必须运行在标签为ni=hao的节点上, preferredDuringSchedulingIgnoredDuringExecution的要求是尽量运行在标签为hello=world节点上,资源定义文件如下:

还可以将键值写成json格式,如图:

上图中节点亲和性表示强制这个pod必须运行在具有标签ni=hao的节点上,同时尽可能调度到具有标签hello=world的节点上
2、创建pod,创建后查看pod所在节点信息,发现被调度到k8s-node1上,如图:

现在在k8s-node2节点也打上标签ni=hao,重新创建此pod看是否可以调度到k8s-node2上,如图:

说明:在k8s-node2上打上了标签ni=hao后,就可以满足节点亲和性中设置的必须调度到标签为ni=hao的节点上,又尽可能调度到标签为hello=world的节点上
从上面的配置中可以看到In操作符,NodeAffinity语法支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt。虽然没有节点排斥功能,但是用NotIn和DoesNotExist就可以实现排斥的功能了。
- In:
label的值在某个列表中 - NotIn:
label的值不在某个列表中 - Gt:
label的值大于某个值 - Lt:
label的值小于某个值 - Exists:某个
label存在 - DoesNotExist:某个
label不存在
NodeAffinity规则设置的注意事项如下:
- 如果同时定义了nodeSelector和nodeAffinity,那么必须两个条件都得到满足,Pod才能最终运行在指定的Node上。
- 如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能够匹配成功即可。
- 如果在nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。
本节代码内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx #deployment名字
namespace: gongguan #命名空间
labels:
name: deployment-nginx
spec:
selector:
matchLabels:
app: nginx-pod #管理标签为nginx-pod的pod
replicas: 2 #副本个数为2
template: #创建pod的模板
metadata:
labels:
app: nginx-pod #pod标签,标签选择器就是选择这个标签
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
imagePullPolicy: IfNotPresent
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- {key: ni,operator: In,values: ["hao"]}
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- {key: hello,operator: In, values: ["world"]}
三、 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
注意:topologyKey主要用来筛选node,可以给一组node打上标签,那么topologyKey的值也可以设置为此标签,表示一个拓扑域
与节点不同的是,Pod是属于某个命名空间的,所以条件Y表达的 是一个或者全部命名空间中的一个Label Selector。
和节点亲和性相同,Pod亲和与互斥的条件设置也是 requiredDuringSchedulingIgnoredDuringExecution(硬策略)和 preferredDuringSchedulingIgnoredDuringExecution(软策略)。
Pod的亲和性被定义于PodSpec的affinity字段下的podAffinity子字段中。Pod间的互斥性则被定义于同一层次的podAntiAffinity子字段中。
下面通过实例来说明Pod间的亲和性和互斥性策略设置:
1、参照的目标pod
首先在命令空间gongguan中创建一个名字为nginx的nginx应用的pod,pod标签为app: nginx-pod, 后面的例子将使用nginx作为Pod亲和与互斥的目标Pod: 如图:

创建完成后,在命名空间gongguan中可以查看到Pod在节点k8s-node1上,如图:

2、接下来在命名空间gongguan创建一个redis应用的pod来说明pod亲和度,定义的亲和度标签为app:=nginx-pod,对应第一步中的pod, topologyKey的值被设置为” kubernetes.io/hostname “,如图:

注意:亲和性是关联的pod标签!!!!!,不是deployment中的标签
从上图可以看出,此Pod在亲和性位置设置的标签为第一步定义的app=nginx-pod标签,表示此pod与第一步定义的pod亲和度高,创建完后,在命名空间查看pod,如图:

从上图可以看出,新运行的名称为redis的pod和旧的nginx的pod 都在同一个节点上,第二个依据亲和性找到第一个pod
注意:当第一步中的pod如果重新调度到其他节点后,第二步的pod在重新创建后也会调度过去
redis.yaml内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis #deployment名字
namespace: gongguan #命名空间
labels:
app: redis
spec:
selector:
matchLabels:
app: redis-pod #管理标签为nginx-pod的pod
replicas: 2 #副本个数为2
template: #创建pod的模板
metadata:
labels:
app: redis-pod #pod标签,标签选择器就是选择这个标签
spec:
containers:
- name: redis
image: redis
ports:
- containerPort: 6379
imagePullPolicy: IfNotPresent
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app #关联的pod标签的键
operator: In
values:
- nginx-pod #关联的pod标签的值
topologyKey: kubernetes.io/hostname
3、接下来在命名空间gongguan中创建名称为redis-app的Pod,用来说明Pod之间的互斥性(反亲和性),如图:

上面通过硬策略的形式,设定此Pod不会调度到和具有标签app=nginx-pod的pod在同一个Node上,反亲和性就是将亲和性中的podAffinity改为podAntiAffinity即可。
与节点亲和性类似,Pod亲和性的操作符也包括In、NotIn、Exists、 DoesNotExist、Gt、Lt
注意: 原则上,topologyKey可以使用任何合法的标签Key赋值,但是出于性能和安全方面的考虑,对topologyKey有如下限制:
- 在Pod亲和性和RequiredDuringScheduling的Pod互斥性的定义中,不允许使用空的topologyKey
- 如果Admission controller包含了 LimitPodHardAntiAffinityTopology,那么针对Required DuringScheduling 的Pod互斥性定义就被限制为kubernetes.io/hostname,要使用自定义的 topologyKey,就要改写或禁用该控制器
- 在PreferredDuringScheduling类型的Pod互斥性定义中,空的topologyKey会被解释为kubernetes.io/hostname、failuredomain.beta.kubernetes.io/zone及failuredomain.beta.kubernetes.io/region的 组合。
- 如果不是上述情况,就可以采用任意合法的topologyKey了。
PodAffinity规则设置的注意事项如下 :
- 除了设置Label Selector和topologyKey,用户还可以指定 Namespace列表来进行限制,同样,使用Label Selector对Namespace进行选择。Namespace的定义和Label Selector及topologyKey同级。省略 Namespace的设置,表示使用定义了affinity/anti-affinity的Pod所在的 Namespace。如果Namespace被设置为空值(””),则表示所有 Namespace。
- 在所有关联requiredDuringSchedulingIgnoredDuringExecution的 matchExpressions全都满足之后,系统才能将Pod调度到某个Node上
四、 Taints和Tolerations(污点和容忍)
前面介绍的NodeAffinity节点亲和性,是在Pod上定义的一种属性, 使得Pod能够被调度到某些Node上运行(优先选择或强制要求)。Taint 则正好相反,它让Node拒绝Pod的运行。
Taint需要和Toleration配合使用,让Pod避开那些不合适的Node。在 Node上设置一个或多个Taint之后,除非Pod明确声明能够容忍这些污点,否则无法在这些Node上运行。Toleration是Pod的属性,让Pod能够 (注意,只是能够,而非必须)运行在标注了Taint的Node上。
1、下面命令表示在节点k8s-node1添加污点, 该Taint的键为key,值为 value,Taint的效果(effect)是NoSchedule ,如图:

上图中的设置除非Pod明确声明可以容忍这个Taint,否则就不会被调度到k8s-node1上。
在命令空间中tain中创建pod,并通过节点定向调度nodeSelector将此pod调度到k8s-node1,会提示Pending,如图:

根据事件查询显示此Node已经设置了污点,并且此pod没有设置容忍,如图:

2、下面在此Pod中添加容忍设置,让此pod可以容忍具有此污点的node,如图:

还有第二种设置办法:

Pod的Toleration声明中的key和effect需要与Taint的设置保持一致, 并且满足以下条件之一:
- operator的值是Exists(无须指定value)
- operator的值是Equal并且value相等
注意: 如果不指定operator,则默认值为Equal
另外,有如下两个特例:
- 空的key配合Exists操作符能够匹配所有的键和值
- 空的effect匹配所有的effect
在上面的例子中,effect的取值为NoSchedule,还可以取值为 PreferNoSchedule,这个值的意思是优先,也可以算作NoSchedule的软限制版本,一个Pod如果没有声明容忍这个Taint,则系统会尽量避免把这个Pod调度到这一节点上,但不是强制的。后面还会介绍另一个 effect “NoExecute”。
系统允许在同一个Node上设置多个Taint,也可以在Pod上设置多个 Toleration。Kubernetes调度器处理多个Taint和Toleration的逻辑顺序为: 首先列出节点中所有的Taint,然后忽略Pod的Toleration能够匹配的部分,剩下的没有忽略的Taint就是对Pod的效果了。下面是几种特殊情况:
- 如果在剩余的Taint中存在effect=NoSchedule,则调度器不会把该Pod调度到这一节点上。
- 如果在剩余的Taint中没有NoSchedule效果,但是有 PreferNoSchedule效果,则调度器会尝试不把这个Pod指派给这个节点
- 如果在剩余的Taint中有NoExecute效果,并且这个Pod已经在该节点上运行,则会被驱逐;如果没有在该节点上运行,则也不会再被调度到该节点上。
一般来说,如果给Node加上effect=NoExecute的Taint,那么在该 Node上正在运行的所有无对应Toleration的Pod都会被立刻驱逐,而具有相应Toleration的Pod永远不会被驱逐。 不过,系统允许给具有NoExecute 效果的Toleration加入一个可选的tolerationSeconds字段,这个设置表明 Pod可以在Taint添加到Node之后还能在这个Node上运行多久(单位为 s)
例如:在pod中定义Toleration并加入tolerationSeconds,如图:

上述定义的意思是,如果Pod正在运行,所在节点都被加入一个匹配的Taint,则这个Pod会持续在这个节点上存活120s后被逐出。如果在这个宽限期内Taint被移除,则不会触发驱逐事件。
五、QoS
即使对Pod进行了资源限制,但是实际使用时依旧会造成节点资源不足,针对资源不足Kubernetes会通过重启或驱逐Pod释放资源,如果驱逐了一些重要的pod就会很不好,驱逐什么样的pod是通过QoS((Quality of Service,服务质量)决定的
Kubernetes为我们提供了3种级别的服务质量,分别是:
- Guaranteed:[ˌɡærənˈtiːd],最高服务质量,当宿主机内存不够时,会先杀死QoS为BestEffort和Burstable的Pod,如果内存还是不够,才会杀死Guaranteed的Pod,该级别Pod的资源占用量一般比较明确,即requests字段的cpu和memory与limits字段的cpu和memory配置的一致
- Burstable:服务质量低于Guaranteed,当宿主机内存不够时,会先杀死QoS为BestEffort的Pod,如果内存还是不够,就会杀死QoS级别为Burstable的Pod,用来保证QoS质量为Guaranteed的Pod,该级别的Pod一般知道最小资源使用量,但是当机器资源充足时,还是想尽可能使用更多的资源,即limits字段的cpu和memory大于requests字段的cpu和memory的配置
- BestEffort:尽力而为,当宿主机内存不够时,首先杀死的就是该QoS的Pod,用以保证Burstable和Guaranteed级别的Pod正常运行
实现不同级别的服务质量是根据requests和limits的配置决定的
- Guaranteed:requests字段的cpu和memory与limits字段的cpu和memory配置的一致
- Burstable:requests字段的cpu和memory值小于limits字段的cpu和memory配置值
- BestEffort:不配置requests和limits字段
1、 下面例子创建一个pod,没有设置limits和requests字段,查看OoS,如图:

查看Qos,此时为BestEffort,如图:

2、修改上述清单文件,设置limits和requests的值相等,如图:

查看容器的Qos,值为Guaranteed,如图:

3、修改上述清单文件,设置limits大于requests的值,如图:

查看容器的Qos,值为Burstable,如图:

说明:
- cpu为0.1表示0.1颗cpu,1个cpu等于1000m,因此设置0.1和100m都行
- 256mi表示内存此时使用的就是256m,如果是G,可设置为1Gi、2Gi等


