☰
K8s集群调度实战:从kube-scheduler到Pod卡Pending排查
2026/10/7 11:17:37 网站建设 项目流程

生产环境里最常干的一类事儿就是:一堆 Pod 建好之后始终卡在 Pending,你问同事为啥,他大概率甩给你一句“调度不上”。这个“调度”,指的就是 Kubernetes 的集群调度能力。我这些年排查过不少类似的问题,从资源不够、污点没容忍、亲和性冲突到自定义调度器接错,基本把集群调度的边边角角都撞过一遍。这篇博客就把 Kubernetes 集群调度这套东西从头到尾捋一遍,包括调度器 kube-scheduler 的工作机制、基础调度策略、亲和性、污点与容忍,以及真实环境里常见的问题排查套路。适合正在学 K8s 的中级用户、刚上手集群运维的同行,也适合那些想把 Pod 打得更合理、想彻底搞明白为什么 Pod “跑不到想要的机器上”的人。

1. 调度器在集群里到底是干什么的

1.1 kube-scheduler 不是“执行者”,而是“分配者”

很多刚接触 Kubernetes 的人容易把调度器理解成“负责把 Pod 拉起来的组件”,这其实是 kubelet 的活儿。kube-scheduler 的职责非常单一:为一个还未绑定节点的 Pod,从集群里挑一个最合适的 Node,然后把“谁应该跑在哪台机器上”这个结论写进 etcd。真正去创建容器、拉镜像、启动进程的是节点上的 kubelet。打个比方:调度器是公司前台,负责分配工位;kubelet 是工位上的行政,负责把电脑、椅子、网线都铺好。前台只管“哪个人去哪个位子”,不管你怎么搬电脑。

这个定位决定了调度器有一个非常重要的特性:它不持有任何业务容器的状态,也不做真正意义上的“启动操作”。它只做决策。这个决策过程在源码里对应着 scheduler 的 scheduleOne 函数:拿到一个待调度 Pod,走一遍过滤和打分,选出一个 Node,然后通过 Binding 写 API Server。整个链路是异步的,调度器通过 informer 监听 Pod 变化,发现 Pod 的 spec.nodeName 为空时,才会进入调度流程。

1.2 集群调度解决的核心问题

调度要解决的根本问题是“怎么把有限的集群资源合理地分配给大量 Pod”。看似简单,实际复杂得多。集群里每台 Node 的 CPU、内存、磁盘、GPU、网络带宽都不一样,每个 Pod 的要求也不一样:有的必须跑在 SSD 节点上,有的希望和另一个 Pod 在同一个可用区,有的绝对不能和某个应用共用一台机器。如果没有调度器,这些约束全靠人肉分配,集群规模一上来就彻底失控。

调度器的价值就是把“资源匹配”和“策略约束”这两件事自动化。资源匹配保证 Pod 需要的 CPU、内存等不超过节点可用资源;策略约束保证业务上的一些要求,比如环境隔离、容灾打散、故障域错开,都能在调度时被满足。Kubernetes 集群调度是一套分层解耦的插件体系,默认行为覆盖了绝大多数场景,同时又留了扩展口子给特殊需求,这也是它比很多自研调度系统更好用的原因。

2. 一条 Pod 从创建到调度的完整链路

2.1 Pod 绑定节点的链路拆解

我把这个链路拆成四步,方便记忆:

  1. 客户端提交 Pod 到 API Server,Pod 被写入 etcd。此时 Pod 的 spec.nodeName 是空的,status.phase 是 Pending。
  2. kube-scheduler 通过 watch 机制感知到新 Pod,进入调度流程。先做预选(过滤),把所有不满足条件的 Node 剔除;再做优选(打分),给剩余 Node 打分排序。
  3. 调度器挑出得分最高的 Node,构造一个 Binding 对象,绑定 Pod 到 Node。这个绑定动作通过 POST 请求更新 API Server,核心结果就是写入 Pod 的 spec.nodeName。
  4. 节点上的 kubelet 一直 watch 着绑定到自己身上的 Pod,发现有新 Pod 后开始创建容器、挂载存储、启动网络,最终把 Pod 状态推进到 Running。

很多人有个误解,认为 Pod 是“调度器拉起的”,不是。调度器只负责“指路”,kubelet 才是“开车的人”。这一步搞混了,后面排查问题会走很多弯路。

2.2 调度周期和绑定周期

