K8s调度精讲:玩转nodeSelector与nodeAffinity,让Pod“精准空降”

大家好!在Kubernetes(K8s)的世界里,如何将我们的应用(Pod)优雅且准确地部署到合适的节点(Node)上,是一门必修课。K8s提供了多种调度策略,比如简单粗暴的nodeSelector,以及更为灵活的节点亲和性(nodeAffinity)、Pod亲和性(podAffinity)和Pod反亲和性(podAntiAffinity)。

今天,我们就先来深入聊聊nodeSelectornodeAffinity这两个“老朋友”,看看它们如何帮助我们实现Pod的定向调度。

Pod的诞生之旅:调度器在其中扮演什么角色?

在我们深入探讨调度策略之前,先快速回顾一下一个Pod从创建请求到最终运行的大致流程。理解这个流程有助于我们明白调度器(Scheduler)在K8s体系中的核心地位。

  1. 用户请求:我们通过kubectl(或其他客户端)向API Server发起创建Pod的请求(例如,kubectl apply -f my-pod.yaml)。
  2. 持久化配置:API Server接收到请求后,会将Pod的配置信息(期望状态)写入高可用的键值存储系统etcd中。
  3. 调度器感知:Scheduler通过List-Watch机制(稍后会简单介绍)监听到API Server中有新的、尚未被调度的Pod。
  4. 智能决策:Scheduler根据内置的调度算法(包括我们后面会讲的nodeSelectornodeAffinity等策略),从集群中选择一个最合适的Node。一旦选定,Scheduler会更新Pod的定义,在.spec.nodeName字段中填入目标节点的名称。这个更新同样会写回etcd。
  5. Kubelet接管:目标Node上的Kubelet组件,同样通过List-Watch机制监听到有新的Pod被分配给自己。
  6. 容器创建与运行:Kubelet读取Pod的配置,调用底层的容器运行时(如Docker、containerd)来创建和运行Pod中的容器。
  7. 状态上报:Kubelet会持续监控Pod和容器的状态,并将实际状态汇报给API Server,API Server再将这些信息更新到etcd。
  8. 用户查看:当我们执行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。它的核心任务是:

  1. 发现新Pod:通过Watch机制发现未被调度的Pod。
  2. 筛选节点(Filtering):遍历集群中所有可用Node,过滤掉那些不满足Pod调度需求的Node。例如,Node资源不足、不满足Pod声明的nodeSelectornodeAffinity等。
  3. 节点打分(Scoring):对通过筛选阶段的Node进行打分,选出最优的Node。评分因素包括资源利用率均衡、满足preferredDuringSchedulingIgnoredDuringExecution类型的亲和性规则等。
  4. 绑定决策:将Pod“绑定”到得分最高的Node上(如果有多个得分相同的,则随机选择一个)。

如果集群中没有一个Node能满足Pod的需求,那么这个Pod会一直处于Pending状态,直到有合适的Node出现,或者Pod的调度需求被修改。

nodeSelector:简单直接的节点选择器

nodeSelector是最简单直接的节点选择方式。你只需要给Node打上特定的标签(Label),然后在Pod的配置中指定这些标签,Pod就会被调度到带有这些标签的Node上。

特点

  • 强制性:如果指定的标签在集群中任何一个Node上都找不到(或者标签写错了),Pod将无法被调度,会一直处于Pending状态。
  • 简单直接:配置简单,易于理解。
  • 逻辑与:如果nodeSelector中指定了多个标签,那么Node必须 同时满足所有 这些标签才能被选中。

