☰
K8S调度实战:nodeSelector、亲和性、Taint、DaemonSet
2026/10/17 16:12:28 网站建设 项目流程

学K8S到了第六天,终于碰上了真正“有味道”的内容:nodeSelector、nodeAffinity、Taint、DaemonSet控制器。这四个词放一起,第一眼容易懵,但我今天学完之后的最大感受是——它们其实是同一条逻辑链上的四个环节,核心只解决一件事:Pod到底该跑在哪台节点上,以及由谁保证它“就该”跑在哪台节点上。

如果你也正在啃K8S,前五天大概已经知道Pod是怎么被创建、怎么设置资源限制、怎么用Service暴露。到了今天,就是从“Pod能跑”进入“Pod按规则跑”的阶段。这篇笔记我自己边敲命令边踩坑整理出来的,适合刚看完Pod基础、对调度只有模糊概念的初学者。我会把四个概念串起来讲,而不是逐个孤立地背定义,看完你会发现它们之间是互相配合的。

1. 先把四个概念的“骨架”搭起来

1.1 四个关键词到底在说什么

我先用一句话总结这四兄弟的分工,这句话我在笔记开头写了好几遍才让自己满意:

  • nodeSelector:Pod自己“挑”节点,只认标签,简单直接,但条件有限。
  • nodeAffinity:Pod“挑”节点的升级版,支持更多匹配逻辑,还能区分“必须满足”和“尽量满足”。
  • Taint(污点)+ Toleration(容忍):反过来了,节点主动“嫌弃”某些Pod,而Pod靠“容忍”来获得入场资格。
  • DaemonSet:不是调度语法,而是一个控制器,核心诉求是“符合规则的每台节点上,恰好跑一个Pod”。

如果你还是觉得抽象,可以想一个开分店的场景:你要在某个商圈租铺子。nodeSelector就相当于“我只租这条街的门面”;nodeAffinity进一步变成“我优先租核心位置,租不到就退而求其次”;Taint则是业主在挂牌上写了“不接受餐饮类租户”,除非你获得特殊豁免;DaemonSet等于总部规定“每个商圈都必须设一个服务点,一个不多一个不少”。

这样一轮下来,这四个概念其实是在不同方向上做同一件事:让Pod和节点之间的配对关系变得可控、可表达、可强制。

1.2 为什么非要把这三样东西分开学

很多人一开始会问:既然nodeSelector能指定节点,为什么还要搞一套nodeAffinity?既然能主动选节点,为什么还需要节点“嫌弃”Pod?这不是重复造轮子吗?

实际上它们解决的语义完全不同。nodeSelector本质是“等值过滤”,只要节点标签里有一个值匹配上了,就能调度上来,它没法表达“不要落在某类节点上”这种否定逻辑。nodeAffinity则补上了反选、多条件组合、软性偏好这些能力。

而Taint解决的是另一个方向的诉求:有些节点本身就是“特殊区域”,比如带GPU的节点、已经打了安全补丁的敏感节点、专用的数据库节点。在这些节点上,你不希望普通业务Pod一拥而上,这时候就需要节点自己“发声表态”。所以这里有个很重要思维转换:前两个是Pod选节点,Taint是节点筛Pod。理解了这个,再看DaemonSet的调度逻辑就顺了——DaemonSet创建出来的Pod,同样要过nodeSelector、nodeAffinity、Taint这几关,一个都不能少。

2. nodeSelector:最简单粗暴的节点选择

2.1 nodeSelector的底层逻辑

nodeSelector是K8S里最早出现、也是最小白的调度方式。它的原理一句话能讲完:Pod定义里写清楚要哪些标签,调度器只把Pod放到带这些标签的节点上。

实现方式也很直白,K8S每个节点都有一堆标签来描述属性,比如磁盘类型是SSD、节点在哪个可用区、是Linux还是Windows、GPU有没有安装。你可以用kubectl get nodes --show-labels直接看到这些。自定义标签也随意,比如usage=web、purpose=test。