从源码角度看,kube-scheduler 将一个 Pod 的调度过程拆成两个周期:调度周期(Scheduling Cycle)和绑定周期(Binding Cycle)。调度周期内完成节点过滤、打分、选择,整个过程在单线程内串行执行,一个 Pod 接着一个 Pod 处理。绑定周期则负责把选定结果写回 API Server、触发 binding 和 volume binding 等异步操作,这些操作是并行执行的。

设计成两个周期,是刻意做的隔离:调度周期要快速、可预测,不能因为等待外部服务而被卡住;绑定周期可以慢一点,因为这时候结果已经确定,失败了可以重试。调度周期结束后如果没有任何 Node 能通过预选,或者打分阶段出错,Pod 会重新回到队列,等下一轮调度;如果绑定失败,也会有 retry 机制。理解这个设计,再看日志的时候就不会奇怪“为什么 Pod 已经选好了节点,状态还是 Pending”。

2.3 预选和优选的本质是“排除法加打分法”

预选阶段的本质是一系列 Filter 插件,逐个节点检查硬性条件。比如:

  • 节点 CPU 和内存是否满足 Pod 的 request;
  • 节点端口是否和 Pod 要使用的 hostPort 冲突;
  • Pod 的 nodeSelector 或 nodeAffinity 硬性要求是否匹配;
  • Pod 的污点容忍是否能匹配节点上的 Taints;
  • 节点上的磁盘压力、PID 压力是否已到阈值。

任何一个条件不满足,这个 Node 直接被排除,不会进入打分。这就好比相亲先筛“学历本科以上、年龄 25-35、本地户口”,条件不达标直接pass,不用再谈感觉。

通过预选后进入优选阶段,多个 Score 插件对节点打分。默认插件主要考虑:节点资源余量、资源均衡度、Pod 亲和性匹配度、拓扑分布均衡度、镜像本地存在情况等。每个插件打一个分数,权重加起来得到一个总分。Kubernetes 调度器在打分时不会只看“哪个节点配置高”,而是看“哪个节点在综合条件上最适合这个 Pod”。比如某台机器 CPU 很强,但上面已经有几十个 Pod 了,而另一台机器虽然配置一般但很空闲,后者打分会更高,因为资源余量更大、更均衡。

3. 最基础的调度策略:nodeName、nodeSelector

3.1 nodeName:绕开调度器的最强指定

nodeName 是最直接的调度方式,直接在 Pod spec 里写死要跑在哪台节点上。它的行为和其他调度策略有天壤之别:kube-scheduler 看到这种 Pod 时,根本不会为它跑调度算法,节点匹配直接跳过调度器。kubelet 会尝试在指定节点上创建 Pod。

我实际使用中很少在常规业务里用 nodeName,但它在两类场景里非常有效:

  • 测试某个节点时,临时把一个调试 Pod 直接钉在目标机器上;
  • 运维紧急迁移,比如要重启某个节点上的 kubelet 服务时,临时把探针 Pod 挂上去。

但 nodeName 的问题也明显:节点宕机或节点不存在,Pod 就永远 Pending,没有任何自愈机制。集群节点多了之后千万别把核心服务用 nodeName 写死,一旦节点维护,整个服务就跟着挂。

3.2 nodeSelector:标签匹配的简单开关

比 nodeName 合理一点的是 nodeSelector。它是 Pod spec 里的一个 map 字段,调度器会确保 Pod 只调度到包含指定标签的节点上。比如给三台节点打上 gpu=true 的标签,然后在 Pod 里设置 nodeSelector: gpu: "true",这样 Pod 只会调度到这三台节点。

nodeSelector 的实现简单,但它有一个明显短板:只能做“等于”匹配。你不能表达“gpu 不为 false”、不能表达“gpu 在 [true, optional] 集合里”、不能表达“优先有 gpu,没有也接受”。这些需求全都是实际生产中存在的。到了这个程度,就该用亲和性了。

nodeSelector 和 nodeName 也有个重要区别:nodeSelector 虽然也是硬性条件,但它走的是完整调度流程,节点过滤阶段会做匹配。也就是说,即便 nodeSelector 匹配到了节点,依然要经过资源过滤、打分,不是“指哪打哪”。

3.3 基础策略到底该用在哪

我的经验是:中小型项目把 nodeSelector 用好,比一上来就上亲和性有效得多。最常见的做法是给节点打环境标签和环境归属标签:

kubectl label node node1 env=prod kubectl label node node2 env=prod kubectl label node node3 env=test

