☰
Kubernetes调度器深度解析:从Pod Pending到调度策略实战
2026/10/28 12:31:08 网站建设 项目流程

1. 调度器到底在忙什么:从一次Pod pending说起

很多人第一次意识到 Scheduler 的存在,不是因为读文档,而是因为一个 Pod 卡在 Pending 状态半天不动。kubectl describe pod一看,Events 里躺着0/5 nodes are available: 5 node(s) didn't match Pod's node affinity。这时候你才反应过来,原来集群里有个叫 kube-scheduler 的组件,它一直在后台默默决定每个 Pod 该落到哪台机器上。

Scheduler 调度策略总结这件事,说白了就是把“一个 Pod 从创建到绑定到某个 Node”这条链路上,所有能干预、能配置、能出问题的环节捋清楚。它解决的核心问题是:在资源有限、约束多样的集群里,如何让每个 Pod 找到一个“合适”的落脚点。合适这两个字很关键——不是随便找台有空闲的机器塞进去,而是要同时满足资源够用、亲和性匹配、污点容忍、拓扑分布合理等一堆条件。

这篇内容适合谁看?如果你已经能熟练跑kubectl get pods,知道 Deployment 和 Node 的关系,但一遇到调度相关的 Pending、Evicted、节点倾斜就抓瞎,那正好。我会从调度框架的默认行为讲起,把过滤和打分的逻辑拆开,再逐条过一遍亲和性、污点、拓扑分布这些策略的实际配置方式,最后落到排查技巧上。全程按我实际踩过的坑来讲,配置能直接抄,参数会告诉你为什么这么填。

需要先明确一个前提:不同版本的 Kubernetes 在调度框架上有差异,调度器在 1.19 之后逐步引入了 Scheduling Framework 的插件化机制,很多老的 Predicates/Priorities 概念被重构了。我下面讲的内容以较新的稳定版本行为为准,涉及版本差异的地方会单独点出来。你如果还在用很老的版本,部分插件名和配置字段可能对不上,这点心里要有数。

2. 调度框架的整体设计与思路拆解

2.1 为什么调度要拆成“过滤+打分”两段

理解 Scheduler 最省力的方式,是把它想成一个招聘流程。假设你是一个 HR,手里有一批候选人(待调度的 Pod),公司有几间办公室(Node)。你要做的第一件事是筛掉那些硬性条件不满足的——比如岗位要求会某门语言,不会的直接淘汰,这就是过滤(Filtering)。剩下的候选人再按匹配度排序,谁更合适谁优先,这就是打分(Scoring)。最后挑分数最高的那个发 offer,也就是绑定(Binding)。

这个两段式设计的好处很直接:过滤阶段只做布尔判断,快;打分阶段才做加权计算,贵。如果一上来就对所有节点做全量打分,集群规模一大,调度延迟会很难看。先过滤把候选集缩小,再在少量节点上打分,是性能上的必然选择。我见过有同学问“为什么不能直接按资源排序选最优”,答案就是:资源只是众多维度之一,而且硬约束(比如节点必须带某个标签)不满足的话,资源再多也不能用,必须先筛。

在 Scheduling Framework 里,这两个阶段被进一步拆成了多个扩展点。过滤对应Filter插件,打分对应Score插件,中间还有PreFilter做预处理、PostFilter在过滤失败后做补救(比如抢占)。这套插件化设计的意义在于:你可以通过配置启用或禁用某个插件,甚至写自己的调度插件,而不用改调度器源码。早期版本里这些逻辑是硬编码在pkg/scheduler/algorithm里的,改起来很痛苦,现在灵活多了。

2.2 默认调度器到底装了哪些插件

很多人以为调度策略就是自己配的那点亲和性规则,其实默认调度器本身就带了一堆插件在干活。你不配任何东西,它也在按一套默认逻辑做过滤和打分。搞清楚这些默认插件,才能明白“为什么我的 Pod 被调到了那台机器”。

过滤侧默认启用的插件里,比较关键的有几个。NodeUnschedulable负责把打了unschedulable标记的节点排除掉,也就是你kubectl cordon之后节点不再接收新 Pod,靠的就是它。NodeResourcesFit检查节点资源是否满足 Pod 的 requests,这是最常见的过滤失败来源。TaintToleration处理污点与容忍的匹配。NodeAffinity和InterPodAffinity分别处理节点亲和与 Pod 间亲和。还有PodTopologySpread处理拓扑分布约束。

