Kubernetes Pod调度全攻略:从nodeSelector到污点容忍的实战详解
干运维久了调度的坑基本都踩过一遍。Pod起不来第一反应是看Events十有八九是调度器没把Pod放到合适的节点上。今天整理的这份实操记录围绕nodeName、nodeSelector、亲和性、污点、容忍和节点删除这些调度机制把原理、配置、常见坑一次性讲清楚。不管是刚接触Kubernetes的运维新人还是被调度问题反复折磨的老手这篇都能直接拿来参考。1. 调度器的工作机制与整体调度流程1.1 kube-scheduler到底在做什么Kubernetes里的Pod调度核心是kube-scheduler这个组件。它不负责创建Pod、不负责拉镜像只做一件事决定每一个Pending状态的Pod应该放到哪个节点上。调度器的工作方式不是主动扫描而是监听API Server。当用户提交了一个Pod它会先检查Pod有没有已经绑定的节点如果没有就进入调度队列。整个调度过程可以拆成三步过滤、打分、绑定。过滤阶段把所有不满足条件的节点剔除打分阶段给剩下的节点排序最后选择得分最高的节点把Pod绑定上去。这里有一个很关键的概念调度器只会推荐一个节点并不直接操作节点上的容器。它往API Server写入Pod与节点的绑定关系然后节点上的kubelet看到这个绑定信息才会去真正创建容器。一旦理解了这条链路后面排查调度问题就会清晰很多。1.2 调度流程的三步走第一步是过滤也叫预选。调度器会检查每一个节点的资源是否充足、端口是否冲突、是否满足volume挂载条件、是否有对应的标签等。这一步不考虑哪个更优只考虑行不行。节点CPU不足、内存不够、存储不匹配都会被直接淘汰。第二步是打分也叫优选。通过了过滤的节点进入打分环节。调度器按一系列优先级策略给节点打分比如资源越空闲分数越高、已有Pod越少分数越高、镜像是否已经存在等。默认策略里资源均衡和镜像本地化占了较重权重。第三步是绑定。调度器把Pod绑定到得分最高的那个节点并将结果写入etcd。如果绑定失败调度器会重新尝试如果Pod状态异常kubelet会负责处理。整个过程从用户角度看不到但通过kubectl describe pod就能看到调度器留下的Events记录。1.3 为什么运维必须掌握调度策略很多人觉得调度是集群初始化时配好的事平时不用碰。但实际生产环境里调度策略是运维日常操作中绕不开的一环某应用内存占用大需要固定调度到大内存节点某业务要求高可用Pod必须分散在不同机器上某台物理机硬件有问题需要先把节点隔离新采购的机器只有GPU需要保证只有GPU任务能上去。这些都离不开调度策略。掌握了调度机制以后操作节点、部署应用、排查Pod Pending都不是靠猜而是有清晰的路径。下面几个章节按从简单到复杂的顺序把最常用的调度方式逐个展开。2. 硬性调度nodeName与nodeSelector的实战用法2.1 第一步不是写YAML而是给节点打标签nodeSelector依赖节点标签工作。如果节点上没有对应的标签写了也白写。所以实操的第一步是先规划好标签的命名规范再给节点打标签。给节点打标签的命令很简单kubectl label nodes node-1 disktypessd kubectl label nodes node-1 node-rolecompute kubectl label nodes node-1 node-rolecompute-第一条给node-1打上disktypessd标签第二条打上node-rolecompute标签第三条是删除标签的用法。生产环境建议标签至少包含三类角色的node-role、硬件属性的disktype、gpu-model、mem-size、机柜/可用区的rack、zone。标签命名尽量统一别一套环境一个命名规则后面维护会很痛苦。查看节点标签用下面命令kubectl get nodes --show-labels打标签之前建议先看清楚节点当前状态别把一台NotReady的节点也打了标签这样调度过去以后Pod照样起不来。2.2 nodeName最直接的指定方式nodeName是Pod级别的字段写在pod.spec.nodeName里直接指定Pod必须调度到某个节点。这种方式完全绕过调度器调度器根本不会参与决策节点上的kubelet发现绑定关系后直接创建容器。apiVersion: v1 kind: Pod metadata: name: test-node-name spec: nodeName: node-1 containers: - name: nginx image: nginx:1.25这种方式的优缺点都明显。优点是指定明确、无需等待调度器运行、可以绕过调度器进行紧急部署缺点是灵活性几乎为零节点宕机、维护、标签变化都不会影响这个绑定关系而且如果指定了一个不存在的节点Pod会一直Pending在那里报错信息是找不到节点。我平时只在两种场景用nodeName一是模拟测试调度行为时快速固定节点二是排查节点本身问题时临时把Pod放到目标节点做验证。生产业务尽量不要用一旦节点故障业务就跟着故障。2.3 nodeSelector基于标签的匹配nodeSelector是另一个简单的调度字段写在pod.spec.nodeSelector里。它只支持键值对完全匹配的语法多个标签之间是AND关系节点必须同时满足所有条件才被选中。apiVersion: v1 kind: Pod metadata: name: test-node-selector spec: nodeSelector: disktype: ssd node-role: compute containers: - name: nginx image: nginx:1.25这段配置的含义是只有同时具备disktypessd和node-rolecompute标签的节点才可能被选中。注意是可能因为调度器还会做资源过滤。nodeSelector的局限在于表达力有限。比如我想表达节点要么有ssd盘要么有大内存二选一即可nodeSelector做不到想表达只要不是gpu节点就行也做不到。它只能做精确的等值匹配。所以更灵活的亲和性调度就应运而生了。2.4 两分钟看懂两者的核心区别对比项nodeNamenodeSelector调度方式直接指定绕过调度器按标签匹配调度器参与过滤灵活性无只能指定单个节点低只能精确等值匹配适用场景测试、故障排查、单节点部署简单分组、标签明确的场景节点不存在时Pod一直Pending并报错无匹配节点时Pod Pending维护性差节点故障影响业务较好标签变化可触发重调度有限从这张表可以看出nodeName适合的是必须往这个节点放的场景nodeSelector适合的是放在这一类节点就行的场景。两者的定位偏基础生产环境组合调度中用的更多的还是亲和性和污点容忍。3. 亲和性调度把Pod放到想放的地方3.1 亲和性与硬性调度的本质区别亲和性调度Affinity分三类节点亲和性、Pod亲和性、Pod反亲和性。它比nodeSelector强大在两点一是支持丰富的操作符比如In、NotIn、Exists、DoesNotExist、Gt、Lt能够表达非、或和范围匹配二是支持软性约束也就是尽量满足而不是必须满足。亲和性里的约束分两种执行类型。requiredDuringSchedulingIgnoredDuringExecution是硬性要求调度时必须满足不满足就PendingpreferredDuringSchedulingIgnoredDuringExecution是软性偏好调度器会尽量满足但找不到完全符合的节点时会退而求其次选一个相对符合条件的节点。命名里DuringScheduling表示调度时生效IgnoredDuringExecution表示运行期间即使节点标签发生变化也不会影响已运行的Pod。这种设计非常贴近实际运维需求。比如我希望带GPU任务的Pod尽量跑到GPU节点上但如果GPU节点实在满了也可以让一部分Pod跑在普通节点上只是得加上权重控制优先级。3.2 nodeAffinity节点亲和性拆解节点亲和性最常用的体现是nodeAffinity字段。下面这段配置包含硬性和软性两种规则apiVersion: apps/v1 kind: Deployment metadata: name: backend-app spec: replicas: 3 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role operator: In values: [compute] preferredDuringSchedulingIgnoredDuringExecution: - weight: 20 preference: matchExpressions: - key: disktype operator: In values: [ssd, nvme]解析一下必须调度到node-rolecompute的节点上如果节点同时还有disktypessd或nvme标签则额外加20分。注意nodeSelectorTerms是一个数组数组里的多个条件是OR关系也就是满足任意一组即可而matchExpressions里的多个表达式是AND关系必须同时满足。操作符这里值得单独说。Exists表示存在这个key即可不管value是什么DoesNotExist表示不存在这个keyGt和Lt表示value大于/小于指定数值注意value必须是整数形式的字符串。Gt和Lt这种操作符在处理内存标签大于64G的节点这类需求时特别好用但使用时要先确认标签值统一规范否则一个节点写的是64G一个是65536就会乱套。3.3 podAffinity与podAntiAffinity让Pod分组或分散Pod亲和性解决的是这个Pod想和某个Pod在同一个拓扑区域的问题。典型的场景是计算Pod和数据缓存Pod希望在同一台机器这样访问延迟低。Pod反亲和性则相反希望两个Pod不要落在同一个拓扑区域。典型场景是高可用部署多个副本必须分布在不同的节点上防止单点故障。先看Pod反亲和性示例spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: [frontend] topologyKey: kubernetes.io/hostname含义是新Pod调度时如果某个节点上已经存在appfrontend标签的Pod那这个节点被排除。topologyKey指定了拓扑区域的定义方式。kubernetes.io/hostname表示按节点区分的也就是每个节点都是独立拓扑域落同一节点的两个Pod是违反规则的。如果要按可用区分散就换成failure-domain.beta.kubernetes.io/zone。这里有一个绕不开的坑Pod反亲和性为required时如果副本数比节点数还多那么多于节点数量的副本会一直Pending这种场景下应该用preferred版本允许尽量分散而不是强制分散。Pod亲和性的配置结构相同只是把podAntiAffinity换成podAffinity。它配合topologyKey还可以实现调度到目标Pod所在的可用区这种更大范围的控制。3.4 一处容易被忽略的细节亲和性规则里的labelSelector匹配的是Pod的标签不是节点的标签这一点很多人第一次都会搞混。nodeAffinity匹配节点标签podAffinity匹配Pod标签两条线别串。配置Pod亲和性时要确保目标Pod的标签确实存在并且没有歧义否则匹配不生效Pod就会按默认调度走。4. 污点与容忍系统的不欢迎机制4.1 Taint和Toleration的基本概念污点Taint打在节点上表示这个节点不太欢迎所有Pod。容忍Toleration打在Pod上表示虽然节点有这个污点但我可以容忍允许调度过来。打个比方节点是一间装了门禁的房间污点是门禁规则容忍是通行证。没有对应通行证的人默认不能进有通行证的人可以进甚至可以无视更严格的限制。每个污点有三个字段key、value、effect。比如kubectl taint nodes node-1 dedicatedgpu:NoSchedulekey是dedicatedvalue是gpueffect是NoSchedule。含义是只有带dedicatedgpu且effect为NoSchedule容忍的Pod才能调度到node-1。4.2 三种Effect的区别Effect决定了污点的强度。NoSchedule是最常用的。调度器看到节点带这个污点如果没有对应容忍新Pod不会被调度到这个节点但已经运行在上面的Pod不受影响。PreferNoSchedule是软性版本。调度器会尽量避免但集群资源紧张时也可能把Pod调度上来。NoExecute是最强的。不仅新的Pod调度不过来节点上已有的、没有对应容忍的Pod还会被驱逐。这就是为什么维护节点时经常会用到NoExecute污点或者直接使用内置的系统污点。Kubernetes自己会在节点异常时自动打上一些系统污点比如node.kubernetes.io/not-ready节点未就绪、node.kubernetes.io/unreachable节点不可达、node.kubernetes.io/memory-pressure内存压力等。这些污点属于系统保护机制一般不需要手动管理但排查Pod被驱逐时经常在Events里看到它们的名字。4.3 管理污点的命令打污点、删除污点、查询污点分别对应下面的命令# 打污点 kubectl taint nodes node-1 dedicatedgpu:NoSchedule # 删除污点 kubectl taint nodes node-1 dedicatedgpu:NoSchedule- # 删除key下所有污点 kubectl taint nodes node-1 dedicated- # 查看节点污点 kubectl describe node node-1 | grep -A 5 Taints删除命令的结尾多了个-这表示移除。注意删除时要写完整的key、value、effect或者只写key加一个减号否则可能删除失败。4.4 容忍配置实操容忍配置在pod的spec.tolerations字段。以下示例说明三种写法spec: tolerations: # 写法一精确匹配key、value、effect - key: dedicated operator: Equal value: gpu effect: NoSchedule # 写法二只匹配key和effect不关心value - key: dedicated operator: Exists effect: NoSchedule # 写法三容忍所有污点不推荐随便用 - operator: Exists写法一要求key、value、effect完全匹配才能生效。写法二只要key和effect对上就行value不限制。写法三是通配容忍表示这个Pod可以容忍任何污点。生产环境建议明确写出key和effect不要图省事用全容忍否则等于把节点的隔离机制全部绕过了。NoExecute还有一个特殊参数tolerationSeconds表示容忍的时长。比如spec: tolerations: - key: node.kubernetes.io/unreachable operator: Exists effect: NoExecute tolerationSeconds: 60这表示节点进入unreachable状态后Pod最多坚持60秒超过60秒就被驱逐。这在处理容忍短暂网络抖动又不想无限期容忍节点断连的场景时很实用。5. 节点维护与删除cordon、drain与节点移除5.1 节点维护三步曲做节点维护有标准操作流程顺序很重要先cordon再drain最后执行具体维护任务。cordon是封锁节点作用是标记节点为不可调度。执行后新Pod不会调度上来但已有Pod继续运行业务不受影响。kubectl cordon node-1执行完后node-1的状态会显示为Ready,SchedulingDisabled。如果只是临时维护维护完用uncordon解除即可kubectl uncordon node-1drain是排空节点把节点上的Pod驱逐到其他可用节点kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data --force参数说明--ignore-daemonsets忽略DaemonSet管理的Pod。因为DaemonSet是每个节点都要跑的驱逐后它还会在同节点重建所以必须忽略。--delete-emptydir-data允许删除使用emptyDir临时卷的Pod。默认情况下含有emptyDir的Pod不会被驱逐这个参数相当于告知可以删除这些临时数据。--force当节点上有非DaemonSet管理的裸Pod时强制驱逐。裸Pod没有控制器管理驱逐后不会自动重建所以执行前要确认这些Pod本来就不需要恢复。drain执行过程中会先把节点标记为不可调度再逐个驱逐Pod。如果某个Pod的POD中断预算PodDisruptionBudget简称PDB不允许打断太多副本drain会等待PDB允许后才执行。生产环境调度权限紧张时drain可能卡住很长时间这时候要检查是哪些PDB卡住了。5.2 彻底删除节点如果节点要下线退役而不是临时维护那就要从集群中彻底删除。先执行cordon和drain把所有Pod赶走然后执行deletekubectl delete node node-1删除之后节点上的kubelet如果还在运行会尝试重新注册节点。所以彻底下线前建议先在节点上停止kubelet服务或直接重装系统避免节点复活后又出现在集群里。5.3 节点重新加入集群如果删除后想重新加回来或者节点系统重装后要重新入集群流程是# 在节点上执行 kubeadm reset # 重新加入集群 kubeadm join 控制面地址:6443 --token token --discovery-token-ca-cert-hash hashkubeadm reset会把节点上的容器、自定义配置清空。重新join之前确保控制面的token还有效token过期了要用kubeadm token create重新生成。加入后节点会因为继承了之前设置的标签而恢复分组但如果用的是kubeadm初始化时的默认节点名称新节点会带着之前的角色标签重新注册。注意节点重新加入后之前打的标签和污点也会一并保留如果想让它是全新的状态加入后先检查一下Taints和Labels把不需要的清理干净。6. 综合场景一套完整的多级调度策略配置6.1 场景背景与节点规划说了这么多知识点组合起来看一个完整案例。假设某公司的一个微服务系统包含前端、后端和数据库三个模块节点规划如下节点角色标签硬件情况额外标签node-1node-rolecomputeGPU机器gputrue, disktypenvmenode-2node-rolecompute高内存机器membig, disktypessdnode-3node-rolecompute普通机器disktypessdnode-4node-rolecompute普通机器disktypesatanode-5node-rolefront普通机器disktypesata需求如下数据库服务必须跑在GPU机器上且只能跑在GPU机器上后端服务尽量跑在高内存机器上如果资源不足也可以退到普通机器前端服务必须有3个副本且副本必须分布在不同的节点保证高可用所有服务都禁止调度到node-5这个专用前端节点。6.2 节点标签与污点规划针对需求先把标签和污点准备好# 给node-1打GPU和高性能磁盘标签并打上GPU专用污点 kubectl label nodes node-1 gputrue kubectl label nodes node-1 disktypenvme kubectl taint nodes node-1 dedicatedgpu:NoSchedule # 给node-2打高内存标签 kubectl label nodes node-2 membig kubectl label nodes node-2 disktypessd # 给node-5打前端专用标签且打上拒绝后端业务的污点 kubectl label nodes node-5 dedicatedfrontend kubectl taint nodes node-5 dedicatedfrontend:NoSchedulenode-1的GPU污点确保只有数据库服务的Pod带容忍node-5的前端污点确保后端和数据库的Pod不会被调度上去。6.3 完整YAML配置与验证数据库服务apiVersion: apps/v1 kind: Deployment metadata: name: database spec: replicas: 1 selector: matchLabels: app: database template: metadata: labels: app: database spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: gpu operator: In values: [true] tolerations: - key: dedicated operator: Equal value: gpu effect: NoSchedule containers: - name: db image: postgres:16数据库强制调度到gputrue的节点同时因为节点有dedicatedgpu:NoSchedule污点只有带对应容忍的Pod能上去。两者叠加保证数据库只跑在node-1。后端服务apiVersion: apps/v1 kind: Deployment metadata: name: backend spec: replicas: 3 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role operator: In values: [compute] - key: dedicated operator: DoesNotExist preferredDuringSchedulingIgnoredDuringExecution: - weight: 40 preference: matchExpressions: - key: mem operator: In values: [big] containers: - name: backend image: backend:latest后端要求节点必须有node-rolecompute标签同时不能有dedicated标签这样它就避开了node-5。软性偏好是membig表示如果集群里有其它空闲节点优先选大内存节点但大内存节点资源不足时可以放普通节点。前端服务apiVersion: apps/v1 kind: Deployment metadata: name: frontend spec: replicas: 3 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role operator: In values: [compute, front] podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: [frontend] topologyKey: kubernetes.io/hostname containers: - name: frontend image: frontend:latest前端有3个副本用required的podAntiAffinity强制不能让两个前端副本落在同一台机器上。如果集群里只有2台机器满足条件第3个副本会一直Pending这也是我要在前面强调required反亲和要注意副本数和节点数关系的原因。6.4 验证调度结果部署后通过以下命令验证kubectl get pods -o wide kubectl get pods -o wide | grep frontend看每个Pod的NODE列是否和预期一致。如果某个Pod一直Pending用describe排查kubectl describe pod pod-nameEvents里会有调度器留下的提示比如FailedScheduling、0/5 nodes are available等。结合前面说到的过滤条件逐项核对即可。组合调度策略的原则就是nodeAffinity控制放到满足条件的节点toleration控制允许进入有污点的节点podAntiAffinity控制避开同一拓扑区域。三者各管一摊配合起来就能实现生产环境里大部分复杂调度需求。7. 常见问题与排查技巧实操实录7.1 Pod一直Pending怎么排查这是遇到最多的问题。排查路径基本固定第一步看Pod状态kubectl get pods第二步看调度事件kubectl describe pod pod-name重点看Events里的FailedScheduling记录它后面会直接列出原因。常见的原因比如0/5 nodes are available: 1 Insufficient cpu, 2 node(s) didnt match node selector, 3 node(s) had taint。这句话已经帮你定位了大致方向。第三步判断原因类别资源不足、标签不匹配、污点拒绝、节点不可调度。分别检查节点资源、节点标签、污点、节点状态。7.2 节点筛选失败的三种可能性节点筛选失败看着像没有匹配节点实际上可能是三种情况节点标签根本不是你想的那样用kubectl get nodes --show-labels检查实际标签节点资源不够用kubectl describe node检查Allocatable和Requests节点处于NotReady或SchedulingDisabled状态用kubectl get nodes看状态列。实际排障时很多人第一反应是改YAML其实应该先确认是不是节点侧的问题不然改了也没用。7.3 污点不生效的几个隐藏原因污点配置了但Pod还是调度过去了原因通常是这几个一是污点的key、value、effect没有完全匹配。比如节点污点是dedicatedgpu:NoSchedulePod容忍写的是key: dedicated、value: gpu但effect没写就不会匹配。二是operator写错了。Equal是必须value相同Exists则只需要key存在。如果节点污点的value是gpu而容忍里写的是Exists并且key匹配这也是能生效的但反过来如果value对不上而operator是Equal就不生效。三是namespace或资源组搞混用了错误的Deployment配置。这种低级错误其实最常见排查时先看Pod的实际标签和配置是否正确。7.4 多级调度策略冲突时的取舍当nodeSelector、nodeAffinity、污点容忍同时配置时需要理解它们之间是叠加关系不是互相替代。调度器判断时任何一条硬性约束不满足Pod都无法调度。软性约束的偏好只是在满足硬性约束的节点里选一个得分最高的。如果出现我明明配了preferred亲和性但Pod还是分到了不符合偏好的节点这是正常的因为preferred本来就不保证。真正要在意的是required条件是否都满足、冲突的硬性条件是否能找到同时满足的节点。7.5 排障常用命令清单场景命令看所有Pod分布kubectl get pods -o wide看调度事件kubectl describe pod看节点标签kubectl get nodes --show-labels看节点资源与污点kubectl describe node临时修改节点调度kubectl cordon/uncordon驱逐节点Podkubectl drain查看Pod调度策略kubectl get pod -o yaml这套命令配合起来大部分调度问题半小时内能定位。如果排查不到再考虑检查coredns、kube-scheduler日志以及API Server的Etcd状态。8. 实操心得与经验总结8.1 调度策略的设计原则经过多次生产环境踩坑我现在设计调度策略时会坚持三条原则。第一标签是基础先把节点规划好再谈调度。没有清晰统一的标签体系写多少亲和性规则都是空中楼阁。建议每套集群最少有一份节点标签规范文档把角色、硬件、可用区、维护状态这些内容全部标准化。第二能用简单的组合就不用复杂的配置。如果nodeSelector已经能满足需求就不要上podAntiAffinity如果preferred就够用别强行required。调度规则写得太重排障时看YAML都费劲。第三永远考虑不满足条件时会发生什么。副本数量超过节点数量怎么办某个标签误删了会不会全部Pending这就是为什么我建议性子急的人少用required多用preferred加权重。8.2 我踩过的坑之前帮一个客户排障数据库Pod一直没有调度成功。排查后发现节点的GPU污点配置正确但数据库Deployment的YAML里nodeAffinity用的是preferred而且没写required的节点角色约束导致调度器把数据库Pod放到了普通节点上数据存储性能直接崩了。这个案例让我意识到数据库这类重应用的关键约束一定要用required不能心存侥幸。还有一次维护节点drain卡住了。原因是某个应用自己配了更新版本的PDB限制了最小可用副本数。drain在等PDB放行。最后只能先调整PDB再重新drain。这个经历让我养成了执行drain之前先检查节点上所有PDB的习惯。8.3 最后再分享一个小技巧如果你要批量检查节点上所有Pod的调度配置可以用下面这个命令把Pod的调度相关内容一次性导出来kubectl get pods -n namespace -o jsonpath{range .items[*]}{.metadata.name}{\t}{.spec.nodeName}{\t}{.spec.nodeSelector}{\n}{end}实际使用中我经常加一个jsonpath过滤模板把nodeSelector、affinity、tolerations都打印出来比逐个describe节约非常多时间。调到卡壳的时候把这几个字段摊开看问题往往一眼就能找到。这些都是踩坑换来的经验希望能帮你在调度这条路上走得更顺一点。