nodeSelector匹配还有一个隐性规则容易被忽略:它只能做“等于”匹配。你写下disks: ssd,节点的标签就必须精确等于ssd,不存在模糊匹配、不等于、或者“在某一组值里”这些高级写法。多标签就是AND关系,比如:

spec: nodeSelector: region: cn-bj disks: ssd

意思是节点必须同时拥有region=cn-bj和disks=ssd这两个标签,缺一个都不行。

2.2 实操:给节点打标签并用nodeSelector调度

跟着敲一遍就记住了。先看当前节点和自带标签:

kubectl get nodes -A kubectl get nodes --show-labels

给某个节点打自定义标签:

kubectl label nodes node1 disks=ssd

这个命令我给初次接触的同学解释下格式:kubectl label nodes <节点名> <key>=<value>。标签一旦打上,不需要重启节点,实时生效。

然后写一个使用nodeSelector的Pod:

apiVersion: v1 kind: Pod metadata: name: test-selector spec: nodeSelector: disks: ssd containers: - name: nginx image: nginx:1.26 ports: - containerPort: 80

创建之后,kubectl get pods -o wide看一下所在节点:

kubectl apply -f test-selector.yaml kubectl get pods -o wide kubectl describe pod test-selector | grep -A5 Events

如果没有任何一个节点带disks=ssd标签,Pod会一直卡在Pending。这个问题初学者必踩,后面第6章我会专门写排查。

2.3 nodeSelector的局限性,为什么它只是“入门”

nodeSelector满足日常快速指定节点没问题,但真到大规模环境就有点捉襟见肘。

第一,它不支持集合操作。你想表达“调度到磁盘是SSD或者NVMe的节点上”,nodeSelector写不出这种逻辑,只会要求一个具体值。

第二,它不支持“prefer”语义。生产环境经常有这样的诉求:“优先调度到高性能节点,但如果没有也不要死磕,放到普通节点也能跑”。nodeSelector没有软性说法,匹配不上就是Pending,硬等。

第三,标签值多了之后维护成本很高。比如你要根据角色、可用区、资源类型组合,YAML里的nodeSelector会越来越长,而且看不出逻辑优先级。

所以从某个版本开始,K8S社区把nodeAffinity做成了nodeSelector的加强版。现在新项目我更推荐直接用nodeAffinity,nodeSelector更适合你只追求“快速固定一个点”的场景,比如本地开发环境、测试集群临时指定某个节点。

3. nodeAffinity:更灵活的节点亲和

3.1 硬性亲和与软性亲和的区别

nodeAffinity把调度逻辑分成两大类:“必须满足”和“尽量满足”。这两个英文后缀很长,但含义是重要的区分点:

  • requiredDuringSchedulingIgnoredDuringExecution:调度时必须满足的条件。
  • preferredDuringSchedulingIgnoredDuringExecution:调度时会尽量满足,但不强制。

这两个名字里还藏着一个重要细节:IgnoredDuringExecution,意思是只要Pod已经运行了,后续即使节点标签变了,已经在这台节点上的Pod也不会被立刻驱逐。它只管“入场的那一下”,不管“入场之后你是否一直符合”。

而硬性亲和,本质上等于加强版nodeSelector,只是一套更灵活的表达式语法。软性亲和则没有对应物,是真正新增的能力。软性亲和每个偏好还可以带权重,调度器会综合所有偏好打分,得分高的节点被选中。

3.2 operator操作符与权重设置

nodeAffinity的表达式核心是matchExpressions,配合不同的operator实现不同逻辑。我用一个YAML一次性说明:

apiVersion: v1 kind: Pod metadata: name: affinity-demo spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disks operator: In values: - ssd - nvme - key: region operator: NotIn values: - region-test preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: usage operator: In values: - high-performance

这里required下面要求节点在disks标签上等于ssd或nvme,同时region不等于region-test。注意In和NotIn的values是数组,可以写多个候选值,这是nodeSelector做不到的。

preferred下面是软偏好,weight: 80意味着如果节点带usage=high-performance标签,在调度器打分时会加80分。这里的weight取值范围是1到100。一个Pod可以写多个preference,每个preference都有自己的权重,调度器算总分再排名。

