Kubernetes之Pod调度约束即将Pod分配给节点
参考:https://kubernetes.io/zh/docs/concepts/configuration/assign-pod-node/
可以约束一个Pod只能在特定的Nodes上运行,或者有限运行在特定的节点上。有几种方法可以实现这点,推荐的方法都是用标签选择器进行选择。通常这种约束不是必须的,因为调度器将自行进行合理的放置(比如,将Pod分散到节点上,而不是将Pod放置在资源不足的节点上),但是在某种情况下,可以需要更多控制pod停靠的节点,例如确保pod最终落在连接了SSD的机器上,或者将来自两个不同服务且有大量通信的pod放置在同一个可用区。
nodeSelector
nodeSelector
是节点选择约束的最简单推荐形式。nodeSelector
是 PodSpec 的一个字段。它指定键值对的映射。为了使 pod 可以在节点上运行,节点必须具有每个指定的键值对作为标签(它也可以具有其他标签)。最常用的是一对键值对。
让我们来看一个使用 nodeSelector
的例子。
步骤零:先决条件
本示例假设你已基本了解 Kubernetes 的 pod 并且已经建立一个 Kubernetes 集群。
步骤一:添加标签到节点
执行 kubectl get nodes
命令获取集群的节点名称。选择一个你要增加标签的节点,然后执行 kubectl label nodes <node-name> <label-key>=<label-value>
命令将标签添加到你所选择的节点上。例如,如果你的节点名称为 ‘kubernetes-foo-node-1.c.a-robinson.internal’ 并且想要的标签是 ‘disktype=ssd’,则可以执行 kubectl label nodes kubernetes-foo-node-1.c.a-robinson.internal disktype=ssd
命令。
你可以通过重新运行 kubectl get nodes --show-labels
并且查看节点当前具有了一个标签来验证它是否有效。你也可以使用 kubectl describe node "nodename"
命令查看指定节点的标签完整列表。
获取node节点名称
kubectl get nodes NAME STATUS ROLES AGE VERSION 192.168.1.65 Ready <none> 10d v1.17.4 192.168.1.66 Ready <none> 10d v1.17.4
添加标签
kubectl label nodes 192.168.1.65 disktype=ssd node/192.168.1.65 labeled
查看添加的标签
# kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS 192.168.1.65 Ready <none> 10d v1.17.4 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/arch=amd64,kubernetes.io/hostname=192.168.1.65,kubernetes.io/os=linux 192.168.1.66 Ready <none> 10d v1.17.4 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=192.168.1.66,kubernetes.io/os=linux
可以看到node192.168.1.65多了一个添加的标签disktype=ssd而node192.168.1.66则没有该标签
步骤二:添加 nodeSelector 字段到 pod 配置中
拿任意一个你想运行的 pod 的配置文件,并且在其中添加一个 nodeSelector 部分。例如,如果下面是我的 pod 配置:
apiVersion: v1 kind: Pod metadata: name: nginx-pod labels: env: test spec: containers: - name: nginx image: nginx
然后像下面这样添加 nodeSelector:
# cat nginx-pod.yaml apiVersion: v1 kind: Pod metadata: name: nginx-pod labels: env: test spec: containers: - name: nginx image: nginx nodeSelector: disktype: ssd
当你之后运行 kubectl apply -f pod-nginx.yaml
命令,pod 将会调度到将标签添加到的节点上。你可以通过运行 kubectl get pods -o wide
并查看分配给 pod 的 “NODE” 来验证其是否有效。
kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-pod 1/1 Running 0 3m10s 172.17.54.8 192.168.1.65 <none> <none>
插曲:内置的节点标签
除了你附加的标签外,节点还预先填充了一组标准标签。这些标签是
kubernetes.io/hostname
failure-domain.beta.kubernetes.io/zone
failure-domain.beta.kubernetes.io/region
beta.kubernetes.io/instance-type
kubernetes.io/os
kubernetes.io/arch
注意: 这些标签的值特定于云供应商的,因此不能保证可靠。例如,kubernetes.io/hostname 的值在某些环境中可能与节点名称相同,但在其他环境中可能是一个不同的值。
节点隔离/限制
向 Node 对象添加标签可以将 pod 定位到特定的节点或节点组。这可以用来确保指定的 pod 只能运行在具有一定隔离性,安全性或监管属性的节点上。当为此目的使用标签时,强烈建议选择节点上的 kubelet 进程无法修改的标签键。这可以防止受感染的节点使用其 kubelet 凭据在自己的 Node 对象上设置这些标签,并影响调度器将工作负载调度到受感染的节点。
NodeRestriction
准入插件防止 kubelet 使用 node-restriction.kubernetes.io/
前缀设置或修改标签。要使用该标签前缀进行节点隔离:
- 检查是否在使用 Kubernetes v1.11+,以便 NodeRestriction 功能可用。
- 确保你在使用节点授权并且已经启用 NodeRestriction 准入插件。
- 将
node-restriction.kubernetes.io/
前缀下的标签添加到 Node 对象,然后在节点选择器中使用这些标签。例如,example.com.node-restriction.kubernetes.io/fips=true
或example.com.node-restriction.kubernetes.io/pci-dss=true
。
亲和与反亲和
nodeSelector提供了一种非常简单的方法来将Pod约束在具有特定节点标签的节点上。亲和/反亲和功能极大地扩展了你可以表达约束的类型。关键的增强点是
- 语言更具表现力(不仅仅是“完全匹配的 AND”)
- 你可以发现规则是“软”/“偏好”,而不是硬性要求,因此,如果调度器无法满足该要求,仍然调度该 pod
- 你可以使用节点上(或其他拓扑域中)的 pod 的标签来约束,而不是使用节点本身的标签,来允许哪些 pod 可以或者不可以被放置在一起。
节点亲和
节点亲和概念上类似于nodeSelector,它使你可以根据节点上的标签来约束pod可以调度到那些节点。
目前有两种类型的节点亲和,分别为 requiredDuringSchedulingIgnoredDuringExecution
和 preferredDuringSchedulingIgnoredDuringExecution
。你可以视它们为“硬”和“软”,意思是,前者指定了将 pod 调度到一个节点上必须满足的规则(就像 nodeSelector
但使用更具表现力的语法),后者指定调度器将尝试执行单不能保证的偏好。“IgnoredDuringExecution”部分意味着,类似于 nodeSelector
的工作原理,如果节点的标签在运行时发生变更,从而不再满足 pod 上的亲和规则,那么 pod 将仍然继续在该节点上运行。将来我们计划提供 requiredDuringSchedulingRequiredDuringExecution
,它将类似于 requiredDuringSchedulingIgnoredDuringExecution
,除了它会将 pod 从不再满足 pod 的节点亲和要求的节点上驱逐。
因此,requiredDuringSchedulingIgnoredDuringExecution
的示例将是“仅将 pod 运行在具有 Intel CPU 的节点上”,而 preferredDuringSchedulingIgnoredDuringExecution
的示例为“尝试将这组 pod 运行在 XYZ 故障区域,如果这不可能的话,则允许一些 pod 在其他地方运行”。
节点亲和通过 PodSpec 的 affinity
字段下的 nodeAffinity
字段进行指定。
下面是一个使用节点亲和的 pod 的实例:
# cat pod-with-node-affinity.yaml apiVersion: v1 kind: Pod metadata: name: with-node-affinity spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/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: kubernetes/pause
PS:如未匹配到任何标签选择则Pod会处于Pending状态
1,此节点亲和规则表示,pod 只能放置在具有标签键为 kubernetes.io/e2e-az-name
且 标签值为 e2e-az1
或 e2e-az2
的节点上。另外,在满足这些标准的节点中,具有标签键为 another-node-label-key
且标签值为 another-node-label-value
的节点应该优先使用。
查看nodes默认标签
# kubectl get node --show-labels NAME STATUS ROLES AGE VERSION LABELS 192.168.1.65 Ready <none> 10d v1.17.4 kubernetes.io/arch=amd64,kubernetes.io/hostname=192.168.1.65,kubernetes.io 192.168.1.66 Ready <none> 10d v1.17.4 kubernetes.io/arch=amd64,kubernetes.io/hostname=192.168.1.66,kubernetes.io
node 192.168.1.65添加一个标签kubernetes.io/e2e-az-name值为e2e-az1
# kubectl label node 192.168.1.65 kubernetes.io/e2e-az-name=e2e-az1 node/192.168.1.65 labeled
查看添加标签以后的node标签项node192.168.1.65增加了一个标签项
# kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS 192.168.1.65 Ready <none> 10d v1.17.4 kubernetes.io/arch=amd64,kubernetes.io/e2e-az-name=e2e-az1,kubernetes.io/hostname=192.168.1.65,kubernetes.io/os=linux 192.168.1.66 Ready <none> 10d v1.17.4 kubernetes.io/arch=amd64,kubernetes.io/hostname=192.168.1.66,kubernetes.io/os=linux
应用
# kubectl apply -f pod-with-node-affinity.yaml pod/with-node-affinity unchanged
查看是否分配到对应的node
# kubectl get pod with-node-affinity -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES with-node-affinity 1/1 Running 0 54s 172.17.54.8 192.168.1.65 <none> <none>
如果一个node满足标签another-node-label-key=another-node-label-key但是不满足标签kubernetes.io/e2e-az-name=e2e-az1则也不会分配到此节点因为requiredDuringSchedulingIgnoredDuringExecution是必须满足的
node192.168.1.66增加标签another-node-label-key=another-node-label-key
kubectl label node 192.168.1.66 another-node-label-key=another-node-label-key node/192.168.1.66 labeled
查看node标签项
kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS 192.168.1.65 Ready <none> 10d v1.17.4 kubernetes.io/arch=amd64,kubernetes.io/e2e-az-name=e2e-az1,kubernetes.io/hostname=192.168.1.65,kubernetes.io/os=linux 192.168.1.66 Ready <none> 10d v1.17.4 another-node-label-key=another-node-label-key,kubernetes.io/arch=amd64,kubernetes.io/hostname=192.168.1.66,kubernetes.io/os=linux
删除以后再应用
# kubectl delete -f pod-with-node-affinity.yaml pod "with-node-affinity" deleted # kubectl apply -f pod-with-node-affinity.yaml pod/with-node-affinity created
查看还是被分配到节点192.168.1.65上
# kubectl get pod with-node-affinity -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES with-node-affinity 1/1 Running 0 29s 172.17.54.8 192.168.1.65 <none> <none>
给node192.168.1.66也增加标签kubernetes.io/e2e-az-name=e2e-az1
# kubectl label node 192.168.1.66 kubernetes.io/e2e-az-name=e2e-az1 node/192.168.1.66 labeled
查看node标签可以看到node192.168.1.66除了满足标签kubernetes.io/e2e-az-name=e2e-az1还满足标签another-node-label-key=another-node-label-key
# kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS 192.168.1.65 Ready <none> 10d v1.17.4 kubernetes.io/arch=amd64,kubernetes.io/e2e-az-name=e2e-az1,kubernetes.io/hostname=192.168.1.65,kubernetes.io/os=linux 192.168.1.66 Ready <none> 10d v1.17.4 another-node-label-key=another-node-label-key,kubernetes.io/arch=amd64,kubernetes.io/e2e-az-name=e2e-az1,kubernetes.io/hostname=192.168.1.66
需要删除重新创建才会重新调度,因为亲和选择只在pod调度期间有效,参考下面第六条
# kubectl delete -f pod-with-node-affinity.yaml pod "with-node-affinity" deleted # kubectl apply -f pod-with-node-affinity.yaml pod/with-node-affinity created
查看,调度到node192.168.1.66上了,因为node192.168.1.66标签满足了kubernetes.io/e2e-az-name=e2e-az1的同时又满足了标签another-node-label-key=another-node-label-key所以优先调度
# kubectl get pod with-node-affinity -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES with-node-affinity 1/1 Running 0 43s 172.17.71.3 192.168.1.66 <none> <none>
2,你可以在上面的例子中看到 In
操作符的使用。新的节点亲和语法支持下面的操作符: In
,NotIn
,Exists
,DoesNotExist
,Gt
,Lt
。你可以使用 NotIn
和 DoesNotExist
来实现节点反亲和行为,或者使用节点污点将 pod 从特定节点中驱逐。
3,如果你同时指定了 nodeSelector
和 nodeAffinity
,两者必须都要满足,才能将 pod 调度到候选节点上。
4,如果你指定了多个与 nodeAffinity
类型关联的 nodeSelectorTerms
,则如果其中一个 nodeSelectorTerms
满足的话,pod将可以调度到节点上。
, 5,如果你指定了多个与 nodeSelectorTerms
关联的 matchExpressions
,则只有当所有 matchExpressions
满足的话,pod 才会可以调度到节点上。
6,如果你修改或删除了 pod 所调度到的节点的标签,pod 不会被删除。换句话说,亲和选择只在 pod 调度期间有效。
preferredDuringSchedulingIgnoredDuringExecution
中的 weight
字段值的范围是 1-100。对于每个符合所有调度要求(资源请求,RequiredDuringScheduling 亲和表达式等)的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的MatchExpressions,则添加“权重”到总和。然后将这个评分与该节点的其他优先级函数的评分进行组合。总分最高的节点是最优选的。
Pod间亲和与反亲和
pod 间亲和与反亲和使你可以基于已经在节点上运行的 pod 的标签来约束 pod 可以调度到的节点,而不是基于节点上的标签。规则的格式为“如果 X 节点上已经运行了一个或多个 满足规则 Y 的pod,则这个 pod 应该(或者在非亲和的情况下不应该)运行在 X 节点”。Y 表示一个具有可选的关联命令空间列表的 LabelSelector;与节点不同,因为 pod 是命名空间限定的(因此 pod 上的标签也是命名空间限定的),因此作用于 pod 标签的标签选择器必须指定选择器应用在哪个命名空间。从概念上讲,X 是一个拓扑域,如节点,机架,云供应商地区,云供应商区域等。你可以使用 topologyKey
来表示它,topologyKey
是节点标签的键以便系统用来表示这样的拓扑域。请参阅上面插曲:内置的节点标签部分中列出的标签键。
注意: Pod 间亲和与反亲和需要大量的处理,这可能会显著减慢大规模集群中的调度。我们不建议在超过数百个节点的集群中使用它们。
注意: Pod 反亲和需要对节点进行一致的标记,即集群中的每个节点必须具有适当的标签能够匹配 topologyKey。如果某些或所有节点缺少指定的 topologyKey 标签,可能会导致意外行为。
与节点亲和一样,当前有两种类型的 pod 亲和与反亲和,即 requiredDuringSchedulingIgnoredDuringExecution
和 preferredDuringSchedulingIgnoredDuringExecution
,分表表示“硬性”与“软性”要求。请参阅前面节点亲和部分中的描述。requiredDuringSchedulingIgnoredDuringExecution
亲和的一个示例是“将服务 A 和服务 B 的 pod 放置在同一区域,因为它们之间进行大量交流”,而 preferredDuringSchedulingIgnoredDuringExecution
反亲和的示例将是“将此服务的 pod 跨区域分布”(硬性要求是说不通的,因为你可能拥有的 pod 数多于区域数)。
Pod 间亲和通过 PodSpec 中 affinity
字段下的 podAffinity
字段进行指定。而 pod 间反亲和通过 PodSpec 中 affinity
字段下的 podAntiAffinity
字段进行指定。
Pod 使用 pod 亲和 的示例
# cat pod-with-pod-affinity.yaml apiVersion: v1 kind: Pod metadata: name: with-pod-affinity spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: security operator: In values: - S1 topologyKey: failure-domain.beta.kubernetes.io/zone podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: security operator: In values: - S2 topologyKey: failure-domain.beta.kubernetes.io/zone containers: - name: with-pod-affinity image: k8s.gcr.io/pause:2.0
在这个 pod 的 affinity 配置定义了一条 pod 亲和规则和一条 pod 反亲和规则。在此示例中,podAffinity
配置为 requiredDuringSchedulingIgnoredDuringExecution
,然而 podAntiAffinity
配置为 preferredDuringSchedulingIgnoredDuringExecution
。pod 亲和规则表示,仅当节点和至少一个已运行且有键为“security”且值为“S1”的标签的 pod 处于同一区域时,才可以将该 pod 调度到节点上。(更确切的说,如果节点 N 具有带有键 failure-domain.beta.kubernetes.io/zone
和某个值 V 的标签,则 pod 有资格在节点 N 上运行,以便集群中至少有一个节点具有键 failure-domain.beta.kubernetes.io/zone
和值为 V 的节点正在运行具有键“security”和值“S1”的标签的 pod。)pod 反亲和规则表示,如果节点已经运行了一个具有键“security”和值“S2”的标签的 pod,则该 pod 不希望将其调度到该节点上。(如果 topologyKey
为 failure-domain.beta.kubernetes.io/zone
,则意味着当节点和具有键“security”和值“S2”的标签的 pod 处于相同的区域,pod 不能被调度到该节点上。)查阅设计文档来获取更多 pod 亲和与反亲和的样例,包括 requiredDuringSchedulingIgnoredDuringExecution
和 preferredDuringSchedulingIgnoredDuringExecution
两种配置。
Pod 亲和与反亲和的合法操作符有 In
,NotIn
,Exists
,DoesNotExist
。
原则上,topologyKey
可以是任何合法的标签键。然而,出于性能和安全原因,topologyKey 受到一些限制:
- 对于亲和与
requiredDuringSchedulingIgnoredDuringExecution
要求的 pod 反亲和,topologyKey
不允许为空。 - 对于
requiredDuringSchedulingIgnoredDuringExecution
要求的 pod 反亲和,准入控制器LimitPodHardAntiAffinityTopology
被引入来限制topologyKey
不为kubernetes.io/hostname
。如果你想使它可用于自定义拓扑结构,你必须修改准入控制器或者禁用它。 - 对于
preferredDuringSchedulingIgnoredDuringExecution
要求的 pod 反亲和,空的topologyKey
被解释为“所有拓扑结构”(这里的“所有拓扑结构”限制为kubernetes.io/hostname
,failure-domain.beta.kubernetes.io/zone
和failure-domain.beta.kubernetes.io/region
的组合)。 - 除上述情况外,
topologyKey
可以是任何合法的标签键。
除了 labelSelector
和 topologyKey
,你也可以指定表示命名空间的 namespaces
队列,labelSelector
也应该匹配它(这个与 labelSelector
和 topologyKey
的定义位于相同的级别)。如果忽略或者为空,则默认为 pod 亲和/反亲和的定义所在的命名空间。
所有与 requiredDuringSchedulingIgnoredDuringExecution
亲和与反亲和关联的 matchExpressions
必须满足,才能将 pod 调度到节点上。
更实际的用例
Pod 间亲和与反亲和在与更高级别的集合(例如 ReplicaSets,StatefulSets,Deployments 等)一起使用时,它们可能更加有用。可以轻松配置一组应位于相同定义拓扑(例如,节点)中的工作负载。
在三节点集群中,一个 web 应用程序具有内存缓存,例如 redis。我们希望 web 服务器尽可能与缓存放置在同一位置。
下面是一个简单 redis deployment 的 yaml 代码段,它有三个副本和选择器标签 app=store
。Deployment 配置了 PodAntiAffinity
,用来确保调度器不会将副本调度到单个节点上。
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:3.2-alpine
下面 webserver deployment 的 yaml 代码段中配置了 podAntiAffinity
和 podAffinity
。这将通知调度器将它的所有副本与具有 app=store
选择器标签的 pod 放置在一起。这还确保每个 web 服务器副本不会调度到单个节点上。
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.12-alpine
创建Deployment以后
web-server
的两个副本都按照预期那样自动放置在同一位置。
# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-cache-6bc7d5b59d-wnm8l 1/1 Running 0 4m29s 172.17.71.4 192.168.1.66 <none> <none> redis-cache-6bc7d5b59d-zhb96 1/1 Running 0 4m29s 172.17.54.8 192.168.1.65 <none> <none> web-server-655bf8bdf4-5fk94 1/1 Running 0 3m43s 172.17.54.9 192.168.1.65 <none> <none> web-server-655bf8bdf4-qbrz8 1/1 Running 0 3m43s 172.17.71.6 192.168.1.66 <none> <none>
永远不放置在相同节点
上面的例子使用 PodAntiAffinity
规则和 topologyKey: "kubernetes.io/hostname"
来部署 redis 集群以便在同一主机上没有两个实例。参阅 ZooKeeper 教程,以获取配置反亲和来达到高可用性的 StatefulSet 的样例(使用了相同的技巧)。
nodeName
nodeName
是节点选择约束的最简单方法,但是由于其自身限制,通常不使用它。nodeName
是 PodSpec 的一个字段。如果它不为空,调度器将忽略 pod,并且运行在它指定节点上的 kubelet 进程尝试运行该 pod。因此,如果 nodeName
在 PodSpec 中指定了,则它优先于上面的节点选择方法。
使用 nodeName
来选择节点的一些限制:
- 如果指定的节点不存在,
- 如果指定的节点没有资源来容纳 pod,pod 将会调度失败并且其原因将显示为,比如 OutOfmemory 或 OutOfcpu。
- 云环境中的节点名称并非总是可预测或稳定的。
下面的是使用 nodeName
字段的 pod 配置文件的例子:
apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx nodeName: kube-01
上面的 pod 将运行在 kube-01 节点上。