kube-scheduler给一个Pod做调度选择包含两个步骤:
过滤阶段会将所有满足Pod调度需求的Node选出来。例如,PodFitsResources过滤函数会检查候选Node的可用资源能否满足Pod的资源请求。在过滤之后,得出一个Node列表,里面包含了所有可调度节点;通常情况下,这个Node列表包含不止一个Node。如果这个列表是空的,代表这个Pod不可调度。
在打分阶段,调度器会为Pod从所有可调度节点中选取一个最合适的Node。根据当前启用的打分规则,调度器会给每一个可调度节点进行打分。
最后,kube-scheduler会将Pod调度到得分最高的Node上。如果存在多个得分最高的Node,kube-scheduler会从中随机选取一个。
某些场景下,我们会希望Pod只在固定节点上运行,这时候就需要用到固定节点调度了,下面会介绍两种调度的方式。
通过kubectl get node
命令查看一下集群中的节点名称,我这边worker node有node1和node2两个。
[root@master ~]# kubectl get node
NAME STATUS ROLES AGE VERSION
master Ready master 59d v1.18.0
node1 Ready <none> 59d v1.18.0
node2 Ready <none> 59d v1.18.0
创建一个nodeName.yaml文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
nodeName: node1
上面创建Pod的文件中,spec属性下的nodeName
属性就是用来进行固定节点调度的,该属性的值配置的是node1,表示该Pod只会运行在node1节点上。
通过yaml创建Pod:
kubectl apply -f nodeName.yaml
查看Pod的运行状态:
kubectl get pod -o wide
由上图可知,该Pod确实是运行在node1节点上了的。不过这里只有一个Pod,具有随机性,并不能说明就是nodeName
属性起了作用。所以下面通过创建Deployment的方式来进行验证,并把副本数设置成10,如果10个Pod都在node1节点上的话,肯定就是nodeName
属性起了作用。对应的yaml文件内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-nginx
labels:
app: deploy-label
spec:
selector:
matchLabels:
app: deploy-label
replicas: 10
template:
metadata:
labels:
app: deploy-label
spec:
nodeName: node1
containers:
- name: nginx
image: nginx
通过yaml文件创建Pod后,使用kubectl get pod -o wide
命令查看Pod的运行状态:
由上图可知,通过Deployment方式创建的10个Pod都运行在了node1节点上,说明确实是nodeName
属性起了作用。
这种方式需要用到nodeSelector
属性。首先需要在指定的节点上打标签,然后再在创建Pod的时候使用nodeSelector
属性选择这个标签,通过这种方式即可完成固定节点调度。
给node1节点打标签:
#下面的disktype是自定义的标签的key值,ssd是自定义的标签的value值
kubectl label nodes node1 disktype=ssd
通过如下两种方式查看节点标签是否新增成功:
kubectl get nodes node1 --show-labels
kubectl describe node node1|grep -10 Labels
节点标签新增成功后,新增nodeSelector.yaml文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd
上面创建Pod的文件中,spec属性下的nodeSelector
属性就是用来进行固定节点调度的,该属性下的disktype就是节点标签的key值,ssd就是节点标签的value值。
通过yaml文件创建Pod:
kubectl apply -f nodeSelector.yaml
查看Pod的运行状态:
kubectl get pod -o wide
由上图可知,该Pod确实已经运行在node1节点上了,为了防止是由于偶然性,这里仍然通过创建Deployment的方式来进行验证,副本数设置成5个,对应的yaml文件内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-nginx
labels:
app: deploy-label
spec:
selector:
matchLabels:
app: deploy-label
replicas: 5
template:
metadata:
labels:
app: deploy-label
spec:
containers:
- name: nginx
image: nginx
nodeSelector:
disktype: ssd
通过yaml文件创建Pod后,使用kubectl get pod -o wide
命令查看Pod的运行状态:
由上图可知,通过Deployment方式创建的5个Pod都运行在了node1节点上,说明确实是nodeSelector
属性起了作用。
使用nodeSelector
属性可以提供一种非常简单的方法来将Pod约束到具有特定标签的节点上。而亲和性与反亲和性功能则极大地扩展了我们可以表达约束的类型,关键的增强点包括:
节点的亲和性概念上类似于nodeSelector
,它使我们可以根据节点上的标签来约束Pod可以被调度到哪些节点。
目前有如下两种类型的节点亲和性:
requiredDuringSchedulingIgnoredDuringExecution
:硬策略。该策略指定了将Pod调度到一个节点上必须满足的规则(就像nodeSelector
一样,但使用了更具表现力的语法);preferredDuringSchedulingIgnoredDuringExecution
:软策略。该策略会指定调度器尝试将Pod调度到指定节点上,是具有偏好性的,并不是说Pod就一定要运行到指定的节点上。如果使用硬策略将Pod调度到指定节点上后,节点的相关标签在运行时发生了变更,从而不再满足Pod上的亲和性规则的话,Pod将仍然会继续在该节点上运行。
requiredDuringSchedulingRequiredDuringExecution
这种策略可能会在未来的k8s中出现,使用这种策略会将Pod从不再满足Pod的节点亲和性要求的节点上驱逐。
节点的亲和性是通过Pod.spec的affinity
字段下的nodeAffinity
字段进行指定的。
下面是一个使用节点亲和性的Pod案例:
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: e2e-az-name
operator: In
values:
- e2e-az1
- e2e-az2
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: with-node-affinity
image: nginx
以上节点亲和性的规则表示,Pod只能放置在具有标签键e2e-az-name
且标签值为e2e-az1
或e2e-az2
的节点上。在满足这些条件的节点中,具有标签键为another-node-label-key
且标签值为another-node-label-value
的节点将会被优先使用。
我们可以在上面的例子中看到operator属性值为In
的操作符的使用。节点亲和性语法支持下面这些操作符:
In
:标签值在某个列表中;NotIn
:标签值不在某个列表中;Exists
:某个标签值存在;DoesNotExist
:某个标签值不存在;Gt
:标签值大于某个值(字符串比较);Lt
:标签值小于某个值(字符串比较)。在这些操作符中,我们可以使用
NotIn
和DoesNotExist
来实现节点的反亲和性行为。
注意事项:
nodeSelector
和nodeAffinity
,两者必须都要满足,才能将Pod调度到候选节点上;nodeAffinity
类型关联的nodeSelectorTerms
,只要其中有一个nodeSelectorTerms
满足的话,Pod就可以调度到指定节点上;nodeSelectorTerms
关联的matchExpressions
,则只有当所有的matchExpressions
满足要求,Pod才会调度到指定节点上;weight
属性是权重的意思,取值范围是1-100。如果存在多个软策略的话,对于亲和性规则而言,权重越大越亲和;对于反亲和性规则而言,权重越大越不亲和。基本概念和注意事项:
Pod间的亲和性与反亲和性同样有两种策略,即硬策略requiredDuringSchedulingIgnoredDuringExecution
和软策略preferredDuringSchedulingIgnoredDuringExecution
;
Pod间亲和性与反亲和性使我们可以基于已经在节点上运行的Pod的标签来约束Pod可以调度到的节点,而不是基于节点上的标签;
Pod间亲和性与反亲和性需要大量的处理,这可能会显著减慢大规模集群中的调度,所以不建议在超过数百个节点的集群中使用它们;
Pod反亲和性需要对节点进行一致的标记,即集群中的每个节点必须具有适当的标签能够匹配topologyKey
。如果某些或所有节点缺少指定的topologyKey
标签,可能会导致意外行为;
Pod间的亲和性通过spec属性中affinity
字段下的podAffinity
字段进行指定;而Pod间的反亲和性是通过spec中affinity
字段下的podAntiAffinity
字段进行指定;
Pod间的亲和性与反亲和性的合法操作符有In
,NotIn
,Exists
,DoesNotExist
。
Pod间亲和性与反亲和性的基本案例:
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: "kubernetes.io/hostname"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: "kubernetes.io/hostname"
containers:
- name: with-pod-affinity
image: nginx
在上面的案例中,分别定义了一条Pod亲和性规则和一条Pod反亲和性规则。
在这个案例中,podAffinity
配置的是硬策略,而podAntiAffinity
配置的是软策略。案例中podAffinity
属性下配置的亲和性规则是指将该Pod调度到topologyKey
属性表示的拓扑域上,这个区域上至少需要存在一个正在运行的包含键为security值为S1的标签的Pod。案例中podAntiAffinity
属性下配置的反亲和性规则是指,如果topologyKey
属性表示的拓扑域上存在正在运行的包含键为security值为S2的标签的Pod,那么该Pod将不会被调度到这个区域上的节点中。
关于topologyKey属性的解释说明:
topologyKey
属性是用于表示一个拓扑域的,并不是表示一个节点,一个拓扑域可以包含多个节点;topologyKey
后跟的是节点标签的key值,假设一个k8s集群中有A、B、C三个节点,它们都拥有名为env这个相同key值的节点标签,但是标签的value值分别是dev、test、prod,那么这时候拓扑域和集群中的节点就是一对一的关系,这时候将Pod调度到拓扑域上就是指将Pod调度到k8s集群的某一节点上。如果A、B两个节点的标签的value值都是dev,而C节点的标签的value值是test的话,拓扑域和节点就是一对多的关系了,这时候的A、B节点是处于同一拓扑域的,C节点是处于另一个拓扑域的;topologyKey
属性对应的值是"kubernetes.io/hostname",这个是k8s给集群中所有的节点默认加的标签的key值,value值就是每个节点的节点名,集群中节点的节点名是不会重复的,所以案例中的拓扑域和节点之间就是一对一的关系。原则上,topologyKey可以是任何合法的标签键。然而,出于性能和安全原因,topologyKey有如下限制:
requiredDuringSchedulingIgnoredDuringExecution
要求的Pod反亲和性,topologyKey
不允许为空;requiredDuringSchedulingIgnoredDuringExecution
硬策略的情况,引入了准入控制器LimitPodHardAntiAffinityTopology
来限制topologyKey
不为"kubernetes.io/hostname"。如果我们想使它可用于自定义拓扑结构,则必须修改准入控制器或者禁用它;preferredDuringSchedulingIgnoredDuringExecution
要求的Pod反亲和性,空的topologyKey
的意思就是"所有拓扑结构",这里的"所有拓扑结构"就是kubernetes.io/hostname
、topology.kubernetes.io/zone
和topology.kubernetes.io/region
的组合。Pod间亲和性与反亲和性的进阶案例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
selector:
matchLabels:
app: store
replicas: 2
template:
metadata:
labels:
app: store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: redis-server
image: redis:6.0
以上案例是使用Pod间的反亲和性规则使创建的两个Pod不在同一节点,创建Pod后效果如下:
由上图可知,通过Deployment创建的两个副本Pod确实都不在同一节点上,我这边集群中worker节点只有两个,所以这里副本数才设置成2,上述案例设置了反亲和性规则的硬策略,如果副本数大于worker节点的个数的话,就会出现由于节点不够导致多出的Pod一直处于Pending
状态的情况。假设我把副本数设置为5的话,运行结果就是下面这样:
Pod间亲和性与反亲和性的高级案例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-store
replicas: 2
template:
metadata:
labels:
app: web-store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-store
topologyKey: "kubernetes.io/hostname"
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-app
image: nginx:1.20
这个案例是要结合上面那个进阶案例进行使用的,所以在使用这个yaml文件创建Deployment之前,要保证前面那个进阶案例对应的yaml文件已经创建成功,下面大致说一下这个yaml文件创建后达到的效果。
前面进阶案例的yaml文件中使用反亲和性规则使得创建的两个Pod分配在了不同的节点上,现在这个yaml文件中也使用反亲和性规则使得创建的两个Pod分配在不同的节点上,并且还对上面进阶案例中创建的Pod使用了亲和性规则,所以最终会出现两个节点上都分别有一个进阶案例中创建的Pod和这个案例中创建的Pod,具体截图如下:
节点亲和性是Pod的一种属性,它可以使Pod被吸引到一类特定的节点上。这可能是出于一种偏好,也可能是硬性要求。污点(Taint)则相反,它使节点能够排斥一类特定的Pod。
容忍(Tolerations)是应用于Pod上的,允许(但并不要求)Pod调度到带有与之匹配的污点的节点上。
污点和容忍(Tolerations)相互配合,可以用来避免Pod被分配到不合适的节点上。每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的Pod,是不会被该节点接受的。
可以使用kubectl taint命令给节点增加污点,节点被添加上污点之后就和Pod之间存在了一种相斥的关系,可以让节点拒绝Pod的调度执行,甚至将该节点上已经存在的Pod驱逐出去。
每个污点的组成如下:
key=value:effect
上面的key是污点的标签的键,value是污点的标签的值,其中value值可以为空,effect用来描述污点的作用,它支持如下三个选项:
NoSchedule
:表示k8s将不会把Pod调度到具有该污点的节点上;PreferNoSchedule
:表示k8s将尽量避免把Pod调度到具有该污点的节点上;NoExecute
:表示k8s将不会把Pod调度到具有该污点的节点上,同时会将该节点上已经存在的Pod驱逐出去。污点的新增、查看和删除:
# 新增污点。其中key值为check,value值为gongsl,effect为NoExecute
kubectl taint nodes node1 check=gongsl:NoExecute
# 查看污点
kubectl describe nodes node1|grep Taint
# 删除污点。其中key值为check
kubectl taint nodes node1 check:NoExecute-
下面进行一个案例演示:
当某种条件为真时,节点控制器会自动给节点添加一个污点。当前内置的污点包括:
node.kubernetes.io/not-ready
:节点未准备好。这相当于节点状态Ready
的值为False
;node.kubernetes.io/unreachable
:节点控制器访问不到节点。这相当于节点状态Ready
的值为Unknown
;node.kubernetes.io/out-of-disk
:节点磁盘耗尽;node.kubernetes.io/memory-pressure
:节点存在内存压力;node.kubernetes.io/disk-pressure
:节点存在磁盘压力;node.kubernetes.io/network-unavailable
:节点网络不可用;node.kubernetes.io/unschedulable
: 节点不可调度;node.cloudprovider.kubernetes.io/uninitialized
:如果kubelet启动时指定了一个"外部"云平台驱动,它将给当前节点添加一个污点将其标志为不可用。在cloud-controller-manager的一个控制器初始化这个节点后,kubelet将删除这个污点。设置了污点的节点将根据taint的effect(NoSchedule、PreferNoSchedule、NoExecute)和Pod之间产生互斥的关系,Pod将在一定程度上不会被调度到该节点上。但我们可以在Pod上设置容忍(Tolerations),设置了容忍的Pod将可以容忍污点的存在,可以被调度到存在污点的的节点上。
在k8s中,是使用Pod中spec属性下的tolerations
属性进行Pod容忍的设置的。下面是Pod容忍可能涉及的相关设置:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
tolerationSeconds: 3600
- key: "key2"
operator: "Exists"
effect: "NoSchedule"
key=value:effect
保持一致;Equal
,此时的value属性值需要和污点的value值相对应,operator属性的值还可以是Exists
,此时将会忽略value属性,所以这时value属性是不用写的;特殊情况说明:
Exists
,表示这个容忍度与任意的key、value和effect都匹配,即这个容忍度能容忍任意污点(taint)。tolerations:
- operator: "Exists"
tolerations:
- key: "key1"
operator: "Exists"
关于Pod容忍的案例:
先新建两个Pod,对应的yaml文件如下所示:
apiVersion: v1
kind: Pod
metadata:
name: pod-nginx1
spec:
containers:
- name: nginx
image: nginx:1.20
imagePullPolicy: IfNotPresent
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 120
---
apiVersion: v1
kind: Pod
metadata:
name: pod-nginx2
spec:
containers:
- name: nginx
image: nginx:1.20
imagePullPolicy: IfNotPresent
tolerations:
- key: "key2"
operator: "Exists"
effect: "NoExecute"
既然都运行在了node1节点上,那就给node1节点打污点,演示驱逐效果:
#执行如下命令,给node1节点打一个污点,这里用的是只有key值没有value值的污点
kubectl taint nodes node1 key1:NoExecute
节点打污点后,由于名为pod-nginx2的Pod没有匹配键值为key1的容忍,所以该Pod会被立即驱逐,名为pod-nginx1的Pod虽然有匹配键值为key1的容忍,但是由于将tolerationSeconds
设置为了120,所以120秒后,该Pod也会被驱逐。
我们可以给一个节点添加多个污点,也可以给一个Pod添加多个容忍。k8s处理多个污点和容忍的过程就像一个过滤器:从一个节点的所有污点开始遍历,过滤掉那些Pod中存在与之相匹配的容忍度的污点。余下未被过滤的污点的effect值决定了Pod是否会被分配到该节点,特别是以下情况:
NoSchedule
的污点,则k8s不会将分配到该节点;NoSchedule
的污点,但是存在effect值为PreferNoSchedule
的污点,则Kubernetes会尝试不将Pod分配到该节点;NoExecute
的污点,则Kubernetes不会将Pod分配到该节点(如果 Pod还未在节点上运行),或者将Pod从该节点驱逐(如果Pod已经在节点上运行)。例如,假设我们给一个节点添加了如下污点:
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule
假定有一个Pod,它有两个容忍度:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
在这种情况下,上述Pod不会被分配到上述节点,因为其没有容忍度和第三个污点相匹配。但是如果在给节点添加上述污点之前,该Pod已经在上述节点运行,那么它还可以继续运行在该节点上,因为不能和上述Pod相匹配的上述节点的第三个污点的effect值并不是NoExecute,所以不会驱逐已经运行在节点上的Pod。
前文提到过污点的effect值NoExecute
会影响已经在节点上运行的Pod:
NoExecute
的污点,那么Pod将马上被驱逐;NoExecute
的污点,但是在容忍度定义中没有指定tolerationSeconds
,则Pod还会一直在这个节点上运行;NoExecute
的污点,而且指定了tolerationSeconds
,则Pod还能在这个节点上继续运行这个指定的时间长度。注意事项:
k8s会自动给Pod添加一个key为node.kubernetes.io/not-ready
的容忍度并配置 tolerationSeconds=300
,除非用户提供的Pod配置中已经已存在了key为node.kubernetes.io/not-ready
的容忍度;
k8s也会给Pod添加一个key为node.kubernetes.io/unreachable
的容忍度并配置tolerationSeconds=300
,除非用户提供的Pod配置中已经已存在了key为node.kubernetes.io/unreachable
的容忍度;
DaemonSet中的Pod被创建时,针对以下污点自动添加的NoExecute
的容忍度将不会指定tolerationSeconds
:
node.kubernetes.io/unreachable
node.kubernetes.io/not-ready
这保证了出现上述问题时DaemonSet中的Pod永远不会被驱逐。
DaemonSet控制器自动为所有守护进程添加如下NoSchedule
容忍度以防DaemonSet崩溃:
node.kubernetes.io/memory-pressure
node.kubernetes.io/disk-pressure
node.kubernetes.io/out-of-disk
(只适合关键Pod)node.kubernetes.io/unschedulable
(1.10或更高版本)node.kubernetes.io/network-unavailable
(只适合主机网络配置)添加上述容忍度确保了向后兼容,我们也可以选择自由向DaemonSet添加容忍度。