常用的operator还有:

  • Exists:只要节点有这个key就行,不关心value。
  • DoesNotExist:节点没有这个key。这个我经常用来把Pod避开带GPU标识的节点,效果类似NotIn。
  • Gt和Lt:对value做数值比较,要求value能转成数字,适合根据核心数、内存大小这类可量化标签做选择。

3.3 nodeSelector vs nodeAffinity 怎么选

选型上没有太多纠结,我的习惯是:

  • 只做简单等值匹配,不在乎灵活性,就写nodeSelector,YAML短。
  • 需要NotIn、Exists、多值In,或者需要“尽量满足”的偏好,直接nodeAffinity。
  • 两者可以同时出现在一个Pod里。如果同时写,nodeSelector的条件和required亲和的条件都要满足,相当于AND关系。
  • 如果只写软性亲和,Pod依然会被调度到没有匹配标签的节点上,因为只是“偏好”。这个点我曾以为它和硬性亲和一样会导致Pending,实际上不会,调度器只是尽量。

4. Taint + Toleration:节点端“反向排斥”

4.1 污点的三种effect

学到这里,思维要换方向了。前面都是Pod“主动挑”,现在节点“往外赶”。给节点打污点之后,默认情况下Pod根本不会调度上来,除非Pod显式写出对应的容忍度。

污点的格式是三段式:key=value:effect。value可以省略,但effect有讲究,三种取值分别表示不同强度的排斥:

  • NoSchedule:不调度新的Pod上来,但已经在节点上运行的Pod不会被驱逐。这是最常用的效果,适合“这节点你新任务别来,旧任务我不管”。
  • PreferNoSchedule:软版本,调度器尽量不把Pod放上来,但如果没其他选择也可以。这个适合不太严格的场景,实际生产用得少。
  • NoExecute:最狠,不仅不调度新Pod,已经在节点上运行的Pod也会被驱逐。配合tolerationSeconds使用,可以实现“给现有Pod一段缓冲时间”的效果。

4.2 给节点打污点与给Pod加容忍度

打污点用kubectl,注意节点名后面是=和:分隔的完整结构:

kubectl taint nodes node1 gpu=present:NoExecute

如果只是想让新业务别来,不想驱逐已经在跑的,用:

kubectl taint nodes node1 application=canary:NoSchedule

查看节点污点:

kubectl describe node node1 | grep -i taints

删除污点是在末尾加一个减号:

kubectl taint nodes node1 application=canary:NoSchedule-

注意减号是直接在结尾,不能有空格。

对应的Pod容忍度写法:

spec: tolerations: - key: application operator: Equal value: canary effect: NoSchedule

4.3 master节点为什么默认调度不了Pod

看到这里,如果你用kubeadm装过集群,会突然想起一件事:master节点默认看kubectl get nodes是Ready状态,但普通Pod从不会调度上去,是不是就是污点的功劳?

没错。kubeadm初始化的master节点默认自带一个污点node-role.kubernetes.io/master:NoSchedule。控制平面组件本身有对应容忍,所以它们没问题,普通业务Pod匹配不上容忍度,就被调度器绕开。

有时候你想让单机学习环境的master节点也能跑业务Pod,可以手动去掉这个污点。但我强烈建议只在学习环境里这么干,生产集群master最好保持原样,避免影响控制平面稳定性。

# 学习环境可选操作 kubectl taint nodes master node-role.kubernetes.io/master:NoSchedule-

还有一种情况:节点出现故障,K8S会自动给节点打上node.kubernetes.io/unreachable这类污点。如果你的Pod想在这种故障节点上强行运行,就得显式容忍它。这在网络分区、节点宕机的场景很关键,后面的NoExecute缓冲时长正好用在这里。

4.4 Toleration的匹配规则细节和NoExecute驱逐