然后各环境的工作负载通过 nodeSelector 绑定到对应节点池。这样做最大的价值是环境隔离:测试的 Pod 不会跑到生产节点上混用资源。很多线上故障都是因为有人忘了配 nodeSelector,导致测试服务跑到了生产环境机器上,把生产资源挤爆。基础策略虽然简单,但能挡住大多数低级事故。

4. 进阶策略:节点亲和性与 Pod 亲和性

4.1 nodeAffinity:把“必须”和“最好”分开

nodeSelector 的问题在于它只支持等于匹配,而 nodeAffinity 把它扩展成了完整的表达式匹配体系,还额外区分了硬性要求和软性偏好:

  • requiredDuringSchedulingIgnoredDuringExecution:硬性要求,调度时必须满足,不满足就不调度。
  • preferredDuringSchedulingIgnoredDuringExecution:软性偏好,调度时尽量满足,不满足也可以调度,只是打分偏低。

名字很长,拆开看就能理解:DuringScheduling 是说“调度期间怎么样”,DuringExecution 是说“运行期间怎么样”。IgnoredDuringExecution 意味着调度完就不再管了,即使后面节点标签变了也不会把 Pod 重新调度走。这也是 K8s 设计上刻意保留的简单性:调度是一次性决策,运行期变化交给其他机制处理。

一个典型的硬性亲和例子:

affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd

这个配置要求 Pod 必须调度到 disktype 为 ssd 的节点上。支持的操作符有 In、NotIn、Exists、DoesNotExist、Gt、Lt,表达能力比 nodeSelector 强一个量级。

软性亲和的例子:

affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - zone-a

这个配置的意思是:最好调度到 zone-a,如果 zone-a 资源不够,调度到别的可用区也完全可以。weight 可以设置多个偏好之间的相对权重,范围是 1-100。用好软性亲和,比硬性亲和更能在容灾和资源利用率之间找到平衡,我一般建议能用 preferred 的地方就不要用 required,因为 required 太容易把 Pod 卡死在 Pending。

4.2 podAffinity 与 podAntiAffinity:让 Pod “抱团”或“分开”

节点亲和性解决的是 Pod 和节点的关系,而 Pod 亲和性解决的是 Pod 和 Pod 的关系。这一层在微服务架构里非常实用。

podAffinity 让一个 Pod 倾向于跑到另一组 Pod 所在的节点或拓扑域中。典型的场景是:Web 应用和本地缓存 Redis 希望尽量在同一个节点上,减少网络开销。你怎么表达这个需求?你不能说“这个 Pod 要跑到 Redis Pod 那个节点上”,因为节点可能随时变化,你也不知道 Redis 被打到哪里。podAffinity 通过标签选择器匹配目标 Pod,目标 Pod 跑在哪,当前 Pod 就跟到哪,这才是正确的表达方式。

podAntiAffinity 则相反,让一个 Pod 尽量不跑到某组 Pod 所在的节点或拓扑域。典型场景是:同一个应用的多个副本必须分散到不同节点上,避免一台机器挂了,整个服务的所有副本全军覆没。如果没有这个约束,K8s 默认只做资源打分上的均衡,不保证故障域维度上的打散。

配置例子:

affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: web topologyKey: kubernetes.io/hostname

这段配置要求:任何带有 app=web 标签的 Pod,都不得和另一个同样带 app=web 标签的 Pod 调度到同一个节点。这正是高可用部署里最常见的写法。

4.3 topologyKey:拓扑域就是亲和性的“作用范围”

亲和性里有个绕不开的字段 topologyKey。它定义了“一组 Pod 放在同一个地方”里的“地方”到底是什么粒度。

  • 如果 topologyKey 是 kubernetes.io/hostname,那“同一个地方”就是同一台物理机;
  • 如果 topologyKey 是 topology.kubernetes.io/zone,那“同一个地方”就是同一个可用区;
  • 如果 topologyKey 是 topology.kubernetes.io/region,那就是同一个地域。

这个设计其实是在“打散”和“聚合”之间给了一个可控的粒度开关。比如反亲和性,如果你只在 hostname 粒度打散,那万一整个可用区挂了,服务一样全挂。所以容灾要求高的服务,会把反亲和性的 topologyKey 设为 zone,强制把副本分布到不同可用区。代价是可选节点变少,集群资源利用率下降,这就是高可用的成本。

需要注意一个容易踩的坑:podAntiAffinity 的 required 场景里,如果 topologyKey 是自定义的,K8s 对合法取值有限制(必须满足系统预置条件),否则调度器会直接拒绝。我建议生产环境优先使用系统自带的 hostname 和 zone 这两个 key,省心。