打分侧默认插件里,NodeResourcesBalancedAllocation倾向于让 CPU 和内存的使用率更均衡,避免某台机器 CPU 打满但内存空着。NodeResourcesLeastAllocated倾向于选资源使用率最低的节点,也就是“摊大饼”式分散。InterPodAffinity在打分阶段也会参与,给满足亲和条件的节点加分。ImageLocality会给已经缓存了 Pod 所需镜像的节点加分,这个细节很多人不知道,但它实实在在影响调度结果——镜像大的 Pod 更容易被调到已经有镜像的节点上。

提示:这些默认插件的权重是可以调的。通过 KubeSchedulerConfiguration 里的plugins字段,你可以给某个打分插件设置weight,也可以禁用某个插件。调权重是优化调度倾斜的常用手段,但改之前一定要清楚默认值,别拍脑袋。

2.3 调度周期与绑定周期的边界

Scheduler 的工作其实分成两个周期:调度周期(Scheduling Cycle)和绑定周期(Binding Cycle)。调度周期是串行的,一次只处理一个 Pod,从队列里取出来、过滤、打分、选出节点。绑定周期是异步的,选好节点后把绑定操作丢给一个 goroutine 去执行,不阻塞下一个 Pod 的调度。

这个设计的意义在于吞吐量。如果绑定也串行,那每次写 API Server 的网络延迟都会拖慢整个调度队列。异步绑定让调度器可以快速处理下一个 Pod。但代价是:绑定可能失败。比如你选好了节点,结果在绑定前那一刻节点被删了,或者资源被别的 Pod 抢了,绑定就会报错,Pod 重新回到队列。这也是为什么偶尔会看到 Pod 短暂地被“调度过”又回到 Pending。

理解这个边界对排查问题很有用。如果你在日志里看到FailedScheduling,那是调度周期过滤失败;如果看到FailedBinding或者绑定相关的错误,那是绑定周期的问题,两者排查方向完全不同。前者看亲和性、资源、污点,后者看 API Server 连通性、节点状态变化。

3. 核心调度策略的细节解析与实操要点

3.1 资源请求与限制:调度只看 requests

这是最容易踩坑的一点,必须放在最前面讲。调度器在过滤阶段判断节点资源是否够用,看的是 Pod 的requests,不是limits,也不是节点当前的实际使用量。换句话说,调度器是个“账面主义者”,它只认你声明的资源请求,不关心节点上实际跑了多少负载。

举个例子,一个节点 8 核 16G,上面已经跑了几个 Pod,requests 加起来 6 核 12G,但实际 CPU 只用了 2 核。这时候来一个 requests 是 3 核的 Pod,调度器一算:6+3=9 > 8,超了,过滤失败,Pod Pending。哪怕节点实际还有大把空闲 CPU,调度器也不管。反过来,如果 requests 加起来没超,但实际负载已经把节点压垮了,调度器照样会把新 Pod 塞进去,因为它看不到实际使用率。

这个机制的直接后果是:requests 填得太小,节点会超卖,实际运行时互相抢资源;requests 填得太大,节点账面被占满,实际却很闲,浪费严重。我见过不少团队为了“让 Pod 能调度上去”把 requests 填得极小,结果节点上 Pod 密度过高,一到高峰期就集体卡顿。也见过 requests 按峰值填的,节点利用率长期在 30% 以下。

实操上,requests 的设定应该基于压测后的稳定运行值,而不是拍脑袋。CPU 的 requests 可以适当低于峰值(因为 CPU 是可压缩资源,超了会限流但不会 OOM),内存的 requests 必须接近实际使用峰值(内存不可压缩,超了直接 OOMKill)。这个区别很关键,配置的时候要分开对待。

resources: requests: cpu: "500m" # 按稳定运行值填,可略低于峰值 memory: "512Mi" # 按实际峰值填,宁大勿小 limits: cpu: "1000m" # 允许突发到 1 核 memory: "512Mi" # 与 requests 持平,避免 OOM 风险

注意:内存的 limits 如果设得比 requests 大很多,Pod 在突发时会用到超过 requests 的内存,但调度器只按 requests 算账。多个这样的 Pod 挤在一台节点上,实际内存可能远超节点容量,触发节点级 OOM。所以内存的 requests 和 limits 建议持平,或者 limits 只略高一点。