容忍度务必要看匹配细节,我踩过坑才记住:

  • 如果使用operator: Equal,那么key、value、effect三者全匹配才算容忍。
  • 如果使用operator: Exists,value可以不写,只有key和effect匹配即可。若连effect都不写,仅key匹配就代表容忍所有effect的污点,这种写法最宽松,生产环境慎用。
  • 一个Pod可以挂多个tolerations,只要其中一个匹配上,调度就允许通过。
  • tolerationSeconds只对NoExecute有效,含义是节点被打上污点后,Pod允许继续运行多少秒再被驱逐。不写这个字段,默认永久容忍,也就是不驱逐。

举个例子,节点打上health=bad:NoExecute后,你想让已经跑在上面的Pod有个180秒的善后时间,就在Pod里加:

spec: tolerations: - key: health operator: Equal value: bad effect: NoExecute tolerationSeconds: 180

这个机制做节点优雅维护特别好用。比如要重启节点,可以先打一个NoExecute污点,给Pod 60秒缓冲,处理完再重启,Pod会在期间自动迁移到其他健康节点。

5. DaemonSet:每个节点一个Pod的“守夜人”

5.1 DaemonSet在集群里到底干什么

DaemonSet是我学习的第六天里最让人舒坦的一部分,因为前面绕来绕去的调度选择,到这里有了一个具体应用场景。DaemonSet的目标不是固定副本数,而是每个符合条件的节点上都跑一个Pod,动态跟随节点数量变化。

新增节点时,DaemonSet会自动在新节点上创建Pod;节点被删除时,这个节点上的DaemonSet Pod也会被回收。传统上Deployment是“我要几个副本”,DaemonSet是“只要节点符合要求,我就铺满”。

典型使用场景:

  • 网络插件,比如calico、flannel这类,必须在每个节点上有agent。
  • 日志采集,比如filebeat、fluentd,每个节点都要采集日志。
  • 监控agent,比如node-exporter、dcgm-exporter,每台机器数据都要采集。
  • 存储插件,比如ceph、glusterfs相关客户端组件。

命令查看:

kubectl get daemonsets -A kubectl get ds -A kubectl describe ds <名称>

它的状态字段会显示desiredNumberScheduled、currentNumberScheduled、numberReady等,一眼就能看出哪些节点还差Pod。

5.2 DaemonSet的调度与前面学习的联动

DaemonSet创建的Pod同样跑默认调度流程,所以它天然和nodeSelector、nodeAffinity、Taint这三个知识点联动。

比如,你想让DaemonSet日志组件只在带有log-agent=true标签的节点上运行:

apiVersion: apps/v1 kind: DaemonSet metadata: name: log-agent spec: selector: matchLabels: app: log-agent template: metadata: labels: app: log-agent spec: nodeSelector: log-agent: "true" tolerations: - operator: Exists containers: - name: log-agent image: busybox:1.36 command: ["/bin/sh", "-c", "sleep 3600"]

注意上面tolerations我写了operator: Exists,意思是容忍所有污点,这里只是为了演示灵活写法。放到生产环境别学这个写法,日志agent一般需要容忍NoExecute类的系统污点,但不该无脑容忍所有。

再看一个更贴合实际的组合:DaemonSet组件通常对master节点要有专门的容忍。原因很简单,master节点默认自带node-role.kubernetes.io/master:NoSchedule,不带容忍就铺不上去。所以很多网络插件的YAML里,你一定会看到类似这样的配置:

tolerations: - operator: Exists

5.3 更新策略:平滑升级DaemonSet

DaemonSet升级不是用kubectl set image就完事,更新策略需要先懂一下。K8S支持两种机制:

  • RollingUpdate:默认模式,可以设置maxUnavailable,表示最多允许几个节点上的Pod同时不可用。例如maxUnavailable: 1,升级时一次只挂一个节点的Pod,完成后继续下一个,不会让全集群agent同时断掉,适合日志、监控这些持续运行的守护型组件。
  • OnDelete:升级时不会自动更新已有的Pod,需要手动删掉某个Pod后才会重新创建新版本。适合你想精确控制每个节点的更新节奏。

更新命令:

kubectl set image ds/log-agent log-agent=busybox:1.36-new kubectl rollout status ds/log-agent

还可以指定minReadySeconds,新Pod必须稳定运行这个秒数后才算就绪,否则升级会中止。集群规模大的时候我会设一个比较大的值,避免agent反复重启仍然被当成健康。

