学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: NoSchedule4.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: Exists5.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 EventsEvents里会明确告诉你为什么调度不了,比如上面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一直Pending | kubectl describe pod <名称>看Events | nodeSelector没匹配、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 ds | maxUnavailable设置过小、Pod卡在Creating |
再说一个经验技巧:排查调度问题时,kubectl describe node <节点名>这个命令看的不只是节点状态,还会列出该节点上所有Pod,方便你判断它是被污点挡了还是被资源占满了。我见过太多人只describe Pod不describe节点,绕了一大圈。
第六天学到这里,我最大的收获不是背住了几个字段,而是理解了K8S的调度其实是一场“双向选择”。Pod通过nodeSelector和nodeAffinity表达自己要什么,节点通过Taint表达自己排斥什么,DaemonSet则在这些规则之上做自动化。把这些组合好,你就能把集群里的Pod安排得明明白白。
最后分享一个实用小技巧:调试这些调度规则时,别用真的业务Pod,直接用busybox加sleep 3600这种轻量Pod测试,起来之后看节点落在哪,分析完删掉,干净利落。后面的课程应该要深入控制器和资源配额了,到时候这套“双向选择”的底子会派上大用场。