3.2 节点亲和性:硬约束与软偏好的分界线

节点亲和性(Node Affinity)分两种:requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution。名字很长,但拆开看就清楚了。required是硬约束,调度时必须满足,不满足直接过滤掉;preferred是软偏好,满足就加分,不满足也能调度,只是分数低。

后半截IgnoredDuringExecution的意思是:这个规则只在调度那一刻生效,Pod 跑起来之后节点标签变了,也不会把 Pod 赶走。这一点经常被误解,有人以为改了节点标签 Pod 会自动迁移,其实不会。要让运行中的 Pod 响应标签变化,得靠别的机制。

硬约束的典型场景是“这个 Pod 必须跑在带 GPU 的节点上”。配置大概长这样:

affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: accelerator operator: In values: - gpu

软偏好的典型场景是“尽量跑在 SSD 节点上,没有就算了”。配置时每个偏好项带一个weight(1 到 100),权重越高越优先:

affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: disktype operator: In values: - ssd

这里有个实操细节:matchExpressions支持In、NotIn、Exists、DoesNotExist、Gt、Lt几种操作符。Gt和Lt只能用于整数类型的标签值,而且比较的是字符串转整数后的结果。我见过有人用Gt比较版本号字符串,结果行为不符合预期,因为它是按整数解析的,"1.10"这种带小数点的会解析失败。

还有一个容易忽略的点:nodeSelector和nodeAffinity是可以共存的,但nodeSelector本质上是硬约束的简化写法,最终会被转换成requiredDuringScheduling的匹配条件。两者同时存在时,条件是与的关系,都得满足。排查时如果发现 Pod 调不上去,别忘了检查nodeSelector这个“隐形”约束。

3.3 污点与容忍:节点主动拒绝的艺术

亲和性是 Pod 挑节点,污点(Taint)是节点拒绝 Pod。方向反过来,用途也不同。污点适合表达“这台节点有特殊情况,普通 Pod 别来”,比如专用节点、正在维护的节点、有特殊硬件的节点。

污点的格式是key=value:effect,effect 有三种:NoSchedule、PreferNoSchedule、NoExecute。NoSchedule是硬拒绝,没有对应容忍的 Pod 不会被调度上来,但已经在上面跑的 Pod 不受影响。PreferNoSchedule是软拒绝,尽量不调度,但没别的地方去还是可以来。NoExecute最狠,不仅新 Pod 不来,已经在上面跑的、没有容忍的 Pod 会被驱逐。

# 给节点打污点 kubectl taint nodes node-1 dedicated=gpu:NoSchedule # Pod 里配置容忍
tolerations: - key: "dedicated" operator: "Equal" value: "gpu" effect: "NoSchedule"

operator可以是Equal(值必须匹配)或Exists(只要 key 存在就容忍,不管值)。用Exists时可以省略value,表达“只要这个 key 的污点我都忍”。还有一个特殊写法:operator: Exists且不写key,表示容忍所有污点,这基本等于“我哪都能去”,慎用。

NoExecute的容忍可以带tolerationSeconds,表示“被驱逐前还能赖多久”。这个在节点维护场景很有用:给 Pod 配一个 300 秒的容忍,节点打上NoExecute后,Pod 还能跑 5 分钟做优雅退出,而不是立刻被赶走。

注意:Master 节点默认带有node-role.kubernetes.io/control-plane:NoSchedule污点(老版本是master)。如果你想让普通 Pod 跑上去,要么加容忍,要么去掉污点。生产环境不建议去掉,控制面节点混跑业务 Pod 会影响稳定性。

3.4 Pod 间亲和与反亲和:让 Pod 抱团或分散

节点亲和性管的是 Pod 和节点的关系,Pod 间亲和性(Inter-Pod Affinity)管的是 Pod 和 Pod 的关系。典型场景:缓存服务和业务服务尽量放一起减少网络延迟(亲和),同一服务的多个副本尽量分散到不同节点提高可用性(反亲和)。

配置里有个关键字段topologyKey,它定义了“在一起”或“分开”的粒度。topologyKey: kubernetes.io/hostname表示以节点为单位,topologyKey: topology.kubernetes.io/zone表示以可用区为单位。这个字段选错了,效果完全不对。

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

