为了使Pods分配到特定的Nodes上运行,K8s提出了两种解决方案:nodeSelector和nodeAffinity。
nodeSelector
nodeSelector是一种最简单的指定Pod运行在哪一个Node上的方式。它的使用简单粗暴,直接给node打上一个label,然后在Pod的定义中添加一个nodeSelector即可。
- 为node-1打上一个"disktype=ssd"的label
kubectl label nodes node-1 disktype=ssd
- 在Pod的定义中添加一个nodeSelector
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd # 对应node-1的label
通过label选择器的方式,这样这个名为nginx的Pod就会调度到拥有"disktype=ssd"的label的Node上运行。如果K8s集群找不到任何一个拥有"disktype=ssd"的label的Node,那么这个Pod就会处于Pending状态(即无法运行成功)。
nodeAffinity
nodeAffinity提供的功能与nodeSelector类似,都是用于指定Pod运行在哪些Node上。但是nodeAffinity拥有更加复杂的语法和更加强大的功能。下面是一个例子:
apiVersion: v1
kind: Pod
metadata:
name: database
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values: [ "database-require" ]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: role
operator: In
values: [ "database-prefer" ]
目前nodeAffinity支持两种模式:requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution
requiredDuringSchedulingIgnoredDuringExecution:可以分为两部分requiredDuringScheduling和IgnoredDuringExecution。requiredDuringScheduling意味着K8s调度器必须将这个Pod调度到目标Node(这个目标Node由matchExpressions决定)上,否则像nodeSelector一样,Pod的状态将处于Pending。IgnoredDuringExecution意味着这个node affinity的过程仅仅作用于Pod调度阶段,它不适用与已经处于运行中的Pod。也就是说,如果一个Pod被分配到node1之后,此时node1的label被修改了(修改后不满足这个Pod的nodeAffinity了),此时K8s并不会重新调度这个Pod。
preferredDuringSchedulingIgnoredDuringExecution:同样的,preferredDuringScheduling意味着一种倾向性,K8s调度器首先尝试将Pod分配到满足node affinity的目标Node上,如果找不到这样的Node,那么允许将其分配到其他Node上。
matchExpressions中的operator可选值包括:In, NotIn, Exists, DoesNotExist, Gt, Lt。可以使用NotIn和DoesNotExist实现node anti-affinity,或者使用taints(稍后会提到)来实现node anti-affinity。
如果一个nodeAffinity中包含了多个nodeSelectorTerms,那么Pod可以被分配到任意一个满足了nodeSelectorTerms的Node上。
如果一个nodeSelectorTerms包含了多个matchExpressions,那么Pod分配的Node必须完全满足所有的atchExpressions。
preferredDuringSchedulingIgnoredDuringExecution中的weight字段,其取值范围为1-100,用于定义每一个preference的权重。每满足一个preference,该Node的得分就会增加(增加的值即为这个weight)。最终K8s会统计每一个Node的得分,从而选择得分最高的Node作为目标Node。
podAffinity和podAntiAffinity
podAffinity用于根据已经在Node上运行的Pod的标签来限制Pod调度在哪个Node上。例如有些时候出于性能考虑,我们希望将某2个Pod分配在同一个Node上,此时我们就能够使用nodeAffinity。下面是一个例子:
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: k8s.gcr.io/pause:2.0
例如上面的例子中,requiredDuringSchedulingIgnoredDuringExecution中的定义表示,必须将这个Pod调度到一个运行着拥有label:security=S1的Pod的Node上。也就是说,当且仅当某个Node上运行着一个Pod,并且这个Pod拥有security=S1的label,那么这个例子中Pod才能分配到这个Node上。
podAntiAffinity则刚好相反,如果某个Node拥有了满足matchExpressions的Pod,那么久不会将这个Pod分配到这个Node上。
注意到podAffinity和podAntiAffinity还包含一个属性:topologyKey。topologyKey本身代表着一个label的key,它用于限定Node范围。因此nodeAffinity的规则是:如果 X 已经运行一个或多个符合规则 Y 的 pod,那么这个 pod 应该运行在 X 上。这里的Y就是matchExpressions定义的规则。而X则是这里的topologyKey,例如上面的kubernetes.io/hostname将Node的范围限定在拥有"kubernetes.io/hostname"这个label的Node中。通常情况下,"kubernetes.io/hostname" 是Node默认就拥有的Label,Node默认拥有的其他Label包括:failure-domain.beta.kubernetes.io/zone,failure-domain.beta.kubernetes.io/region,topology.kubernetes.io/zone,topology.kubernetes.io/region,beta.kubernetes.io/instance-type,node.kubernetes.io/instance-type,kubernetes.io/os,kubernetes.io/arch。(参考资料:https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#built-in-node-labels)
Taints和Tolerations
如果说我们可以使用nodeSelector和nodeAffinity来表达Pod对于Node的亲和性,那么Taints和Tolerations就是用于表达Pod对于Node的反亲和性。换种方式说,nodeSelector/nodeAffinity是Pod的一个属性,将其吸引到一个特定的Node集合上;而Taint则刚好相反,它允许Node排斥Pod。
Taint中文译为污点,它作用于Node,当为Node打上污点后,任何没有显示声明能够容忍这个污点的Pod都不会被调度到这个Node上。Pod通过定义Tolerations来声明自己能够容忍某些污点(Taints)。下面是Taints的使用例子:
kubectl taint nodes node1 key=value:NoSchedule
一个Taint由三部分组成:key,value和effect。格式为 key=value:effect。key/value类似于label的使用,而effect包含三个可选值:
- NoSchedule:打上这种effect的taint的Node,一定不会被调度上没有声明对应Toleration的Pod
- PreferNoSchedule:打上这种effect的taint的Node,尽可能不会被调度上没有声明对应Toleration的Pod
- NoExecute:不仅不会调度,还会驱逐Node上已有的没有声明对应Toleration的Pod
为Pod添加Toleration:
apiVersion: v1
kind: Pod
metadata:
name: database
spec:
tolerations:
- key: <key>
operator: "Equal"
value: <value>
effect: "NoSchedule"
声明了Toleration的Pod表示其可以容忍该污点,可以被调度到有该污点的Pod上。
可以添加多个 taint 到一个 node 上,也能添加多个 toleration 到一个 pod 上。 Kubernetes以过滤器的形式来处理多个taint和toleration的情况: 遍历 node 的所有 taint,如果 pod 拥有匹配的 toleration 则将 taint 忽略掉;最后剩下的没有被忽略的 taint 将作用于这个 pod。
我们提到了 NoExecute taint 的效果,它将对正在运行的 pod 产生如下影响:
- 不能容忍对应 taint 的 pod 将立即被驱逐
- 能够容忍对应 taint 但是没有在toleration specification中指定tolerationSeconds则将继续保持在原节点上
- 能够容忍对应 taint 并且指定 tolerationSeconds 则在原节点上保持指定的时间,指定时间到达后将被驱逐