
1. 污点和容忍到底解决了什么问题1.1 从节点调度说起为什么要设计这套机制在Kubernetes集群里Pod要跑起来第一步就是被调度器放到某个节点上。默认情况下调度器会综合节点的资源余量、亲和性规则、资源请求等条件做选择。但有一个很尴尬的场景比如集群里有一台机器是GPU服务器整机只有这一台有GPU资源跑的是训练任务另外还跑着一些普通的Web服务。如果不加任何限制调度器很可能把普通Pod也调度到GPU机器上虽然不影响正确性但会抢占CPU、内存挤压真正需要GPU的Pod的生存空间更麻烦的是一旦普通Pod占用了过多内存GPU任务反而可能因为资源不足被驱逐。反过来讲还有一种场景更常见运维要对某台节点做内核升级或者硬件维修需要把节点上的Pod全部赶走并且在一段时间内禁止新的Pod调度上来。虽然可以一个个删除Pod但你没法阻止调度器又把新Pod送上来。用kubectl cordon可以把节点标记为不可调度但它只影响未来调度的Pod对已经运行在上面的Pod无能为力。这就是污点Taint和容忍Toleration机制出现的意义。它的核心思路特别朴素给节点“抹上”一个标记默认情况下所有Pod都不允许调度到这个节点上只有那些明确声明“我受得了这个标记”的Pod才允许上来。就像房东在门口贴了一张“不准养宠物”的告示所有租客默认都不能带宠物入住但如果有租客在合同里签了“我接受养宠物条款”就可以住进来。1.2 污点的三种效果NoSchedule、PreferNoSchedule、NoExecute污点本身由三个部分组成key、value、effect。其中effect是核心它决定了污点对Pod产生什么影响一共就三种NoSchedule带有这种污点的节点调度器不会把新的、没有对应容忍的Pod调度上来。注意关键词是“新的”已经在节点上运行的Pod不受影响。PreferNoSchedule这是NoSchedule的“软版本”。调度器会尽量避免把Pod调度上来但如果集群里实在没有其他节点可用调度器还是会把这个Pod调度上去。说白了就是“尽量别来但实在没地方去也只能来”。NoExecute这是最狠的一种。它不仅阻止新Pod调度上来还会立刻驱逐节点上所有没有对应容忍的存量Pod。这个效果和cordon完全不同——cordon只管新来的NoExecute连老住户一起清退。这三种效果就是一套从“软提醒”到“硬隔离”的完整梯度。实际使用中NoSchedule最常用于划分专用节点NoExecute最常用于节点维护和故障隔离。1.3 为什么有nodeSelector和nodeAffinity还不够有人会问Kubernetes里已经有nodeSelector和节点亲和性nodeAffinity了为什么还需要污点和容忍这两套机制其实是互为补充的两个方向。亲和性是Pod主动选择节点是“我想到哪里去”而污点和容忍是节点主动排斥Pod是“我不欢迎哪些Pod来”。举一个典型场景一组GPU节点需要部署一个监控采集器DaemonSet希望它能跑在所有节点上包括GPU节点。如果用nodeAffinity你得小心处理各种匹配关系。如果用污点加容忍事情就简单了给GPU节点打上污点让普通业务Pod默认不上去监控采集器本身声明容忍这个污点就能顺利调度到所有节点上。这就是“阳性选择”和“阴性排除”的组合拳两者配合使用才能实现精细的调度策略。2. 核心配置语法与原理解析2.1 污点的定义格式与命令操作污点的格式非常简单长这样keyvalue:effectkey和value就是普通的键值对effect就是上面说的三种效果之一。比如给节点node-gpu-01打一个污点表示“这台节点是GPU专用机器没有容忍的Pod别来”kubectl taint nodes node-gpu-01 gputrue:NoSchedule常用命令就三条查看污点kubectl describe node node-gpu-01在输出里找Taints字段。删除污点kubectl taint nodes node-gpu-01 gputrue:NoSchedule-。注意末尾的减号这是在告诉Kubernetes“把这个污点移除”减号不能丢。更新污点重复执行kubectl taint nodes node-gpu-01 gputrue:NoExecute会直接把同key同effect的污点覆盖更新。这里要特别提醒一个细节value其实是可以省略的。你可以写kubectl taint nodes node-gpu-01 gpu:NoSchedule这样污点的value就是空的。对应的容忍配置会略有不同后面我会讲。还有一点污点是可以叠加的一个节点可以同时挂多个污点。调度器在判断时要求Pod必须能容忍节点上的所有污点只要有一个不能容忍Pod就不会被调度上来。这个“全容忍”机制容易踩坑我在后面的排查部分会再展开。2.2 容忍的YAML配置与匹配规则容忍是写在Pod的spec里的典型配置长这样apiVersion: v1 kind: Pod metadata: name: nginx-gpu spec: containers: - name: nginx image: nginx tolerations: - key: gpu operator: Equal value: true effect: NoSchedule这个配置的意思是这个Pod声明可以容忍gputrue:NoSchedule这个污点它会被允许调度到打了这个污点的节点上。如果污点value是空的或者你想匹配某个key下的所有情况可以用Exists操作符tolerations: - key: gpu operator: Exists effect: NoScheduleExists的意思是只要存在key为gpu的污点并且效果是NoSchedule不管value是什么都能容忍。如果连effect都不写只留key和operator那这个容忍能匹配所有包含这个key的污点不管效果是哪种。还有一种极端写法用不带key、不带effect的容忍来匹配所有污点通常不推荐这样做副作用太大。2.3 tolerationSecondsNoExecute场景下的“缓刑期”如果effect是NoExecute容忍还可以加上tolerationSeconds字段表示Pod在被驱逐之前可以继续留在节点上的时间。这个字段特别有用尤其是在节点故障通告的场景里。举个例子节点宕机后Kubernetes会在一段时间后给节点打上node.kubernetes.io/unreachable污点并开始驱逐Pod。但如果你给Pod配置了这样一个容忍tolerations: - key: node.kubernetes.io/unreachable operator: Exists effect: NoExecute tolerationSeconds: 60Pod就有60秒的缓冲时间。在这60秒内Pod还会继续运行但超过60秒就会被驱逐。这对于无状态服务来说可能没什么感觉但对于有状态服务比如数据库、消息队列的Pod这60秒可能是优雅停机、做数据刷盘的关键窗口。我见过一些团队在业务Pod上配置tolerationSeconds: 30利用这个窗口做优雅退出实测效果比裸奔要好很多——至少日志里不再出现“Pod被突然杀死”的错误了。这里有一个很关键的技术背景实际上Kuberneteskubelet会给每个Pod自动注入一组默认容忍包括对node.kubernetes.io/not-ready的容忍默认300秒和node.kubernetes.io/unreachable的容忍默认300秒。这也是为什么节点发生故障时Pod不会立刻被驱逐而是会等5分钟。如果你在YAML里自己写了这些容忍就会覆盖默认值所以tolerationSeconds一旦写小了代价就是Pod出问题时被更快驱逐。3. 实操演示从零配置污点与容忍3.1 环境准备与节点信息确认做实验之前先确认集群环境。我用的是一个三节点集群一个master节点两个worker节点。在实际操作中通常会给worker节点分配不同的业务角色。这次我准备把node-worker-01模拟成一台“GPU专用节点”。先查看节点状态kubectl get nodes -o wide输出结果类似NAME STATUS ROLES AGE VERSION node-master Ready control-plane 23d v1.28.2 node-worker-01 Ready none 23d v1.28.2 node-worker-02 Ready none 23d v1.28.2再确认一下节点当前的污点情况kubectl describe node node-worker-01 | grep -A 3 Taints如果输出是空的说明当前节点没有任何污点。3.2 给节点打污点验证调度拒绝给node-worker-01打上GPU专用污点kubectl taint nodes node-worker-01 gputrue:NoSchedule执行后终端会提示node/node-worker-01 tainted。接着创建一个普通的Pod不配置任何容忍kubectl run nginx-without-toleration --imagenginx --restartNever过几秒查看Pod状态kubectl get pods -o wide | grep nginx-without-toleration你会发现Pod一直停在Pending状态。用kubectl describe pod看事件最后几行会出现这样一条关键信息Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 12s default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {gpu: true}, 2 node(s) didnt match Pods node affinity/selector.这就是污点机制在起作用调度器发现node-worker-01上有gputrue:NoSchedule污点而当前Pod没有对应的容忍直接跳过。另外两个节点因为没有匹配的亲和性规则也跳过。最终没有任何节点可用Pod只能Pending。这个实验非常直观地说明了污点的“排斥”效果。3.3 添加容忍Pod成功调度现在创建一个带容忍的Pod配置文件gpu-nginx.yamlapiVersion: v1 kind: Pod metadata: name: nginx-with-toleration spec: containers: - name: nginx image: nginx tolerations: - key: gpu operator: Equal value: true effect: NoSchedule创建并验证kubectl apply -f gpu-nginx.yaml kubectl get pods -o wide | grep nginx-with-toleration这次Pod进入Running状态且NODE列显示的是node-worker-01。说明容忍配置生效Pod被成功调度到了带有污点的GPU节点上。这里补充一个经验点如果想让某个Pod“优先”调度到GPU节点但又不想完全禁止其他节点接收业务Pod可以同时用nodeAffinity和污点容忍配合。nodeAffinity保证“尽力往GPU节点靠”容忍保证“GPU节点接受你”。实际操作中我经常这样组合使用。3.4 NoExecute驱逐实验看存量Pod如何被清退接下来验证NoExecute的驱逐效果。先创建一个Pod不配置任何容忍让它正常调度到node-worker-01上kubectl run nginx-noexecute-demo --imagenginx --restartNever kubectl wait --forconditionready pod nginx-noexecute-demo确认Pod处于Running状态后给节点打上NoExecute污点kubectl taint nodes node-worker-01 gpu-dedicatedtrue:NoExecute观察Pod状态kubectl get pods -o wide | grep nginx-noexecute-demo几秒之内这个Pod就会变为Terminating然后被删除。这就是NoExecute的威力——它不仅阻止新Pod进来还会把已经在节点上运行的、没有容忍的Pod全部驱逐。这个特性在节点维护场景下特别有用。比如要对节点做内核升级先打上NoExecute污点把业务Pod全部安全驱逐到其他节点等节点维护完成、污点移除后Pod才会由Deployment控制器拉到新Pod重新被调度回来。整个过程不需要手动去删除一个个Pod干净利落。3.5 清理污点与收尾实验做完把污点清理掉kubectl taint nodes node-worker-01 gputrue:NoSchedule- kubectl taint nodes node-worker-01 gpu-dedicatedtrue:NoExecute-注意两个污点要分开删每个命令都会返回untainted。建议用kubectl describe node node-worker-01再确认一下Taints字段为空避免残留污点导致后续实验出现莫名其妙的问题。4. 常见问题与排查技巧实录4.1 加了容忍还是Pending可能是value不匹配这是我在实际排障中遇到最多的问题。有人配置了容忍但Pod依然调度不过去。最常见的原因是value对不上。比如节点污点是gputrue:NoSchedule容忍写的却是tolerations: - key: gpu operator: Equal value: false effect: NoSchedule这种情况是匹配不上的。Kubernetes的容忍匹配要求key、value、effect三者都一致operator为Equal时。value写错了等于没写容忍。还有一种情况节点上有多个污点Pod只容忍了其中一个。比如节点同时有gputrue:NoSchedule和diskssd:NoSchedule两个污点Pod只容忍了gpu那依然调度失败。排查的时候用kubectl describe node看清楚节点上的全部Taints逐个核对容忍列表问题一般很快就能定位。4.2 NoExecute会立即驱逐存量Pod不要和cordon混用很多人第一次用NoExecute时会“翻车”原本以为只是让节点不再接收新Pod结果存量Pod被删光了。这里再强调一遍NoExecute会驱逐节点上所有没有容忍的存量Pod。如果只想让节点停止接收新Pod用kubectl cordon更合适或者用NoSchedule存量Pod不受影响。另外还有一种容易搞混的情况节点的kubelet会在节点变为NotReady时自动打上node.kubernetes.io/not-ready污点同时NoExecute效果会触发存量Pod驱逐。如果不希望故障节点上的Pod被驱逐可以给Pod配置对这个污点的容忍。但需要注意这会导致Pod长时间停留在故障节点上如果节点永久宕机这些Pod就永远恢复不了了。生产环境建议结合tolerationSeconds来设置缓冲窗口给控制器留出重新调度的时间。4.3 云厂商节点池与内置污点的联动效应使用云厂商的托管Kubernetes服务时节点池本身可能自带污点。比如某些云厂商的GPU节点池创建时就带上了nvidia.com/gputrue:NoSchedule之类的污点。这时候如果你自己创建的Pod没有容忍即使集群有GPU节点Pod也调度不上去。在大型集群里做节点自治管理时我习惯用一个统一的脚本去检查节点污点防止有人误操作把生产节点的污点改掉。脚本里面通常会列出节点、污点数量、污点key/value定时巡检一旦发现异常就告警。平时多花这点心思能省下不少半夜被电话吵醒的时间。4.4 与DaemonSet的配合坑管控组件别被污点拦住DaemonSet默认情况下是不会忽略污点的。如果在某个节点上打了NoSchedule污点而DaemonSet的Pod没有对应的容忍那这个Pod就不会在该节点上创建。这会导致一些基础组件缺失比如日志采集器、监控Agent、网络插件在某些节点上就是起不来。Kubernetes在设计上也意识到了这个问题所以给一些系统组件默认加了容忍。在kube-system命名空间里你可以看到很多DaemonSet比如网络插件、kube-proxy自动带了一堆容忍能接受各种内置污点。但如果是你自己部署的DaemonSet比如自研的日志采集器就得手动加上容忍。常见的做法是给自研DaemonSet配置一个比较宽泛的容忍允许它在任何节点上运行tolerations: - operator: Exists这样DaemonSet就能在所有节点上创建Pod不管节点上有什么污点。但这样做有风险如果节点上有特殊污点比如GPU专用采集器依然会被调度上去资源竞争问题会存在。所以更精细的做法是按需配置比如只对node.kubernetes.io/not-ready和node.kubernetes.io/unreachable做容忍其他污点不碰。4.5 从一次GPU集群事故谈调度策略的完整设计最后讲一个真实的案例。有一次我维护一个GPU集群几个节点上装了A100显卡用来跑深度学习训练任务。最初只打了gputrue:NoSchedule污点想着普通Pod不会上去只有训练任务通过容忍才能调度。结果上线后问题来了有些核心业务Pod因为副本数不够被调度器“迫不得已”调度到了GPU节点上——因为它们没有容忍理论上不会但如果你还有别的亲和性配置或者集群其他节点资源已满调度器会尝试找一切可能的节点。排查后发现这些业务Pod没有容忍但Pod模板里被人加了一个nodeAffinity把GPU节点也纳入到了可调度范围两个配置冲突了。那次事故让我体会到调度策略是一个组合拳单靠一个机制防不住所有问题。污点和容忍只是其中的一块拼图必须和节点亲和性、Pod反亲和性、资源请求等配合使用。在设计调度策略时建议先问自己几个问题哪些节点是隔离的哪些Pod需要调度到隔离节点哪些Pod必须在所有节点上运行想清楚之后再配置才能形成一个闭环。很多人看文档觉得污点容忍很简单真正把它放进集群架构里的时候才发现牵一发动全身。5. 从污点看Kubernetes集群的调度治理思路5.1 控制面节点隔离污点的默认配置如果你用过kubeadm搭建集群会发现控制面节点master节点默认自带一个污点node-role.kubernetes.io/control-plane:NoSchedule这个污点的作用不言而喻默认情况下普通业务Pod不允许调度到master节点上防止业务负载影响控制面组件的稳定性。这也是污点机制最经典的内置应用。如果你确实需要让某些Pod跑在master节点上比如自建集群机器数量有限可以有两种方式要么手动删除这个污点要么给Pod配置对应的容忍。前者风险较大不推荐在生产环境用后者更灵活但也只能在明确知道自己在做什么的情况下使用。5.2 用污点做灰度发布和故障域隔离污点机制在日常运维里还有一个用法做故障域隔离。比如机房A的一批节点上运行着重要的支付服务机房B的节点出现网络波动。如果不做隔离调度器可能把新建的支付服务Pod调度到网络不稳定的机房B节点上导致服务不可用。这时可以给机房B的节点打上failure-domainbeta:NoSchedule之类的污点让新建Pod默认不上去只让已经容忍这个污点的Pod在上面运行。等故障恢复后再移除污点。这种方法比用节点标签加nodeSelector更“安全”因为它默认是拒绝的而nodeSelector默认是允许的只要不配置就全部匹配。在故障场景下“默认拒绝”比“默认允许”要可靠得多。5.3 污点持久化与集群变更管理有人可能会问污点信息存在哪里节点重启后会不会丢失答案是污点是保存在etcd里的和节点对象绑定节点重启不会影响污点。如果你用kubectl taint命令设置的污点它会持久化保存。但这里有一个坑如果用云厂商的节点池功能节点重建后污点默认是丢失的。云厂商在创建节点时可以通过节点池的taints参数指定污点这样节点重建后污点也会保留。如果直接在节点上手动打污点节点一旦被回收重建污点就没了。在生产环境凡是需要长期生效的污点都应该在节点池配置或者基础设施即代码IaC里声明而不是靠手工命令维护。5.4 结合Kubernetes Dashboard和DevOps平台做可视化治理如果你在业务里使用了Kubernetes Dashboard或者其他可视化管理平台可以在界面上查看节点的污点信息也可以直接在UI上给节点添加污点。团队协作时把污点信息展示在Dashboard上能让运维同学不用挨个敲命令就能了解集群当前的调度约束。但可视化工具也带来了一个新问题权限控制。默认情况下任何有节点更新权限的用户都能给节点加污点如果一个新人误操作给所有节点加了NoExecute污点后果是整个集群的业务Pod被全部驱逐。所以建议在RBAC层面限制节点更新权限把taint操作收拢到少数管理员手里。我在团队里推行过一个简单规范所有涉及节点污点变更的操作必须走变更审批流程并且要有回滚预案。看起来有些“重”但生产环境吃过大亏之后都会认同这个规矩。一句话总结污点和容忍是Kubernetes调度体系里最容易被忽略、但一旦用对威力极大的机制。它解决的是节点与Pod之间“排斥”关系的问题和亲和性机制互补共同组成了一套完整的调度约束体系。从基础的三类效果到实际运维中的节点维护、GPU资源隔离、故障域隔离再到集群层面的权限治理每一个环节都值得花时间仔细打磨。在实际操作中遇到Pod调度不达预期建议先跑一遍kubectl describe node看清污点全貌再逐个核对Pod的容忍配置90%的问题都能在这两步里找到答案。