上面这段的意思是:这个 Pod 不能和任何app=web的 Pod 跑在同一台节点上。如果集群只有 3 台节点,而app=web已经有 3 个副本,那第 4 个副本就会 Pending,因为硬反亲和找不到满足条件的节点。这是硬反亲和最常见的坑——副本数超过拓扑域数量时,多出来的副本无处可去。

所以生产环境里,反亲和通常用软约束preferredDuringSchedulingIgnoredDuringExecution,允许在节点不够时“挤一挤”,而不是直接 Pending。硬反亲和只用在“绝对不能共存”的场景,比如两个实例抢同一个本地端口。

Pod 间亲和还有一个命名空间的概念。默认只在 Pod 所在命名空间内匹配,如果要跨命名空间匹配,需要配namespaces字段,而且从某个版本开始还要求目标命名空间有对应的NamespaceSelector标签。这个细节在跨命名空间部署时经常被漏掉,导致亲和规则“看起来配了但不生效”。

3.5 拓扑分布约束:比反亲和更精细的分散

Pod 拓扑分布约束(PodTopologySpread)是后来引入的机制,可以看作反亲和的升级版。反亲和只能表达“不要在同一节点”,拓扑分布能表达“每个节点最多差 1 个副本”这种更精细的均衡。

核心参数有四个:maxSkew允许的最大不均衡度,topologyKey拓扑域,whenUnsatisfiable不满足时的行为(DoNotSchedule硬约束或ScheduleAnyway软约束),labelSelector匹配哪些 Pod 参与计算。

topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: web

这段配置的意思是:app=web的 Pod 在各节点上的数量差不能超过 1,不满足就不调度。假设 3 个节点,已有分布是 2、1、0,新 Pod 只能去数量为 0 的节点,因为去 1 的节点会变成 2、2、0,skew 是 2 超过 maxSkew 1;去 0 的节点变成 2、1、1,skew 是 1,满足。

maxSkew的理解要小心:它算的是“任意两个拓扑域之间数量的最大差值”,不是“和平均值的差”。我见过有人把maxSkew设成 0,以为要求完全平均,实际上maxSkew: 0在数量不能整除时会永远无法满足,导致 Pod 全部 Pending。一般设 1 就够了。

whenUnsatisfiable: DoNotSchedule是硬约束,和硬反亲和一样有“副本数超过拓扑域”导致 Pending 的风险。ScheduleAnyway是软约束,调度器会尽量均衡但不强求,生产环境更常用这个。

3.6 优先级与抢占:让重要的 Pod 先跑

优先级(PriorityClass)解决的是“资源不够时谁先上”的问题。高优先级的 Pod 调度不上时,调度器会尝试驱逐低优先级的 Pod 来腾位置,这叫抢占(Preemption)。

apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false preemptionPolicy: PreemptLowerPriority

value越大优先级越高,系统保留了一批内置的高值给关键组件,自己定义时别超过 10 亿这个量级。preemptionPolicy设为PreemptLowerPriority才允许抢占,设为Never则只排序不抢占。

抢占的机制值得多说两句。当高优先级 Pod 调度失败时,调度器会模拟“如果驱逐某些低优先级 Pod,能不能腾出足够资源”。如果能,就在被驱逐的 Pod 上打上nominatedNodeName标记,然后异步驱逐它们。注意是“模拟”和“异步”,所以抢占不是瞬时的,中间可能有延迟,而且被选中的“牺牲者”不一定是最终被驱逐的——如果在这期间有别的节点空出来了,高优先级 Pod 可能改去别处。

注意:抢占会驱逐正在运行的 Pod,这对有状态服务或有长连接的服务影响很大。给关键业务配高优先级之前,一定要评估被抢占方的容忍度。另外,抢占不考虑 PodDisruptionBudget,被驱逐的 Pod 可能一次性全没,这点和自愿驱逐不同。

4. 实操过程与核心环节实现

4.1 从零搭一个带调度约束的部署

光看配置不过瘾,走一遍完整流程。假设有个 Web 服务,要求:跑在带env=prod标签的节点上,尽量分散到不同节点,资源 requests 是 500m CPU / 512Mi 内存,优先级中等。

先给节点打标签:

kubectl label nodes node-1 node-2 node-3 env=prod

