K8S调度核心:nodeSelector、亲和性与污点容忍实战
1. 开篇从一次“Pod跑错节点”说起学K8S到这个阶段很多同学已经能把Deployment、Service玩得很溜了Pod能起、能通、能滚动更新集群看起来“一切正常”。但等你负责的应用真正上了多节点集群第一个让你头疼的问题大概率不是Pod起不来而是——Pod被调度到了你不想让它去的那台机器上。我之前就踩过这个坑。一个内存型缓存服务莫名其妙被调度到了集群里一台只有2G内存的旧节点上结果节点直接OOM连带拖垮了同节点上的其他应用。当时的第一反应是“调度器是不是傻了”后来才意识到是我自己没告诉调度器“这个Pod应该去哪”。K8S默认的调度策略只保证“有地方放”不保证“放得最合适”。而今天要聊的这套东西——nodeSelector、nodeAffinity、Taint/Toleration、DaemonSet就是解决“Pod该去哪”和“哪些节点不收留普通Pod”这两件事的核心工具。这篇文章是K8S入门学习笔记的第六天我会把这几个概念串起来讲不只是逐个报名词而是说清楚它们各自解决什么问题、怎么配合使用、实操中有哪些坑。适合刚学完Deployment和Service、准备深入理解调度逻辑的同学。2. 先把调度这件事捋清楚三问定位法2.1 为什么默认调度不够用默认调度器kube-scheduler的职责是给一个新创建的Pod找到一个合适的Node。它内部有一套复杂的打分机制会综合CPU、内存、磁盘、网络、Pod分布等因素。但问题是它只看“资源够不够”根本不看“业务上该不该放这”。举个例子你集群里有GPU节点和非GPU节点跑模型训练的Pod如果被调度到非GPU节点任务直接崩。你有一些节点接了SSD日志采集Pod如果落到普通机械盘节点上性能就差一大截。更麻烦的是有些节点是给内部监控用的业务流量根本不该进去。这时候就需要人为干预调度告诉调度器“哪些约束条件必须满足哪些只是倾向性建议”。K8S提供了好几套方案我习惯用一个“三问定位法”来快速选型问题一这个约束是“一刀切”的简单规则吗比如“只放GPU节点”——用nodeSelector。问题二约束条件比较复杂有硬性要求也有偏好倾向比如“最好放SSD节点但没有SSD也能跑”——用nodeAffinity。问题三某些节点需要“清场”普通Pod别来只有特殊Pod能进比如“这台节点专门给日志组件用”——用Taint Toleration。这三问基本覆盖了绝大多数调度需求。至于DaemonSet它其实不是一种调度策略而是一个控制器它的任务是“保证每个符合条件的节点上都运行同一个Pod副本”天然会和前面三样东西配合使用。2.2 两套核心机制标签体系和调度约束在深入每个工具之前必须先把“标签Label”这个概念焊死在心里。nodeSelector和nodeAffinity本质上都是“根据节点的标签来选择节点”只是语法和表达能力不同。节点标签是挂在Node对象上的一组键值对格式是keyvalue。它非常灵活你可以随便定义比如disktypessd标记SSD节点gputrue标记GPU节点regionbeijing标记地域调度器在调度Pod的时候会读取Pod里声明的选择条件再去匹配所有节点的标签命中的才进入候选名单。理解了这一点后面所有内容都会变得很顺畅。3. nodeSelector简单粗暴的第一课3.1 最小配置示例nodeSelector是K8S里最古老的调度约束方式语法极其简单。看个例子就懂apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: nginx image: nginx nodeSelector: disktype: ssd这段配置的意思是这个Pod只会被调度到带有disktypessd标签的节点上。如果集群里没有任何节点带这个标签Pod就会一直处于Pending状态永远调度不上去。给节点打标签的命令是kubectl label node 节点名 disktypessd查看节点标签kubectl get nodes --show-labels或者只看某个节点的详细标签kubectl describe node 节点名3.2 生产环境里的常规用法我在实际项目里最常用nodeSelector的场景有两类第一类是区分硬件规格。集群里有配置差异很大的机器把高配机器打上cpu-typehigh低配的保持默认然后把核心数据库应用指定到高配节点上。第二类是合规隔离。某些业务要求数据必须落在特定物理区域的机器上这部分机器打上regioninternal标签相关Pod用nodeSelector固定过去。nodeSelector的优点是清晰、简单、好排查yaml里一眼就能看明白。但它的缺点也很明显只有“等值匹配”这一种能力不支持“不在某组节点”“优先选择某类节点”这类稍微复杂一点的条件。想表达更丰富的策略就得用nodeAffinity。3.3 踩坑提示用nodeSelector最容易踩的坑是标签拼写错误。标签打上去之后一定要用kubectl get nodes --show-labels确认一下。我就遇见过一种情况标签手动打在了node上但后来节点被回收重建标签丢了结果所有挂了nodeSelector的Pod全部Pending。节点池自动伸缩、节点重建这种场景下标签很容易被“洗掉”所以生产环境最好配合自动化运维工具在节点加入集群时就统一打标。4. nodeAffinity从“只能选”到“更想要”4.1 两类亲和性与表达式规则nodeAffinity比nodeSelector强大得多它支持复杂的逻辑表达式还分两个级别requiredDuringSchedulingIgnoredDuringExecution硬性要求不满足就不调度可以理解为“必须”。preferredDuringSchedulingIgnoredDuringExecution软性偏好不满足也调度但调度器会优先选满足的节点可以理解为“尽量”。名字很长我记不住的时候就拆开看前半段是调度阶段的行为后半段是执行阶段的行为其中IgnoredDuringExecution的意思是这个规则只在Pod调度时生效Node后续标签变了也不会把已经运行的Pod驱逐出去。4.2 运算符详解nodeAffinity支持多种运算符这才是它真正的威力所在运算符含义示例Inkey的值在给定列表中region in (east, west)NotInkey的值不在给定列表中region notin (south)Exists节点存在该key不看值gpu ExistsDoesNotExist节点不存在该keygpu DoesNotExistGtkey的值大于某个数值仅用于整数值memory-cache-size Gt 8Ltkey的值小于某个数值memory-cache-size Lt 164.3 完整配置示例下面是一个硬性 软性混合的配置apiVersion: apps/v1 kind: Deployment metadata: name: affinity-demo spec: replicas: 3 selector: matchLabels: app: affinity-demo template: metadata: labels: app: affinity-demo spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: region operator: In values: - beijing containers: - name: nginx image: nginx拆开解读这个Deployment要求必须调度到disktypessd节点上在满足硬性条件的节点里调度器会优先选regionbeijing的节点权重80代表这个偏好在打分时占的比重。权重范围是1到100可以同时写多个偏好项不同偏好通过权重来决定优先级。4.4 实战选型建议从我自己的使用经验来看nodeAffinity适合表达“有优先级梯度”的策略。比如业务请求优先走高性能节点扛不住时再溢出到普通节点设置不同权重。要求Pod分布在不同机柜上用topology-key类似的思路加上NotIn表达式排除某几个节点。GPU任务必须在GPU节点上用硬性规则锁死。有一个常见的误区很多人觉得nodeSelector会被nodeAffinity完全替代。其实不是nodeSelector胜在简单可读小规模集群完全够用nodeAffinity表达能力强但yaml写起来冗长交付给团队维护时阅读理解成本更高。如果你管理的是一个几十台规模、节点类型不复杂的集群我建议优先从nodeSelector起步真的发现表达不够了再升级到nodeAffinity。5. Taint与Toleration给节点设门槛让Pod办通行证5.1 为什么需要TaintnodeSelector和nodeAffinity解决的是“Pod主动选节点”的问题。那反过来呢如果某个节点就是不想让大多数Pod上去该怎么办举个真实场景集群里有一台用来跑日志采集的节点上面IO压力比较大我不希望业务Pod也挤上去。或者说有台机器硬件异常I/O极慢想暂时别再往上面调度新Pod。这时候光靠Pod那边写Affinity是管不过来的——因为所有新Pod都不会想着主动避开这台机器。Taint污点就是打在节点上的“门禁规则”它声明“我有问题/我特殊普通Pod别来。”而Toleration容忍是打在Pod上的“通行证”Pod声明“我知道你有问题但我能接受让我进去。”5.2 Taint的三个Effect给节点打污点的命令格式如下kubectl taint node 节点名 keyvalue:Effect关键的其实不是key和value而是Effect。有三种Effect调度行为运行行为NoSchedule不能调度新Pod到该节点已有Pod不受影响已运行的Pod继续运行PreferNoSchedule尽量避免但不是硬性禁止已运行的Pod继续运行NoExecute不能调度新Pod同时驱逐所有没有对应容忍的已有Pod不满足容忍的Pod会被杀掉NoExecute是杀伤力最强的它会直接影响已经运行在节点上的Pod。比如集群要维护某台节点你可以先给它打上maintenancetrue:NoExecute所有没有容忍的Pod自动被驱逐到其他节点等维护完再把污点去掉。5.3 内置污点不可不知的自动行为K8S的节点故障控制管理器node-lifecycle-controller会自动给节点打污点这是初学阶段非常容易忽略的一块。常见的几个内置污点node.kubernetes.io/not-ready节点未就绪网络不通或kubelet异常node.kubernetes.io/unreachable节点控制器无法访问节点node.kubernetes.io/disk-pressure磁盘压力大node.kubernetes.io/memory-pressure内存压力大node.kubernetes.io/out-of-disk磁盘空间不足node.kubernetes.io/network-unavailable网络不可用这些污点的Effect大部分都是NoSchedule或NoExecute。默认情况下很多系统级Pod已经内置了对应容忍所以你能看到kube-system命名空间里的Pod在节点出问题时仍然能正常工作或者被安全迁移。5.4 写一个TolerationPod上的容忍配置写法apiVersion: v1 kind: Pod metadata: name: taint-demo spec: tolerations: - key: special operator: Equal value: true effect: NoSchedule containers: - name: nginx image: nginx这段的意思是我只容忍specialtrue:NoSchedule这一个污点。还有一种更宽松的写法容忍所有污点tolerations: - operator: Exists当然生产环境不建议这么用等于放弃了节点隔离机制。如果维护节点时想让Pod自动迁移可以给节点打kubectl taint node 节点名 node-role.kubernetes.io/control-plane:NoSchedule等等这个是控制节点自带的污点生产环境要小心处理。我的建议是日常维护场景下用maintenancetrue:NoExecute这种自定义污点比直接操作内置污点要安全得多。5.5 三套机制如何联动这里有个很关键的理解点nodeSelector/nodeAffinity 和 Taint/Toleration 是从两个方向收敛调度决策的。节点打了污点等于“拒绝大多数”Pod写了容忍等于“获得准入”。节点有标签等于“提供选项”Pod写Affinity等于“选择目标”。实际生产环境的调度策略往往是两套并用比如DaemonSet控制器要部署到所有节点上但控制平面节点自带NoSchedule污点所以DaemonSet的Pod模板里必须带上对node-role.kubernetes.io/control-plane的容忍否则Pod永远上不了控制节点。6. DaemonSet集群级常驻部署利器6.1 它和Deployment的本质区别Deployment追求的是“我要跑几个副本”副本分散在哪台机器上你通常不关心。DaemonSet不一样它的目标是“每个节点一个副本”新增节点自动补Pod删掉节点自动回收Pod。理解DaemonSet最好的方式是看它的名场景日志采集每个节点上都要跑一个采集Pod如fluentd、filebeat把节点日志统一收集走。监控探针每个节点上都要跑一个指标采集器比如node-exporter采集节点CPU、内存、磁盘指标。网络插件CNI插件的Pod通常每个节点都要有Agent。存储客户端每个节点都要挂载某种存储客户端组件。6.2 一个完整的DaemonSet定义apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter namespace: monitoring spec: selector: matchLabels: app: node-exporter template: metadata: labels: app: node-exporter spec: tolerations: - operator: Exists nodeSelector: kubernetes.io/os: linux containers: - name: node-exporter image: prom/node-exporter ports: - containerPort: 9100 hostPort: 9100这个配置里就体现了前面讲过的多个概念组合toleration让Pod能上任何节点nodeSelector限定只在Linux节点运行。DaemonSet的调度规则和普通Pod完全一样你可以给它加nodeAffinity、指定容忍条件甚至设置资源水位。6.3 DaemonSet的滚动更新和回滚DaemonSet管理下的Pod由于每个节点都要更新更新策略值得单独说下。DaemonSet支持两种更新策略OnDelete只有手动删除某个节点上的Pod后DaemonSet才重建该Pod适合谨慎场景。RollingUpdate默认策略逐个节点更新支持maxUnavailable参数控制最多同时不可用的Pod数。滚动更新时需要重点关注maxUnavailable。比如集群有10个节点你设置maxUnavailable: 2那么更新过程中最多只能有2个节点的Pod处于不可用状态避免大面积采集中断。实际操作上我会先更新小规模环境确认新版本没问题再改生产环境DaemonSet镜像这样一旦有问题能快速回滚。6.4 最常见的DaemonSet调度问题DaemonSet虽然好用但有几个高频坑点DaemonSet的Pod会被调度到控制平面节点吗 在较新版本的K8S中控制平面节点默认有污点而标准DaemonSet不自动容忍该污点所以默认不会上去。如果你确实需要在控制节点上跑DaemonSet需要在模板中显式加容忍或给节点去掉污点。我建议保留污点只用Toleration控制。节点标签变了怎么办 DaemonSet创建的Pod遵循Pod模板中的nodeSelector/affinity。如果节点标签变了新Pod按新规则来但已运行的Pod不会自动销毁重建需要你自己处理。DaemonSet的Pod总是排在所有节点上吗 不一定。如果加了nodeSelector或nodeAffinity只在匹配的节点上创建如果没有这些约束理论上所有节点都会创建一个。7. 常见问题与排查技巧实录7.1 问题速查表现象可能原因排查命令Pod一直Pending没有节点满足nodeSelector/Affinitykubectl describe pod pod名看EventsPod一直Pending节点上有NoSchedule污点且Pod无对应Tolerationkubectl describe node 节点名看Taints节点上有NoExecute污点上面的Pod被莫名其妙杀掉kubectl get events --sort-by.metadata.creationTimestamp看驱逐记录DaemonSet没有在所有节点创建PodnodeSelector标签没打全或容忍缺失kubectl get ds -A 检查节点labels和taints更新DaemonSet镜像后部分节点没更新rollingUpdate的maxUnavailable设置不合理或节点隔离kubectl rollout status ds/名称 -n 命名空间7.2 排查Pod为什么没被调度一旦遇到Pod卡在Pending我最常用的三板斧第一步kubectl describe pod pod名重点看最底部Events区域那里会直接写调度器拒绝的原因比如0/5 nodes are available: 1 node(s) didnt match node selector, 2 node(s) had taint...。这行信息基本上把问题直接告诉你了。第二步kubectl get nodes --show-labels核对目标节点是否确实有Pod声明要用的标签。这里特别提醒Label的key和value完全区分大小写DiskType和disktype是两个完全不同的标签。第三步看污点。kubectl describe node 节点名在Taints字段会列出所有污点对照检查Pod的tolerations是否匹配。一个常见的匹配误区是Pod容忍里写了key和effect但忘了value结果key能对上value对不上还是不匹配。7.3 我的独家排查小技巧我会写一个快速检查shell脚本把它存成check-schedule.sh新集群搭好第一件事就是跑一遍#!/bin/bash kubectl get nodes -o custom-columnsNAME:.metadata.name,LABELS:.metadata.labels,TAINTS:.spec.taints这个命令能一眼看到每台节点的标签和污点情况比kubectl describe的输出清爽太多了。遇到调度问题先跑这个十有八九问题在哪就清楚了。另外一个建议当你给Deployment配置了调度约束最好同时用kubectl get pods -o wide确认Pod实际分布别只看Running就觉得一切正常。因为 “Running” 只能说明它起来了不能说明它在正确的位置上。这一眼能省去很多后续排查时间。8. 最后的个人体会学调度这一块我觉得最大的体会不是记住那些yaml字段而是建立一种思维模式在K8S里Pod在哪里运行从来不是随机的而是被显式和隐式规则共同决定的。显式规则是你写的nodeSelector、Affinity、Toleration隐式规则是节点污点、网络插件行为、控制平面限制。调试一个调度问题时把这两类规则都过一遍通常比死盯着一个配置文件有效得多。这几天的笔记写下来我自己也在不断地把零散知识往一起串。nodeSelector最朴素nodeAffinity更强大Taint/Toleration是反向拦路DaemonSet是这一切规则在“每节点常驻”场景下的集大成者。回头想想调度这块并不复杂难的是在真实环境里快速定位“规则没生效”的原因。所以我特别建议你在自己的测试集群里把今天说的每一种组合都亲手实验一遍打错标签、写错权重、少加容忍这些坑都踩一遍之后上生产才能真正心里有数。