ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes污点与容忍度实战:从原理到排障一网打尽

Kubernetes污点与容忍度实战:从原理到排障一网打尽 生产环境跑了一段时间 Kubernetes 之后你会发现节点资源调度这件事光靠按标签分组和亲和性根本不够。比如你想把某几个节点专门留给数据库或者让监控组件必须跑到所有节点上再或者集群里有几台机器硬件老化需要标记出来别让新业务落上去——这种我要拒绝某些Pod来的需求标签选择器做不到亲和性也做不到。这个时候你能用的就是污点Taints和容忍度Tolerations。Kubernetes 里这套机制的核心思路特别简单给节点打上污点就像在门上挂了一块某些人请勿入内的牌子给 Pod 配置容忍度就像是给特定的人发了一张特别通行证。有通行证的 Pod 可以无视牌子往里走没有通行证的 Pod 会被调度器自动安排到其他节点。这套机制从设计之初就是为了解决节点与 Pod 之间的排斥关系和亲和性正好是一对互补的工具。这篇文章我会把污点和容忍度的原理、语法、实操以及我在排障中踩过的坑一次讲清楚适合刚接触到调度策略的运维和开发也适合已经在用但有些细节没搞明白的人。1. 污点和容忍度到底解决什么问题1.1 从一次误调度事故说起先讲一个我实际遇到过的事情。之前管理的一个集群里有两台机器是专门的 GPU 节点给训练任务用的。有一天新上线了一个 Web 服务它既没要求 GPU也没写任何调度相关的配置。结果 Pod 一起调度器顺手就把其中两个副本放到了 GPU 节点上。训练任务跑起来需要占满显存Web 服务在 CPU 和内存上也不算省心两边挤在一起训练任务直接 OOMWeb 服务也出现大量超时。当时整个集群没有做节点资源隔离因为节点数不多大家觉得没必要。但这个问题暴露了一个事实调度器只关心节点是否满足 Pod 的资源需求它并不知道哪些节点是专用的哪些节点是共享的。我缺一个表达这节点不欢迎你的机制。后来我做了两个动作给 GPU 节点打上污点gputrue:NoSchedule给训练任务的 Deployment 加上对应的容忍度。从此调度器再也不把普通业务放到 GPU 节点上问题彻底消失。这就是污点与容忍度最核心的价值节点通过污点表达偏好Pod 通过容忍度表达身份两边各管各的调度器按规则执行。对比一下另外两个常用工具就能看得更清楚。1.2 语义对比节点选择器和亲和性差在哪Kubernetes 里控制 Pod 落在哪个节点上主要有三套机制nodeSelector、nodeAffinity、污点和容忍度。很多人刚接触时会混淆觉得它们都是用来挑节点的其实它们的方向完全不同。nodeSelector和nodeAffinity表达的是Pod 对节点的需求我要跑在带diskssd标签的节点上。这是从 Pod 视角出发的正向选择。如果没有任何节点满足条件Pod 就一直 Pending 在那里等着。污点表达的是节点对 Pod 的拒绝我不接受不带通行证的 Pod 进来。这是从节点视角出发的反向选择。这种语义有一个天然优势即使你集群里后面新加了节点只要新节点没打污点就不影响任何 Pod 的调度如果新节点打了污点同样能自动屏蔽掉不相关的业务。维护成本比逐个改nodeSelector低很多。你完全可以把一套机制组合起来用用nodeAffinity表达我倾向于去某类节点用污点表达某些节点绝对不能放普通业务两者互不冲突。实际上生产环境里很多团队就是同时用这两种方式做双保险的——首先通过亲和性把特定 Pod 引导到特定节点池再用污点把其他不该来的 Pod 挡在外面。打个比方亲和性是我自己主动想去的方向污点是门口保安不允许我进。你出门前既要知道自己想去哪也要知道哪些地方没有特别通行证就进不去。两件事都不冲突一起考虑才完整。2. 核心概念与语法把定义吃透2.1 污点的三个组成要素与四种 effect一个污点Taint本质上就是一条由key、value、effect三个字段组成的信息挂在节点上。key和value好理解就是键值对通常用来描述污点的类型或来源。effect才是真正决定调度行为的字段它一共有四种取值其中三种常用一种是 Alpha 特性。effect 值调度层面的影响对已有 Pod 的影响典型应用场景NoSchedule新 Pod 如果没有匹配的容忍度不会被调度到该节点节点上已运行的 Pod 不受影响继续运行把某个节点从调度池里摘出去用于维护、隔离、专用节点PreferNoSchedule调度器会尽量避免把不匹配的 Pod 放到该节点但不保证绝对不调度已运行的 Pod 不受影响软性规避比如某节点性能较差希望尽量少跑业务NoExecute新 Pod 若无匹配容忍度不会被调度到该节点节点上已运行的、无匹配容忍度的 Pod 会被驱逐节点故障、安全隔离、立即清理该节点上的 PodNoExecute的扩展字段tolerationSeconds实际属于容忍度一侧如果 Pod 有容忍度且指定了tolerationSeconds则到期后仍被驱逐控制驱逐的宽限期节点维护前给 Pod 一个优雅退出窗口第一种NoSchedule是最常用的打上去之后只是不让新 Pod 进来已经在那跑的不受影响。第二种PreferNoSchedule是软性的调度器会尝试避开该节点但如果确实没有更合适的节点它还是会调过去。第三种NoExecute是最狠的它不仅拦新 Pod还会把节点上所有没有对应容忍度的存量 Pod 全部驱逐掉。我一般只在节点磁盘故障、内存持续飙高这类真需要清场的情况下才用。kubeadm 初始化的集群里master 节点上默认就带了一个node-role.kubernetes.io/master:NoSchedule新版本是node-role.kubernetes.io/control-plane:NoSchedule目的就是防止普通业务 Pod 被调度到控制平面节点上。这个默认行为如果你不知道直接用kubeadm init创建的集群你会看到业务 Pod 怎么都调度不到 master 上——不是节点有问题而是门卫不让进。2.2 容忍度语法operator 与书写规则容忍度Toleration配置在 Pod 的spec.tolerations字段里语法核心是我要匹配节点上的哪条污点。它有两种写法对应operator字段的两种取值。第一种写法是operator: Equal表示精确匹配需要key、value、effect三个字段全部和节点上的污点一致才能算匹配上。这也是最容易理解的写法。tolerations: - key: gpu operator: Equal value: true effect: NoSchedule上面这段表达的意思是这个 Pod 容忍节点上形如gputrue:NoSchedule的污点。注意如果节点上挂着的是gpufalse:NoSchedule那这条容忍度是不起作用的因为 value 不匹配。第二种写法是operator: Exists表示只要 key 存在即可value 无所谓effect 也可以不写。不写 effect 时它匹配该 key 下的所有 effect 类型。tolerations: - key: gpu operator: Exists这行的意思是只要节点上存在任意一条以gpu为 key 的污点不管 value 是什么、effect 是什么这个 Pod 都能容忍。还有一种比较特殊的情况如果你写operator: Exists且不写key那这条容忍度会匹配节点上的所有污点。这是一种我谁都能忍的无敌配置通常在系统组件比如网络插件 DaemonSet里见得到日常业务不建议这么写否则等于没有门槛任何节点都能把你调度上去。两种 operator 的选择原则很简单如果你想针对某一条具体污点做精准放行用Equal如果你只关心 key 不关心 value或者想兼容各种 effect 类型用Exists。2.3 常见误区匹配规则与容忍范围这里有一个新手几乎必踩的误区以为 Pod 要能调度到某节点就必须在tolerations里把节点上的所有污点全部列出来。实际上完全不是这样。Kubernetes 的匹配逻辑是只要有一条容忍度能匹配上节点上的任意一条污点这个 Pod 就可以调度到该节点。更准确地说调度器会逐个检查节点上的污点检查 Pod 的容忍度列表里是否存在能匹配当前的污点的条目——只要存在匹配就认为这个污点被容忍了。如果节点上所有污点都能被容忍度集合覆盖就允许调度如果有一条污点没有任何容忍度能匹配就拒绝调度。举个例子节点上有两条污点dedicateddb:NoSchedule gputrue:NoExecutePod 的容忍度里只写了tolerations: - key: dedicated operator: Equal value: db effect: NoSchedule这时候调度器检查dedicateddb:NoSchedule时发现匹配可以容忍检查gputrue:NoExecute时找不到匹配的容忍度于是判定这个 Pod 不能调度到该节点。因为你只需要匹配每一条污点即可而不是匹配所有污点。再有就是容忍度并不只对调度生效NoExecute类型的污点还会触发驱逐机制。很多人只记住了它拦截新 Pod忘了它还会清理存量 Pod结果给节点打上NoExecute污点后整个节点上的业务瞬间清零以为自己误操作了其实这本来就是设计好的行为。3. 实操全流程从命令行到 YAML3.1 打污点、查污点、去污点在实际运维中给节点打污点的操作比想象中频繁。我梳理一下最常用的几个命令都是踩过坑之后沉淀下来的。先查看节点现有的污点用kubectl describe是最直观的kubectl describe node node1在输出里找Taints字段如果节点没有污点显示的是空有污点的话会列出类似dedicateddb:NoSchedule这样的记录。有时节点多了之后用describe一页页翻太慢可以直接用 JSON 提取kubectl get node node1 -o jsonpath{.spec.taints}给节点打污点的标准格式是kubectl taint nodes node-name keyvalue:effectkubectl taint nodes node1 dedicateddb:NoSchedule这条命令执行后node1 上就多了一条dedicateddb:NoSchedule污点。以后再创建的新 Pod如果没写对应的容忍度调度器就不会把 Pod 放上去。如果要取消污点在命令末尾加一个减号即可kubectl taint nodes node1 dedicateddb:NoSchedule-这个减号一定要写在最后和 effect 之间没有空格。写错格式的话kubectl 会报错提示。如果节点上有多条污点想去掉某一条必须把 key、value、effect 原样写上去再加减号。也有人只写 key 就去取消比如kubectl taint nodes node1 dedicated-这个操作会移除所有以dedicated为 key 的污点不管是哪种 effect。我觉得这个用法挺实用的尤其是你想批量清掉某一类污点时效率高很多。还有一个小技巧操作前最好先确认节点名称。很多生产环境里节点主机名不是node1这种好记的名字而是类似ip-10-0-1-123这种长名称。用kubectl get nodes先看一眼避免敲错导致提示找不到节点。我确实见过有人把节点名写错然后开始排查为什么 Pod 不调度——其实污点根本没打上。3.2 一节完整的容忍度 YAML 实例光有污点没有容忍度业务 Pod 就会被挡在外面。实际配置容忍度时我习惯直接把整段 YAML 放在 Deployment 或 StatefulSet 的spec.template.spec下下面给一个完整示例apiVersion: apps/v1 kind: Deployment metadata: name: nginx-gpu namespace: production spec: replicas: 3 selector: matchLabels: app: nginx-gpu template: metadata: labels: app: nginx-gpu spec: tolerations: - key: gpu operator: Equal value: true effect: NoSchedule - key: gpu operator: Equal value: true effect: NoExecute tolerationSeconds: 3600 containers: - name: nginx image: nginx:1.25 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Gi这个配置有两段容忍度。第一段gputrue:NoSchedule让它能够被调度到打了这个污点的 GPU 节点上。第二段gputrue:NoExecute加了tolerationSeconds: 3600意思是即使该节点上存在NoExecute污点Pod 也能在节点上继续运行 3600 秒之后如果污点还在才被驱逐。有一种情况需要重点说明如果节点上同时存在NoSchedule和NoExecute两条污点且 Pod 只写了NoSchedule的容忍度那么新 Pod 无法调度过来因为NoExecute那条污点没有被容忍。如果节点上只有NoExecute污点Pod 没写对应容忍度即使它已经在节点上运行也会被立即驱逐。在生产环境里我经常把tolerationSeconds当作优雅退出的缓冲时间用。比如计划对某个节点做维护提前给它打上maintenancetrue:NoExecute污点业务 Pod 如果带有tolerationSeconds: 300的容忍度那么维护命令执行后Pod 会在 300 秒内被平滑驱逐。这比直接用kubectl drain暴力驱逐要温和得多。3.3 master 节点污点与业务部署回避如果你用 kubeadm 初始化集群默认情况下 master 节点上会带有node-role.kubernetes.io/master:NoSchedule或node-role.kubernetes.io/control-plane:NoSchedule污点。初始化日志里你会看到类似[init] using kubernetes version: v1.26.0、[preflight] running pre-flight checks的过程等集群起来后kubectl get nodes能看到 master 节点处于Ready状态但普通业务 Pod 永远不会调度上去就是这条污点在起作用。网上很多教程会教你怎么把这个污点去掉让业务 Pod 也能调度到 master 节点。我的建议是除非你的集群实在太小、节点太少否则千万别这么干。master 节点上跑着 etcd、kube-apiserver、controller-manager 这些核心组件如果混入高负载业务 Pod一旦资源被抢占整个集群的稳定性都会受影响。如果实在有某个组件必须跑在 master 节点上比如有些严格的网络插件要求 master 上的 kube-proxy 必须调度成功正确做法是给那个 Pod 加上对应的容忍度而不是全局去掉污点。比如tolerations: - operator: Exists有人会写这种容忍所有污点的配置仅仅是为了让一个 Pod 上 master。能用但暴力相当于把门卫的通行标准全部废掉。我自己的习惯是具体污点具体写比如tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule这样这个 Pod 只对 control-plane 的污点放行其他污点该卡还是卡风险可控。4. NoExecute 与驱逐真正的高级用法4.1 NoExecute 驱逐逻辑与 tolerationSecondsNoExecute之所以特殊是因为它同时影响调度和驱逐两层行为。节点上只要存在NoExecute污点调度器就不会把没有对应容忍度的新 Pod 放过来同时 kubelet 也会监视节点状态把已经运行在该节点上的、且没有对应容忍度的 Pod 全部杀掉。对于已经在节点上运行的 Pod行为分成三种情况Pod 情况kubelet 处理行为没有匹配的容忍度立即被驱逐有匹配容忍度但没有设置tolerationSeconds永久容忍驱逐不触发有匹配容忍度且设置了tolerationSeconds容忍倒计时结束后被驱逐到期时间从污点添加到节点那一刻开始计算也就是说tolerationSeconds给了你一个临时豁免权。我举个实际用法某台节点需要紧急下线但上面有数据库的 Pod我希望给它两分钟的优雅关闭时间让它能刷盘结束事务。我就可以这样做kubectl taint nodes node-db-shard-03 shutdowntrue:NoExecute假设数据库 Pod 上带了tolerations: - key: shutdown operator: Equal value: true effect: NoExecute tolerationSeconds: 120那么从污点打上的那一瞬间起这个 Pod 会在 120 秒后被强制驱逐。120 秒内Pod 还在正常处理请求应用层可以做优雅下线处理。等到 kubelet 开始驱逐时数据已经保存完毕风险大幅降低。这里有个容易忽略的细节如果节点上的NoExecute污点在两分钟之内被移除了tolerationSeconds的倒计时也会被取消Pod 继续正常运行。所以这相当于是给节点维护操作加上了一个软时限很灵活。4.2 DaemonSet 的默认容忍与普适场景DaemonSet 在 Kubernetes 里承担着每个节点都必须跑一个 Pod 的任务比如kube-proxy、calico-node、fluentd这些。为了让它们在所有节点上都能运行系统给 DaemonSet 内置了针对NoSchedule和NoExecute的无限容忍度。换句话说就算你给某个节点打了NoSchedule污点DaemonSet 的 Pod 依然会被调度上去就算打了NoExecute污点已在运行的 DaemonSet Pod 也不会被驱逐。这个默认行为在大多数场景下是合理的你总不希望网络插件因为节点上有污点就装不上去导致这个节点网络不通。但也有例外我之前在一个多租户集群里遇到过麻烦用户数据面节点被打上了很严格的租户污点为了避免计算节点被平台组件过多占用我希望能把某些 DaemonSet 组件排除在特定节点之外。默认行为偏偏拦不住它。这种情况下就得靠nodeSelector或亲和性来配合限制 DaemonSet 的调度范围。没有绝对的默认行为能适配所有场景组合使用才能达到预期效果。DaemonSet 默认容忍这条规则也提醒我们当你在排查为什么这个节点上的 DaemonSet Pod 还在时不要以为自己的污点没生效——先确认你查看的对象是不是 DaemonSet 控制的 Pod。4.3 节点池隔离、告警检测等延伸用法既然理解了污点的拒绝语义就可以玩出一些比较高级的场景。第一个是节点池隔离。好多团队会按节点用途打不同的污点比如dedicatedredis:NoSchedule只让 Redis 类业务调度dedicatedtraining:NoSchedule只让训练任务调度gputrue:NoSchedule只让 GPU 任务调度这样即便集群没有用复杂的节点池管理系统也能在调度层面实现逻辑隔离。再加上对应的业务 Deployment 里写容忍度就能非常精确地控制每个 Pod 的落点。第二个场景是做异常节点标记。比如某节点的磁盘 IO 持续异常但节点还没完全挂掉。我非常不建议直接kubectl delete node这时候可以给它打个degradedtrue:NoExecute污点不用NoSchedule而是用带驱逐能力的NoExecute存量业务马上被排走保留节点本身供排查。等修复完成后再把污点去掉节点重新进入调度池。第三个场景是配合PriorityClass做抢占控制。重要系统组件比如指标采集、DNS可以同时设置高优先级和容忍度普通业务不设置容忍度。一旦节点出现故障调度器会优先保证这些高优先级组件的调度请求被满足而普通 Pod 自然被挡在外面。这是一种低成本、见效快的保障手段。5. 实战排障Pod 为什么不调度5.1 问题现象与分析路径几乎每个用过污点的人都遇到过Pod 一直 Pending的问题。排查路径其实很固定我总结了一套自己的方法。第一步看事件。kubectl describe pod pod-name里Events部分会直接告诉你调度失败的原因。如果是污点导致你会看到类似0/5 nodes are available: 5 node(s) had untolerated taint {dedicated: db}这样的提示。这个信息基本是终极定位不用再瞎猜。第二步看节点污点。kubectl describe node node-name拉到Taints字段确认节点上的污点是什么。然后对比 Pod 的tolerations看有没有漏写 key、value、effect 对不上的情况。第三步检查容忍度语法。最常见的问题是operator: Equal情况下 value 写错。比如节点污点是gputrue:NoSchedule容忍度里写的是value: True大小写不一致匹配失败。Kubernetes 对字符串值是严格匹配的不是英语阅读理解大小写任何差异都不放过。第四步检查是否被其他机制影响。污点只是 Pending 原因之一节点NotReady、CPU 内存资源不足、PVC 无法挂载同样会造成 Pending。不要一看到 Pending 就说是污点问题要结合事件信息一起判断。5.2 一个从 Pending 到 Running 的排查实录我在线上处理过这样一个案例写出来分享一下完整过程。同事反馈新上的服务一直 Pending。我看了一眼 Deployment发现没有设置任何调度规则按理说集群有几十个节点不应该调度不上去。kubectl get pods -n production显示 Pod 处于Pending再用kubectl describe pod查看事件输出里有这样一行Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 30s default-scheduler 0/32 nodes are available: 12 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 20 node(s) had resource cpu insufficient.这个信息非常有价值。它明确告诉我们 32 个节点里12 个是 master 带 control-plane 污点20 个是因为 CPU 资源不足。问题并不是污点没配好而是业务 Pod 申请的资源太大所有可用节点都装不下了。我去查了这个服务的 resource requests发现 CPU 请求写的是9000m但整个工作节点每台只有 8 核。也就是说一台节点最多只能塞进一个这样的 Pod而且由于其他业务已经占了一部分资源20 台节点全都剩不下 9 核的空闲。最后的解决方法是把 requests 降到4000mPod 立刻调度成功。这次排障给我一个很重要的提醒看到had untolerated taint时不要立刻认为是污点的问题也可能是多个原因叠加。处理原则是先按事件信息逐条排除优先解决资源不足再看污点匹配。5.3 避坑清单与检查速查表我把这些年遇到过的和污点相关的问题整理成一张速查表希望对你有帮助。现象可能原因排查命令常用解法Pod 一直 Pending没有写容忍度或 key/value 不匹配kubectl describe pod看事件补容忍度核对 key/value/effect节点上某 Pod 被突然驱逐节点被打上NoExecute污点且 Pod 无容忍度kubectl describe node看 Taints给 Pod 加容忍度避免随意打NoExecute污点DaemonSet Pod 出现在所有节点DaemonSet 默认容忍全部污点kubectl get ds -A配合nodeSelector限制范围master 节点上出现业务 Pod有人手动加了容忍度或删了默认污点查看节点 Taints删掉不必要的容忍度恢复默认污点去掉污点后 Pod 仍不调度节点 NotReady 或资源不足kubectl describe node看 Conditions检查 kubelet 状态、资源水位多个污点叠加导致调度失败Pod 只匹配了其中部分污点列表对比节点污点和 Pod 容忍度补齐每条污点对应的容忍度还有一个比较隐蔽的坑如果你给某个节点同时打了多段污点比如NoSchedule和NoExecute并存Pod 的容忍度列表需要能够覆盖所有污点才能调度。哪怕漏掉一条事件里都会明确提示是哪条污点没被容忍。我曾经因为NoExecute后缀写错导致一个本该被调度的 Pod 等了十几分钟后来看事件才发现自己把tolerationSeconds放在了没用的字段上kubectl 并没有帮你校验配置文件里的语义只是简单跳过。在 YAML 里写好容忍度后可以用kubectl apply --dry-runserver -f deployment.yaml检查一遍 API 层面的校验。虽然它不会主动帮你检查业务逻辑但至少能过滤掉拼写错误、缩进错误这类低级问题。6. 写在最后的经验个人体会污点和容忍度是那种一学会就觉得简单、但遇到问题才觉得水很深的机制。它表面上只有两三个字段实际上牵扯到调度、驱逐、节点生命周期管理、DaemonSet 行为等多个环节。我踩过的坑里最典型的还是把NoSchedule和NoExecute混为一谈——以为只是调不调得上去的区别忽略了驱逐这一层威力。建议从小集群开始做练习自己拿 kubeadm 起一个三节点集群给 worker1 打gputrue:NoSchedule分别创建不带容忍度和带容忍度的 Deployment观察它们的调度结果再试一次打NoExecute看存量 Pod 会发生什么。这套实验做下来整个机制基本就刻在脑子里了。最后分享一个小技巧写污点 key 时尽量采用域名前缀 描述性名称的格式比如dedicated.example.com/gpu。看到的人能一眼知道这个污点是谁定义、什么用途排查问题的时候少很多沟通成本。毕竟 Kubernetes 集群是多人协作的环境一个一清二楚的污点比什么都强。
返回列表