然后写 Deployment:

apiVersion: apps/v1 kind: Deployment metadata: name: web-demo spec: replicas: 3 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: priorityClassName: medium-priority affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: env operator: In values: - prod topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app: web-demo containers: - name: app image: nginx:stable resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "1000m" memory: "512Mi"

应用之后,用kubectl get pods -o wide看分布。3 个副本在 3 台节点上各一个,说明拓扑分布生效了。如果某台节点资源不够,ScheduleAnyway会让它挤到别的节点,而不是 Pending。

这里有个实操顺序问题:先打标签再创建 Deployment。如果顺序反了,Pod 创建时节点还没标签,硬亲和过滤失败,Pod 会 Pending,等你打完标签,调度器会重新尝试(因为节点标签变化会触发重新调度),但中间会有延迟。养成先准备节点再部署的习惯,能省掉很多“为什么 Pending 了”的困惑。

4.2 用 describe 和 events 定位调度失败原因

Pod Pending 时,第一手信息永远在kubectl describe pod的 Events 里。调度失败的信息格式很固定:0/N nodes are available: <原因汇总>。常见的原因和对应策略我整理成表:

Events 中的提示对应策略排查方向
Insufficient cpu/Insufficient memoryNodeResourcesFit节点 requests 余量不足,看kubectl describe node的 Allocated resources
node(s) didn't match Pod's node affinityNodeAffinity节点标签是否匹配,nodeSelector是否冲突
node(s) had untolerated taintTaintToleration节点污点与 Pod 容忍是否匹配
node(s) didn't match pod affinity rulesInterPodAffinity亲和的目标 Pod 是否存在,topologyKey 是否正确
node(s) didn't match pod anti-affinity rulesInterPodAntiAffinity反亲和导致无处可去,考虑改软约束
node(s) didn't satisfy topology spread constraintsPodTopologySpreadmaxSkew 是否过严,拓扑域是否够多
node(s) were unschedulableNodeUnschedulable节点是否被 cordon

kubectl describe node里的Allocated resources段落特别有用,它列出了每个节点上 requests 的汇总和百分比。我排查资源类 Pending 时,第一步就是看这个,比逐个 Pod 加 requests 快得多。

kubectl describe node node-1 | grep -A 10 "Allocated resources"

输出里会显示 CPU Requests、CPU Limits、Memory Requests、Memory Limits 的已分配量和百分比。如果 Requests 已经 95% 以上,那基本就是资源不够了,要么加节点,要么调小 requests,要么清理无用 Pod。

4.3 调度器配置的调整与生效

默认调度器行为不满足需求时,可以通过 KubeSchedulerConfiguration 调整。这个配置通过 ConfigMap 挂载给调度器,或者用--config参数指定。一个典型的配置长这样:

apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: default-scheduler plugins: score: disabled: - name: NodeResourcesBalancedAllocation enabled: - name: NodeResourcesLeastAllocated weight: 2 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: LeastAllocated

这段配置禁用了均衡分配插件,把最少分配插件的权重调到 2,让调度更倾向于把 Pod 摊到空闲节点上。NodeResourcesFit的scoringStrategy可以选LeastAllocated(最少分配优先)或MostAllocated(最多分配优先,适合装箱提高利用率)。

改完配置后,调度器需要重启才能生效。如果是 kubeadm 部署的集群,改/etc/kubernetes/manifests/kube-scheduler.yaml里的挂载配置,然后 kubelet 会自动重建静态 Pod。改之前务必备份,配置写错会导致调度器起不来,整个集群新 Pod 全部无法调度。

注意:多调度器场景下,Pod 通过schedulerName字段指定用哪个调度器。默认是default-scheduler,如果你部署了自定义调度器,记得在 Pod spec 里显式指定,否则还是走默认的。这个字段写错的话,Pod 会一直 Pending 且 Events 里没有任何调度失败信息,因为根本没有调度器认领它。

4.4 一个真实的调度倾斜修复过程

说个我实际处理过的场景。某集群 6 台节点,跑一个 12 副本的服务,结果发现 4 台节点各跑了 3 个,另外 2 台一个都没有。查下来是那 2 台节点有NoSchedule污点,但服务没配容忍,所以全挤在剩下 4 台上。

