K8s调度精讲:玩转nodeSelector与nodeAffinity,让Pod“精准空降”
大家好!在Kubernetes(K8s)的世界里,如何将我们的应用(Pod)优雅且准确地部署到合适的节点(Node)上,是一门必修课。K8s提供了多种调度策略,比如简单粗暴的nodeSelector,以及更为灵活的节点亲和性(nodeAffinity)、Pod亲和性(podAffinity)和Pod反亲和性(podAntiAffinity)。
今天,我们就先来深入聊聊nodeSelector和nodeAffinity这两个“老朋友”,看看它们如何帮助我们实现Pod的定向调度。
Pod的诞生之旅:调度器在其中扮演什么角色?
在我们深入探讨调度策略之前,先快速回顾一下一个Pod从创建请求到最终运行的大致流程。理解这个流程有助于我们明白调度器(Scheduler)在K8s体系中的核心地位。
- 用户请求:我们通过
kubectl(或其他客户端)向API Server发起创建Pod的请求(例如,kubectl apply -f my-pod.yaml)。 - 持久化配置:API Server接收到请求后,会将Pod的配置信息(期望状态)写入高可用的键值存储系统etcd中。
- 调度器感知:Scheduler通过List-Watch机制(稍后会简单介绍)监听到API Server中有新的、尚未被调度的Pod。
- 智能决策:Scheduler根据内置的调度算法(包括我们后面会讲的
nodeSelector、nodeAffinity等策略),从集群中选择一个最合适的Node。一旦选定,Scheduler会更新Pod的定义,在.spec.nodeName字段中填入目标节点的名称。这个更新同样会写回etcd。 - Kubelet接管:目标Node上的Kubelet组件,同样通过List-Watch机制监听到有新的Pod被分配给自己。
- 容器创建与运行:Kubelet读取Pod的配置,调用底层的容器运行时(如Docker、containerd)来创建和运行Pod中的容器。
- 状态上报:Kubelet会持续监控Pod和容器的状态,并将实际状态汇报给API Server,API Server再将这些信息更新到etcd。
- 用户查看:当我们执行
kubectl get pod时,API Server会从etcd中查询Pod的信息并返回给我们。
List-Watch机制简介
贯穿上述流程的“List-Watch”机制是K8s组件间通信的核心。简单来说:
- List:客户端(如Scheduler、Kubelet)首次连接API Server时,会获取(List)指定资源的当前完整列表。
- Watch:之后,客户端会建立一个长连接到API Server,监听(Watch)这些资源的后续变化(创建、更新、删除)。当有变化发生时,API Server会主动推送事件给客户端。
这种机制使得K8s的各个组件能够高效、实时地同步状态,实现了声明式API驱动的自动化运维。
什么是调度(Scheduling)?
一言以蔽之,调度就是为Pod找到一个合适的家(Node),让它能够健康运行。K8s集群的默认调度器是kube-scheduler。它的核心任务是:
- 发现新Pod:通过Watch机制发现未被调度的Pod。
- 筛选节点(Filtering):遍历集群中所有可用Node,过滤掉那些不满足Pod调度需求的Node。例如,Node资源不足、不满足Pod声明的
nodeSelector或nodeAffinity等。 - 节点打分(Scoring):对通过筛选阶段的Node进行打分,选出最优的Node。评分因素包括资源利用率均衡、满足
preferredDuringSchedulingIgnoredDuringExecution类型的亲和性规则等。 - 绑定决策:将Pod“绑定”到得分最高的Node上(如果有多个得分相同的,则随机选择一个)。
如果集群中没有一个Node能满足Pod的需求,那么这个Pod会一直处于Pending状态,直到有合适的Node出现,或者Pod的调度需求被修改。
nodeSelector:简单直接的节点选择器
nodeSelector是最简单直接的节点选择方式。你只需要给Node打上特定的标签(Label),然后在Pod的配置中指定这些标签,Pod就会被调度到带有这些标签的Node上。
特点:
- 强制性:如果指定的标签在集群中任何一个Node上都找不到(或者标签写错了),Pod将无法被调度,会一直处于
Pending状态。 - 简单直接:配置简单,易于理解。
- 逻辑与:如果
nodeSelector中指定了多个标签,那么Node必须 同时满足所有 这些标签才能被选中。
实战案例:将Pod调度到有GPU的节点
-
给Node打标签:
假设test-b-k8s-node02是一台配备了GPU的机器。# 给节点打上标签 gpu=true (注意:标签的value是字符串) kubectl label node test-b-k8s-node02 gpu="true" # 查看节点标签 kubectl get node test-b-k8s-node02 --show-labels # 或者查看所有节点的标签 kubectl get node --show-labels -
在Pod配置中指定
nodeSelector:
下面是一个Deployment的YAML示例,它会创建10个Pod副本,并要求它们都运行在具有gpu: "true"标签的Node上。apiVersion: v1 kind: Namespace metadata: name: gpu-apps --- apiVersion: apps/v1 kind: Deployment metadata: name: goweb-gpu-demo namespace: gpu-apps spec: replicas: 10 selector: matchLabels: app: goweb-gpu-demo template: metadata: labels: app: goweb-gpu-demo spec: nodeSelector: gpu: "true" # 这里的 "true" 必须是字符串 containers: - name: goweb-gpu-container image: your-repo/goweb-demo:latest # 替换成你的镜像重要提示:Node标签的Value是字符串。即使你打标签时用
gpu=true,在nodeSelector中也应该写成gpu: "true"。如果写成gpu: true(布尔值),YAML解析器会将其视为布尔型,而标签匹配是基于字符串的,会导致匹配失败。为了避免混淆,建议标签值尽量避免使用纯粹的true/false,或者始终在YAML中用引号包围。 -
部署与验证:
kubectl apply -f your-deployment.yaml kubectl get pods -n gpu-apps -o wide你会看到所有10个Pod都运行在
test-b-k8s-node02上。如果标签打错了或Node不存在对应标签会怎样?
假设你把gpu: "true"写成了gpu: "ture",或者集群中没有任何Node有gpu: "true"这个标签,那么这些Pod会一直处于Pending状态。通过kubectl describe pod <pod-name> -n gpu-apps可以看到事件(Events)部分会提示类似 "0/N nodes are available: N node(s) didn't match node selector." 的信息。 -
删除标签:
如果想移除Node的标签:kubectl label node test-b-k8s-node02 gpu- # 注意:key 和减号 "-" 之间不能有空格删除标签后,如果Pod的重启策略是
Always(Deployment中Pod的默认策略),且之前因为标签不匹配而处于Pending的Pod,不会自动重新调度。但如果这些Pod因为其他原因(如Node宕机)被重建,它们依然会因为nodeSelector不满足而Pending。新创建的Pod也会因为找不到匹配节点而Pending。
nodeAffinity:更灵活的节点亲和性
nodeSelector虽然简单,但表达能力有限。例如,你无法表达“尽量调度到A类型的节点,如果没有A类型,调度到B类型也可以”这样的软需求,也无法使用更复杂的逻辑操作符(如NotIn, Exists)。这时,nodeAffinity就派上用场了。
nodeAffinity提供了两种类型的亲和性规则:
-
requiredDuringSchedulingIgnoredDuringExecution(硬策略):- 必须满足:Pod调度时,Node 必须 满足这里定义的所有规则,否则Pod不会被调度到该Node。行为上类似
nodeSelector,但表达能力更强。 IgnoredDuringExecution:Pod成功调度到Node上运行后,如果该Node的标签发生了变化,不再满足此规则,Kubelet 不会 将Pod从该Node上驱逐。Pod会继续在该Node上运行。
- 必须满足:Pod调度时,Node 必须 满足这里定义的所有规则,否则Pod不会被调度到该Node。行为上类似
-
preferredDuringSchedulingIgnoredDuringExecution(软策略):- 尽量满足:调度器会 尝试 寻找满足这些规则的Node。如果找到了,会给这些Node打更高的分数,增加Pod被调度到其上的几率。
- 找不到也行:如果找不到完全满足偏好规则的Node,Pod仍然会被调度到其他符合硬策略(以及其他基本调度要求的)Node上。
IgnoredDuringExecution:同上,Pod运行后,Node标签变化不会导致Pod被驱逐。weight:每个偏好规则可以指定一个weight(权重),取值范围1-100。调度器在打分阶段,会对满足偏好规则的Node,将其weight值累加到该Node的总分上。权重越高的规则,其影响力越大。
nodeAffinity的强大之处在于其丰富的操作符和逻辑组合:
nodeSelectorTerms:是一个列表,列表中的多个条件是逻辑或(OR)的关系。只要Node满足nodeSelectorTerms中 任意一个term的条件即可。matchExpressions:在每个nodeSelectorTerm内部,可以定义一个或多个matchExpressions。这些expressions之间是逻辑与(AND)的关系。Node必须 同时满足 同一个term下的所有matchExpressions。- 操作符(
operator):In: 标签的value在给定的值列表中。NotIn: 标签的value不在给定的值列表中。Exists: Node上存在指定的标签key(不关心value)。DoesNotExist: Node上不存在指定的标签key。Gt: 标签的value大于给定的值(需要标签value是数字字符串)。Lt: 标签的value小于给定的值(需要标签value是数字字符串)。
注意:
- 如果同时指定了
nodeSelector和nodeAffinity,那么Node 必须同时满足 两者的条件。 nodeAffinity的NotIn和DoesNotExist操作符可以用来实现一定程度的节点反亲和性,即不希望Pod调度到某些特定类型的Node上。当然,K8s中更专门的节点反亲和机制是污点(Taints)和容忍(Tolerations),我们后续文章再详细探讨。
实战案例1:硬策略 + 软策略组合
假设我们希望Pod:
- 必须部署在属于
team-a或team-b的节点上(硬性要求)。 - 最好部署在品牌为
ibm的主机上(偏好性要求)。
-
准备Node标签:
kubectl label node test-b-k8s-node01 team="team-a" hostbrand="ibm" kubectl label node test-b-k8s-node02 team="team-b" kubectl label node test-b-k8s-node03 team="team-c" hostbrand="dell" -
Pod YAML (
pod-node-affinity-combo.yaml):apiVersion: v1 kind: Pod metadata: name: goweb-affinity-demo spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬策略 nodeSelectorTerms: - matchExpressions: - key: team operator: In values: - "team-a" - "team-b" preferredDuringSchedulingIgnoredDuringExecution: # 软策略 - weight: 1 # 权重,这里只有一个偏好规则,权重设为1即可 preference: matchExpressions: - key: hostbrand operator: In values: - "ibm" containers: - name: container-goweb-demo image: your-repo/goweb-demo:latest # 替换成你的镜像 -
部署与分析:
kubectl apply -f pod-node-affinity-combo.yaml kubectl get pod goweb-affinity-demo -o wide预期结果:
test-b-k8s-node03因为不满足硬策略(team不是team-a或team-b),首先被过滤掉。test-b-k8s-node01和test-b-k8s-node02都满足硬策略。- 在满足硬策略的节点中,
test-b-k8s-node01同时满足软策略(hostbrand: ibm),而test-b-k8s-node02不满足。 - 因此,Pod
goweb-affinity-demo会被调度到test-b-k8s-node01。
实战案例2:多重偏好与权重
假设我们希望Pod:
- 必须部署在属于
team-a的节点上(硬性要求)。 - 优先选择磁盘类型为
sas的节点 (权重50)。 - 如果找不到
sas盘的,选择ssd盘的节点也可以 (权重1,优先级较低)。
-
准备Node标签:
# Node1: 满足硬性,满足高权重偏好 kubectl label node k8s-node01 team="team-a" disktype="sas" # Node2: 满足硬性,满足低权重偏好 kubectl label node k8s-node02 team="team-a" disktype="ssd" # Node3: 满足硬性,不满足任何偏好 kubectl label node k8s-node03 team="team-a" disktype="hdd" # Node4: 不满足硬性 kubectl label node k8s-node04 team="team-b" disktype="sas" -
Pod YAML (
pod-weighted-affinity.yaml):apiVersion: v1 kind: Pod metadata: name: goweb-weighted-affinity spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: team operator: In values: - "team-a" preferredDuringSchedulingIgnoredDuringExecution: - weight: 50 # sas磁盘权重高 preference: matchExpressions: - key: disktype operator: In values: - "sas" - weight: 1 # ssd磁盘权重低 preference: matchExpressions: - key: disktype operator: In values: - "ssd" containers: - name: container-goweb-weighted image: your-repo/goweb-demo:latest # 替换成你的镜像 -
部署与分析:
kubectl apply -f pod-weighted-affinity.yaml kubectl get pod goweb-weighted-affinity -o wide预期结果:
k8s-node04因不满足硬策略(team不是team-a)被过滤。k8s-node01满足硬策略,且满足disktype: sas(权重50)。k8s-node02满足硬策略,且满足disktype: ssd(权重1)。k8s-node03满足硬策略,但不满足任何偏好规则(权重0)。- 调度器会计算总分,
k8s-node01的得分会因为满足高权重的偏好而最高,因此Pod会被调度到k8s-node01。 - 如果
k8s-node01不存在或资源不足,Pod会优先选择k8s-node02,其次是k8s-node03。
生产环境使用建议
- 优先使用
nodeAffinity:nodeAffinity比nodeSelector更具表达力,是推荐的主流方式。即使是简单的硬性需求,用nodeAffinity的requiredDuringSchedulingIgnoredDuringExecution也能实现,并且为未来扩展更复杂的规则留下空间。 - 标签规范化:为Node打标签时,建议使用统一的、有意义的前缀,例如公司/组织域名反写
com.mycompany.team或node.kubernetes.io/arch(K8s内置标签)。 - 理解
IgnoredDuringExecution:这意味着一旦Pod被调度,后续Node标签的变更不会影响已运行的Pod。如果需要Pod在Node标签变化后被重新调度(例如,Node不再符合要求时将其驱逐),nodeAffinity本身无法实现,需要结合其他工具如Descheduler,或更复杂的自定义控制器。 - 故障排查:当Pod长时间处于
Pending状态时,使用kubectl describe pod <pod-name> -n <namespace>查看Pod的Events,通常能找到调度失败的原因,如 "didn't match node selector" 或 "didn't match node affinity rules"。 - 适度使用:虽然亲和性规则很强大,但过度复杂的规则可能导致调度困难或集群资源分配不均。保持规则的简洁和必要性。
- 小心标签值类型:再次强调,Node标签的key和value都是字符串。在YAML中定义亲和性规则时,
values列表中的元素通常也应该是字符串(除非使用Gt,Lt操作符且你知道标签值可以被解析为数字)。
总结
nodeSelector和nodeAffinity是K8s中控制Pod部署位置的强大工具。
nodeSelector简单直接,适用于固定的、强制性的节点选择。nodeAffinity则提供了更丰富的表达能力,支持软/硬策略、复杂的逻辑操作,让我们能更精细化地管理Pod的调度行为。
掌握了它们,你就向着K8s调度大师的目标又迈进了一大步!在后续的文章中,我们还会探讨Pod亲和性/反亲和性,以及污点和容忍等更高级的调度特性,敬请期待!
希望这篇整理和补充对你和你的粉丝有所帮助!去实践一下吧,你会发现它们在实际生产中非常有用。
浙公网安备 33010602011771号