4.4 一个真实案例:Web 服务与缓存的部署策略

说个我实际调过的场景。公司有个订单服务,后面挂了 Redis 做会话缓存,Redis 用主从架构。业务侧希望订单服务的 Pod 和 Redis 主节点尽量同节点,减少跨机网络延迟;同时要求订单服务自身多副本必须打散到不同节点,Redis 主从也必须打散到不同节点。

方案拆开就是两条约束:

  • 订单服务 Pod 使用 podAffinity 绑定到带 role=redis-master 的 Pod,topologyKey 用 hostname,软性偏好即可;
  • 订单服务自身用 podAntiAffinity 打散,required + hostname;
  • Redis 主从之间用 podAntiAffinity 打散,required + hostname。

实际效果是:Redis 主节点分布在哪个节点,订单服务就会尽量跟过去;订单服务的所有副本无论如何不会挤在同一台机器上。这套组合在调度层面就把“就近访问”和“故障隔离”同时做掉了。如果只靠 nodeSelector,根本做不到这么细。

5. 污点与容忍:给节点划出一道隔离带

5.1 污点到底是什么,三个效果有什么区别

污点(Taint)是打在节点上的标记,表示“这个节点上有特殊的含义,默认情况下不接受普通 Pod 调度”。容忍(Toleration)是打在 Pod 上的标记,表示“我愿意接受这个污点,我可以调度到这类节点上”。

有三类污点效果:

  • NoSchedule:新 Pod 不能调度到该节点,已运行 Pod 不受影响,不会被驱逐;
  • PreferNoSchedule:调度器“尽量不”把 Pod 安排到这个节点,但不保证绝对不调度;
  • NoExecute:新 Pod 不能调度,且已经在节点上运行且没有相应容忍的 Pod 会被驱逐。

NoExecute 是最严厉的,也是最需要小心的。它不只是“不让来”,还会“赶走”。如果给节点打上 NoExecute 污点,上面所有没有对应容忍的 Pod 会陆续被清走。

5.2 命令行管理污点的实战操作

污点管理走 kubectl taint 命令:

# 给节点打上污点 kubectl taint nodes node1 key=value:NoSchedule # 去掉污点(命令后面带减号) kubectl taint nodes node1 key=value:NoSchedule- # 查看节点污点 kubectl describe node node1 | grep -i taint

打污点这个操作在集群维护里非常常态化。比如节点要重启做内核升级,我会先打一个 NoExecute 污点,把上面的工作负载平滑迁移走,等维护完成、确认 Pod 已经重新分布后,再把污点去掉。这个流程比直接 kubectl drain 更可控,适合不想把所有 Pod 一次性全部驱逐的场景。

5.3 容忍的配置细节和两个常见误解

容忍的写法:

tolerations: - key: "gpu" operator: "Equal" value: "true" effect: "NoSchedule"

也可以简写为:

tolerations: - key: "gpu" operator: "Exists" effect: "NoSchedule"

第一个坑:operator 为 Equal 时,value 必须匹配;operator 为 Exists 时,不用写 value,只要 key 相同即可。很多人把 Equal 写法里 value 漏了,结果容忍一直不生效。

第二个坑:容忍的匹配必须同时匹配 effect。如果节点污点是 key=value:NoSchedule,而 Pod 容忍写了 key=value:PreferNoSchedule,哪怕 key 和 value 都一样,也不匹配。我在线下环境就见过这种配置,Pod 死活调度不上,describe 一看,事件里明确写着“node(s) had taint {key=value:NoSchedule}, that the pod didn't tolerate”。

还有一个 NoExecute 特有的参数 tolerationSeconds,它定义“容忍这个污点多长时间”。比如:

tolerations: - key: "node.kubernetes.io/not-ready" operator: "Exists" effect: "NoExecute" tolerationSeconds: 60

表示节点进入 not-ready 状态后,Pod 可以继续留在上面 60 秒,超过后开始驱逐。这个机制对负载均衡的优雅摘除很有用。

5.4 污点最常见的几类业务用途

GPU 专用节点是经典用法。给 GPU 节点打上 nvidia.com/gpu=true:NoSchedule,普通 CPU 负载就不会挤上去;只有带 GPU 容忍和 GPU 资源声明的 Pod 才能调度过去。这样能防止一个写崩的服务把 GPU 节点资源吃掉。