6. 第六天踩坑实录:常见问题与排查技巧

6.1 Pod一直Pending?先看Events

前面提到的最常见问题,就是Pod创建后状态一直Pending。排查第一件事永远是describe,看Events字段:

kubectl describe pod test-selector | grep -A20 Events

Events里会明确告诉你为什么调度不了,比如上面nodeSelector的disks=ssd标签没匹配到任何节点,Events就会提示0/3 nodes are available: 3 node(s) didn't match node selector。

新手容易乱猜,其实Events几乎把答案怼到你脸上了。养成习惯:Pod起不来,先describe,别急着删了重建。

6.2 标签没匹配上,集群“假装”你不知道

还有一次我遇到奇怪现象:nodeSelector看起来明明匹配了,Pod却还是Pending。后来才发现,我给节点打的标签是disks=SSD,Pod里写的是disks: ssd,大小写没对上。

K8S的标签值是严格大小写敏感的,这个坑极其低级但我确实踩过。另外,如果你用kubectl label nodes node1 region=这种命令手动改标签,要小心覆盖。没有--overwrite时K8S会直接报错,你得习惯为可能重复的标签加参数,或者先确认节点现有标签再动手。

6.3 污点与容忍不匹配的细节陷阱

这里最阴的陷阱是effect不匹配。比如节点污点是NoExecute,你只在Pod里容忍了NoSchedule,那么这个Pod依然会被驱逐。反过来也一样,effect必须完全对上才算容忍。

还有Exists和Equal的区别。当你用operator: Exists且不带effect时,是容忍所有effect的,这有点反直觉,但理解了匹配规则就合理了。写容忍度的时候,最好按“key + operator + value + effect”四项逐一检查,少写一项可能放大容忍范围。

6.4 DaemonSet“想”跑在master上怎么办

如果你创建DaemonSet后发现desiredNumberScheduled比节点数少一个,大概率是因为master节点的污点和DaemonSet的容忍没有匹配上。网络插件为什么能全铺?因为它们YAML里已经预设了容忍。

自己写的DaemonSet遇到这个情况,就在template里加容忍配置,或者单独针对master的污点key写一条容忍。别一上来就用operator: Exists一把梭,尽量精确匹配master污点key。

6.5 排查技巧速查表

我一直觉得速查表比长篇大论有用,项目里也常用,放在这章最后给各位抄作业:

现象首选排查命令常见原因
Pod一直Pendingkubectl describe pod <名称>看EventsnodeSelector没匹配、preferred全落空、无可用节点
想确认节点标签kubectl get nodes --show-labels标签被覆盖或不存在
想确认节点污点kubectl describe node <节点名>找Taints字段污点没打上、或者删错
Pod被不断驱逐看Pod事件或kubectl get events --sort-by=.lastTimestamp节点有NoExecute污点且Pod无容忍
DaemonSet铺不满kubectl get ds <名称> -o yaml检查template的调度配置有nodeSelector但节点标签不匹配;有污点未容忍
升级卡住kubectl rollout status ds/<名称>+kubectl describe dsmaxUnavailable设置过小、Pod卡在Creating

再说一个经验技巧:排查调度问题时,kubectl describe node <节点名>这个命令看的不只是节点状态,还会列出该节点上所有Pod,方便你判断它是被污点挡了还是被资源占满了。我见过太多人只describe Pod不describe节点,绕了一大圈。

第六天学到这里,我最大的收获不是背住了几个字段,而是理解了K8S的调度其实是一场“双向选择”。Pod通过nodeSelector和nodeAffinity表达自己要什么,节点通过Taint表达自己排斥什么,DaemonSet则在这些规则之上做自动化。把这些组合好,你就能把集群里的Pod安排得明明白白。

最后分享一个实用小技巧:调试这些调度规则时,别用真的业务Pod,直接用busybox加sleep 3600这种轻量Pod测试,起来之后看节点落在哪,分析完删掉,干净利落。后面的课程应该要深入控制器和资源配额了,到时候这套“双向选择”的底子会派上大用场。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询