实战案例:将Pod调度到有GPU的节点

  1. 给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
    
  2. 在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中用引号包围。

  3. 部署与验证

    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." 的信息。

  4. 删除标签
    如果想移除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提供了两种类型的亲和性规则:

  1. requiredDuringSchedulingIgnoredDuringExecution(硬策略)

    • 必须满足:Pod调度时,Node 必须 满足这里定义的所有规则,否则Pod不会被调度到该Node。行为上类似nodeSelector,但表达能力更强。
    • IgnoredDuringExecution:Pod成功调度到Node上运行后,如果该Node的标签发生了变化,不再满足此规则,Kubelet 不会 将Pod从该Node上驱逐。Pod会继续在该Node上运行。
  2. 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是数字字符串)。

注意

  • 如果同时指定了 nodeSelectornodeAffinity,那么Node 必须同时满足 两者的条件。
  • nodeAffinityNotInDoesNotExist操作符可以用来实现一定程度的节点反亲和性,即不希望Pod调度到某些特定类型的Node上。当然,K8s中更专门的节点反亲和机制是污点(Taints)和容忍(Tolerations),我们后续文章再详细探讨。

实战案例1:硬策略 + 软策略组合

假设我们希望Pod:

  1. 必须部署在属于 team-ateam-b 的节点上(硬性要求)。
  2. 最好部署在品牌为 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-ateam-b),首先被过滤掉。
    • test-b-k8s-node01test-b-k8s-node02 都满足硬策略。
    • 在满足硬策略的节点中,test-b-k8s-node01 同时满足软策略(hostbrand: ibm),而 test-b-k8s-node02 不满足。
    • 因此,Pod goweb-affinity-demo 会被调度到 test-b-k8s-node01

实战案例2:多重偏好与权重

假设我们希望Pod:

  1. 必须部署在属于 team-a 的节点上(硬性要求)。
  2. 优先选择磁盘类型为 sas 的节点 (权重50)。
  3. 如果找不到 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

生产环境使用建议

  1. 优先使用 nodeAffinitynodeAffinitynodeSelector 更具表达力,是推荐的主流方式。即使是简单的硬性需求,用nodeAffinityrequiredDuringSchedulingIgnoredDuringExecution也能实现,并且为未来扩展更复杂的规则留下空间。
  2. 标签规范化:为Node打标签时,建议使用统一的、有意义的前缀,例如公司/组织域名反写 com.mycompany.teamnode.kubernetes.io/arch(K8s内置标签)。
  3. 理解IgnoredDuringExecution:这意味着一旦Pod被调度,后续Node标签的变更不会影响已运行的Pod。如果需要Pod在Node标签变化后被重新调度(例如,Node不再符合要求时将其驱逐),nodeAffinity本身无法实现,需要结合其他工具如Descheduler,或更复杂的自定义控制器。
  4. 故障排查:当Pod长时间处于Pending状态时,使用 kubectl describe pod <pod-name> -n <namespace> 查看Pod的Events,通常能找到调度失败的原因,如 "didn't match node selector" 或 "didn't match node affinity rules"。
  5. 适度使用:虽然亲和性规则很强大,但过度复杂的规则可能导致调度困难或集群资源分配不均。保持规则的简洁和必要性。
  6. 小心标签值类型:再次强调,Node标签的key和value都是字符串。在YAML中定义亲和性规则时,values列表中的元素通常也应该是字符串(除非使用Gt, Lt操作符且你知道标签值可以被解析为数字)。

总结

nodeSelectornodeAffinity是K8s中控制Pod部署位置的强大工具。

  • nodeSelector简单直接,适用于固定的、强制性的节点选择。
  • nodeAffinity则提供了更丰富的表达能力,支持软/硬策略、复杂的逻辑操作,让我们能更精细化地管理Pod的调度行为。

掌握了它们,你就向着K8s调度大师的目标又迈进了一大步!在后续的文章中,我们还会探讨Pod亲和性/反亲和性,以及污点和容忍等更高级的调度特性,敬请期待!

希望这篇整理和补充对你和你的粉丝有所帮助!去实践一下吧,你会发现它们在实际生产中非常有用。


posted on 2025-05-07 19:06  Leo-Yide  阅读(328)  评论(0)    收藏  举报