独占节点也是常见场景。比如数据库所在的节点不希望被其他业务打扰,直接打 NoSchedule 污点,只允许数据库自己的 Pod 容忍。这和 taint 配合 nodeSelector 用,能实现“标签选中 + 污点拦截”的双保险:nodeSelector 决定谁能来,污点决定谁真正敢来。

节点维护期驱逐 Pod 也是我经常用到的。给节点打上 NoExecute 污点,上面无容忍的存量 Pod 会被迁走,迁走后节点上就没有业务负载,可以放心升级内核、重启。

6. 调度器的执行细节与扩展点

6.1 多调度器副本怎么保证不冲突

生产环境里 kube-scheduler 通常会部署多个副本,但同一时刻只有一个副本真正在干活。这是因为调度器实例之间通过 leader election 机制抢锁,只有拿到 leader 的那个实例才会执行核心调度循环,其他副本处于待命状态。

这解决了两个问题:一个是高可用,主调度器挂了备用顶上;另一个是避免两个调度器同时对同一个 Pod 做决策导致绑定冲突。我见过有人尝试通过部署多个调度器实例来提升调度吞吐,其实这是无效的,因为 leader 只有一个,吞吐不会有提升。要提升调度吞吐,得从性能参数和调度队列优化入手,而不是简单加副本。

6.2 Scheduler Framework:现在扩展调度的核心方式

Kubernetes 从 1.19 版本开始力推 Scheduler Framework,把调度的各个阶段抽象成插件扩展点。早期版本的调度器核心逻辑是堆在几个大函数里的,想改调度行为就得改源码重新编译。Framework 之后,你可以在不修改调度器源码的情况下,通过注册自定义插件来干预调度流程。

常用的扩展点有:

  • QueueSort:控制待调度 Pod 在队列里的排序逻辑;
  • PreFilter / Filter:预选阶段,做硬性过滤;
  • PreScore / Score:优选阶段,对节点打分;
  • Reserve / Permit / PreBind / Bind / PostBind:绑定周期各阶段钩子。

常见默认插件包括 NodeResourcesFit、NodeName、NodeAffinity、PodTopologySpread、InterPodAffinity、TaintToleration、VolumeBinding 等。这也就是为什么我们说“Kubernetes 集群调度”不是一套死板的算法,而是一个插件容器:默认插件给你一套合理的基线,自定义插件可以把公司特殊策略注入到调度链路上。

6.3 自定义调度器与 schedulerName

如果 Framework 还不够,K8s 允许你直接部署一个完全独立的调度器组件,然后通过 Pod 的 schedulerName 字段来指定用哪个调度器。

spec: schedulerName: my-scheduler

这么写之后,默认的 kube-scheduler 会无视这个 Pod,调度权交给名为 my-scheduler 的控制器。如果集群里根本没有这个名字的调度器,Pod 就永远 Pending。这个坑我遇到过:有人从网上拷贝了一段 yaml,里面写了 schedulerName,自己集群里又没有对应调度器,结果 Pod 一直 Pending,describe 里没有任何默认调度器的事件,排查了大半天才找到原因。

所以我强烈建议:不要轻易在业务 yaml 里写 schedulerName,除非你真的部署了一个自定义调度器。默认调度器已经能处理 99% 的需求。

7. Pod 一直 Pending?排查实录与避坑指南

7.1 先看 Events,再查调度器

Pod 调度不上时,第一步永远是:

kubectl describe pod <pod-name>

重点看 Events 段。调度器没找到节点的话,会写类似这样的事件:

Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 12s default-scheduler 0/4 nodes are available: 2 Insufficient cpu, 2 node(s) had taint {key=value:NoSchedule}, that the pod didn't tolerate.

这条 message 信息量很大:多少个节点不可用、分别因为什么原因不可用,全都列出来了。按这个列表挨个排查:cpu 不够就扩容或调低 request;污点不匹配就加容忍或去掉污点;nodeSelector 不匹配就调整标签。

还有一种情况是事件里什么 FailedScheduling 都没有。这时候优先怀疑调度器本身有问题:

kubectl get pods -n kube-system | grep scheduler kubectl logs -n kube-system <scheduler-pod> --tail=100

我就遇到过一次调度器因为 etcd 访问异常反复重启,导致所有新 Pod 都卡 Pending,节点状态全是 Ready,但任何 Pod 都调度不上去。这类问题不看调度器日志,看 Pod 描述永远找不到原因。

7.2 明明资源还有,却调不上去的那些“隐形坑”