修复分两步。第一步,确认那 2 台节点的污点是否还有必要。查下来是早期做专用节点时打的,后来业务调整已经不需要了,直接去掉:

kubectl taint nodes node-5 dedicated=old:NoSchedule-

注意末尾的减号,表示删除这个污点。第二步,给服务加上拓扑分布约束,防止以后再出现倾斜:

topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app: my-service

改完之后滚动更新,新 Pod 按分布约束重新铺开,最终 6 台节点各 2 个。这个过程里有个细节:滚动更新时旧 Pod 还在,新 Pod 创建时拓扑分布会把旧 Pod 也算进去(因为 labelSelector 匹配的是同一批 Pod),所以分布计算是准确的,不会出现新旧叠加导致倾斜。

5. 常见问题与排查技巧实录

5.1 调度相关高频问题速查

把我在实际运维中反复遇到的问题整理成表,遇到类似现象可以直接对照:

现象可能原因快速验证解决方向
Pod 一直 Pending,Events 无调度信息schedulerName 写错或无调度器kubectl get pod -o yaml看 schedulerName改回 default-scheduler
大量 Pod Pending,提示资源不足节点 requests 占满describe node看 Allocated扩容或调小 requests
单节点 Pod 密度异常高无拓扑约束或反亲和get pods -o wide看分布加 topologySpreadConstraints
滚动更新卡住,新 Pod Pending硬反亲和导致无处可去看 Events 反亲和提示改软约束或调 maxSurge
Pod 被驱逐后不重建节点 NotReady 或 NoExecutedescribe node看状态和污点恢复节点或加容忍
高优先级 Pod 抢占后仍 Pending抢占目标资源不够或 PDB 阻挡看 Events 抢占记录检查牺牲者资源量

5.2 硬约束导致 Pending 的连锁反应

硬约束(硬亲和、硬反亲和、DoNotSchedule的拓扑分布)是 Pending 的重灾区。它们的共同特点是:条件不满足时 Pod 直接卡住,不会退而求其次。设计调度策略时,要时刻问自己一个问题:当集群规模变化、节点故障、副本扩缩时,这个硬约束还成立吗?

举个典型例子。一个服务配了硬反亲和,要求副本不在同一节点,副本数 5,集群 5 台节点。平时正好一台一个。某天一台节点故障下线,剩 4 台,第 5 个副本立刻 Pending,因为硬反亲和找不到第 5 台节点。如果这时候正好在做滚动更新,新副本起不来,更新就卡住了。

这种场景的正确做法是用软反亲和或拓扑分布软约束,允许在节点不足时“挤一挤”,保证服务可用性优先。硬约束只留给真正不能妥协的场景,比如“两个实例不能抢同一个 hostPort”。

5.3 资源碎片与装箱策略的取舍

集群跑久了,经常出现“总资源够但单个 Pod 调度不上”的情况,这叫资源碎片。比如节点 A 剩 2 核 4G,节点 B 剩 1 核 8G,来了个需要 3 核 6G 的 Pod,两台都放不下,但加起来资源是够的。

这是分散调度(LeastAllocated)的副作用——它倾向于把 Pod 摊开,导致每台节点都剩一点零碎资源。解决办法是切换到装箱策略(MostAllocated),让 Pod 尽量往已经比较满的节点上堆,把空闲节点整块留出来给大 Pod。

pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: MostAllocated

但装箱策略也有代价:节点利用率高了,故障时影响面大,而且容易触发资源争抢。我的经验是,在线服务用分散策略保稳定,离线批处理用装箱策略提利用率,通过多调度器 profile 区分不同工作负载。KubeSchedulerConfiguration 支持配多个 profile,每个 profile 有自己的插件配置,Pod 通过 schedulerName 选择。

5.4 调度器日志的查看姿势

调度器日志默认输出到标准错误,kubeadm 部署的话在/var/log/下或者用kubectl logs看静态 Pod。日志里搜FailedScheduling能看到所有调度失败的记录,搜Attempting to schedule能看到调度尝试。

日志级别默认是 2,调到 4 或 5 能看到更详细的过滤和打分过程,包括每个节点在每个插件上的得分。排查“为什么调到了这台而不是那台”时,把日志级别调高,能看到打分明细。但注意高日志级别会产生大量输出,排查完记得调回去,否则日志会撑爆磁盘。