资源够不够不能只看节点总容量,要看 allocatable,也就是 kubelet 给系统预留之后的可用量。调度器判断资源用的是 allocatable 减掉已分配 request,而不是总容量。节点上跑着系统组件、日志采集、监控 Agent,这些都要占资源。

还有两个特别隐蔽的点:

第一个是卷相关。Pod 用到的 PersistentVolume 如果限定在某个 zone,调度器会自动限定节点范围,即使你 nodeSelector 没有约束 zone,Pod 也只会调度到能访问该 PV 的节点。如果那个 zone 里没节点了,就会被卡住。

第二个是 hostPort 冲突。两个 Pod 要在同一节点绑同一个 hostPort,那是肯定绑不上的。但 hostPort 冲突不会体现在资源事件里,只会显示一个“node(s) had a conflict with hostPort”,而且不会告诉你和哪个 Pod 冲突。这时候就得逐个节点排查现有 Pod 的 hostPort。

7.3 调度结果不符合预期,怎么分析

有时候 Pod 跑起来了,但跑的位置不是你想要的位置。这种情况千万别一句“调度器有问题”就完事,按下面顺序查:

  • 先确认 Pod 用的调度器是不是默认的。看 spec.schedulerName;
  • 再看是否有 nodeSelector、nodeAffinity、Affinity 约束叠加。多个约束是 AND 的关系,少看一个都会导致你误判;
  • 检查反亲和性是否把副本打散到了极端位置。有时候你感觉“它应该在这边”,但反亲和性不允许它和另一个副本同节点,它就去了远处;
  • 看看软性亲和是不是被资源打分压过了。preferred 不是硬性的,资源空闲度打分可能权重更高,导致偏好没生效。这时候可以调大 weight,或者把某些节点打污点隔离出来。

我还要提一个少见但真实的问题:如果节点被打了 NoSchedule 污点,而 Pod 又没有对应容忍,它自然跑到别处。很多人改了半天亲和性,其实污点才是真正把它挡在外面的原因。所以看调度结果之前,先 describe node 看看有没有污点。

7.4 排查工具与日志速查表

我把常用命令整理成一张表,方便你直接抄:

场景命令
查看 Pod 调度事件kubectl describe pod
查看节点分配情况kubectl describe node
查看节点污点kubectl get node -o json | jq '.spec.taints'
查看调度器日志kubectl logs -n kube-system --tail=200
查看节点标签kubectl get nodes --show-labels
查看所有 Pending Podkubectl get pods -A | grep Pending
测试调度约束kubectl apply -f test-scheduler.yaml,配合 describe 验证

在诊断时我还会顺手用 jq 查节点资源水位:

kubectl get nodes -o json | jq -r '.items[] | [.metadata.name, .status.allocatable.cpu, .status.allocatable.memory, .status.allocatable.pods] | @tsv'

这套组合拳下来,百分之九十五的调度问题都能定位。剩下的百分之五,基本就是自定义调度器源码层面的问题,那得动 Framework 插件调试了。

8. 基于个人经验的调度配置建议

最后分享几个我实际操盘集群时反复验证过的结论。

第一,能用软性亲和就别用硬性亲和。required 类约束越多,Pod 被卡 Pending 的概率越大,集群资源碎片化也越严重。优先用 preferred + weight 引导,只有在容灾等强约束场景才用 required。

第二,规划标签比配置调度策略更优先。很多调度问题本质是标签没规划好。上生产之前就定好环境、可用区、节点类型、GPU 等标签规范,后面所有 nodeSelector、nodeAffinity、拓扑分布才能有依有据。标签命名统一,谁来了都不会写错。

第三,Pod 一定要写 requests。不写 request 的 Pod 在调度器眼里几乎没有资源占用,会被打满、堆叠到同一台机器,最后触发整机过载。写 request 不只是为了调度准确,更是自我保护。

第四,多测试少赌运气。每次调整调度策略后,建议先跑一个最小副本的测试 Pod 验证行为,确认符合预期再批量发布。我在测试环境里调反亲和性时,经常因为拓扑域理解不一致,把本来应该打散的副本全都堆到一个可用区,到了生产才发现,那就很被动了。

Kubernetes 集群调度这套体系,看着复杂,但只要你把过滤、打分、亲和性、污点容忍、topologyKey 这几个概念彻底想清楚,绝大多数场景都能顺手应对。后面如果遇到需要深度定制调度策略的业务,我再单独写一篇 Scheduler Framework 插件的实操笔记,那个才是真正的进阶玩法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询