kubectl logs -n kube-system kube-scheduler-node-1 --tail=200 | grep -i "score\|filter"

5.5 几个容易忽略的细节

第一个是Pod 的nodeName字段。如果你在 Pod spec 里直接写了nodeName,调度器会完全跳过这个 Pod,直接由 kubelet 接管。这相当于手动指定节点,绕过了所有调度策略。有人用它来“强制”调度,但代价是失去了调度器的所有保护,节点资源不够也照塞,容易出问题。

第二个是schedulerName与多调度器。集群里如果装了多个调度器,每个 Pod 只会被schedulerName指定的那个调度器处理。默认调度器只处理schedulerName为空或为default-scheduler的 Pod。自定义调度器要处理 Pod,得让 Pod 显式指定名字,否则两边都不管,Pod 永远 Pending。

第三个是节点资源预留。kubelet 有system-reserved和kube-reserved配置,会从节点可分配资源里扣掉一部分给系统进程。所以describe node里的Allocatable往往小于Capacity。算资源余量时要用Allocatable减去已分配 requests,用Capacity算会高估。

第四个是DaemonSet 的调度特殊性。DaemonSet 创建的 Pod 会自带一些容忍,能容忍节点的一些标准污点(如node.kubernetes.io/not-ready),而且调度器对 DaemonSet Pod 有特殊处理,会尽量保证每个节点一个。但 DaemonSet 也受资源约束,节点资源不够时它的 Pod 一样会 Pending,只是优先级处理上有些差异。

6. 调度策略的组合与演进思路

6.1 策略不是越多越好,而是越清晰越好

我见过一些团队的 Pod spec,亲和性、反亲和、拓扑分布、污点容忍全配上了,层层叠叠几十行。问为什么这么配,答不上来,说是“之前出过问题就加了一条”。这种堆叠式配置的问题在于:一旦出问题,根本不知道是哪条规则导致的,排查成本极高。

我的建议是,调度策略要分层设计。第一层是硬约束,只放真正不能妥协的,比如必须跑在特定硬件节点上。第二层是软偏好,表达倾向但不强制,比如尽量分散、尽量靠近缓存。第三层是兜底机制,比如优先级和抢占,保证关键服务在资源紧张时能挤进来。每一层都要能说清楚“为什么需要它”,说不清楚的就删掉。

配置写完要做一次“反向测试”:故意制造节点不足、标签缺失、副本超限的场景,看 Pod 的行为是否符合预期。调度策略的坑往往在异常场景才暴露,正常场景下怎么配都能跑。

6.2 从单集群到多集群的调度延伸

单集群内的调度策略玩熟之后,会遇到跨集群调度的问题。比如两个集群,一个资源紧张一个空闲,怎么让 Pod 跑到空闲的那个去。这超出了单集群 Scheduler 的范畴,需要上层的多集群调度方案,通过联邦或者自定义控制器把 Pod 分发到不同集群。

但底层的调度逻辑是相通的:过滤、打分、绑定这套思路在跨集群场景依然适用,只是“节点”变成了“集群”,“资源”变成了“集群剩余容量”。理解了单集群的调度原理,往上扩展到多集群时,只是换了一层抽象,核心的判断逻辑没变。

6.3 调度策略与集群容量的联动

最后说个容易被忽视的联动关系:调度策略直接影响集群的“有效容量”。同样一批节点,用分散策略可能只能跑 70% 的负载就出现碎片,用装箱策略能跑到 90% 但故障影响面大。所以容量规划不能只看节点总数,要结合调度策略算有效容量。

我的做法是,在容量评估时跑一遍模拟:用当前调度策略,按典型 Pod 规格,算出每个节点能容纳的 Pod 数量,再乘以节点数,得到理论容量,然后打个折(考虑碎片和预留)作为实际可用容量。这个数字比单纯看 CPU 内存总量靠谱得多,因为它把调度约束的影响算进去了。

调度这块内容,配置本身不难,难的是理解每条规则背后的取舍,以及在异常场景下的行为。我踩过的坑基本都集中在“硬约束太硬导致 Pending”和“requests 填得不合理导致超卖或浪费”这两类。把这两类问题想清楚,大部分调度问题都能自己排查。后续如果集群规模上去了,再考虑调插件权重、上多调度器 profile,那是另一个层次的优化了。

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

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

立即咨询