Kubernetes v1.37 调度器如何给节点打分:默认插件、权重与选点
两个节点都能运行同一个 Pod,A 的 CPU 请求余量更大,B 已缓存镜像,并且这个 Pod 更偏好 B 的节点标签。调度器如何在这些条件之间取舍?默认调度器会让每个适用的插件分别打分,再把分数乘上插件权重求和。CPU 余量只是其中一项;即使配置了软亲和,也可能被其他插件的分差抵消。
下文固定到 Kubernetes v1.37.2(提交 c50e5395f1ae2d4a2c05f28fdcc38c0916cc1a32),分析默认的 default-scheduler profile、v1.37 默认特性门控,以及没有外部 extender 的单 Pod 路径。完整的调度与绑定周期见原生调度器。
一轮打分的输入与输出
调度入口先从节点快照里找可行节点。资源请求超出可分配量、节点标签不符合硬约束、污点不被容忍等条件先由过滤插件处理。没有候选节点时进入失败路径;只有一个候选时直接选中,跳过完整的 PreScore 和 Score。有多个候选才执行 prioritizeNodes。
候选集合也不一定是集群中所有可行节点。候选节点数量由 percentageOfNodesToScore 控制,profile 设置优先于全局设置。默认值 0 表示自适应:节点数少于 100 时检查全部;更大时以 50 - 节点数 / 125 估算百分比,下限 5%,目标至少找到 100 个可行节点。过滤可在收集到足够候选后停止。后续最高分只在这批候选中比较。
框架按以下顺序计算分数:
PreScore针对当前 Pod 准备共享数据,例如解析软亲和规则或统计某个 zone 里的匹配 Pod。插件返回Skip时,同名的Score在本轮被跳过。Score并行遍历候选节点,每次调用接收一个节点的NodeInfo。框架按插件收集所有候选节点的原始分。- 实现了
NormalizeScore的插件调整自己的整组分数。框架检查归一化后或无需归一化的分数是否在 0–100,然后分别乘权重,再按节点求和。插件错误或分数越界会使本轮计算失败。 - 调度器从按总分排列的最大堆取出一个节点。堆有
Randomizer作为次级比较项,但 v1.37.2 的普通打分路径只在合并 extender 分数时为对应节点填入随机值。没有 extender 时,默认路径没有按节点名排序,也没有承诺在同分节点间均匀随机。
令 $C$ 为本轮候选节点集合,$P$ 为当前 profile 启用且本轮没有跳过的打分插件。对候选节点 $n$,$s_p(n)$ 是插件 $p$ 最终交给框架的 0–100 分,$W_p$ 是该插件的 profile 权重。没有 extender 时,整体公式是:
$$ \begin{aligned} T(n)&=\sum_{p\in P} W_p\,s_p(n),\\ n^*&\in\operatorname*{arg\,max}_{n\in C} T(n). \end{aligned} $$框架直接求和,不除以权重之和,也不再对总分做归一化。把跳过的项按贡献 0 记账,代入默认 profile 的九个权重,就得到:
$$ \begin{aligned} T(n)={}&3s_{\mathrm{Taint}}+2s_{\mathrm{NodeAffinity}}\\ &+s_{\mathrm{Fit}}+s_{\mathrm{Volume}}\\ &+2s_{\mathrm{Spread}}+2s_{\mathrm{InterPodAffinity}}\\ &+2s_{\mathrm{DRA}}+s_{\mathrm{Balanced}}+s_{\mathrm{Image}}. \end{aligned} $$如配置了参与打分的 extender,还要为该节点加上各 extender 的 返回分 × extender 权重 × (100 / 10);10 是 extender API 的分值上限。下文算例不使用 extender。
因此这里有三种不同的权重:Pod 软亲和条目的权重是插件输入;NodeResourcesFit 的资源权重用于合并 CPU、内存等分数;profile 的插件权重才用于合并各插件的分数。把软亲和条目的权重从 50 改成 100,如果该插件只有这一个条目,且候选中有匹配节点,归一化之后匹配节点仍是 100 分。
另一个容易漏掉的分支是默认启用的 OpportunisticBatching。条件相符的连续 Pod 可以利用上轮候选排序:批处理逻辑检查缓存是否适用并刷新上轮选中节点的状态,给当前轮一个节点提示;提示通过相关过滤后,可走单候选路径。这是受条件限制的快路径,并非每个 Pod 都会重新打印所有节点的分项分数。
默认打分插件看什么
默认配置把插件声明在 MultiPoint 下。框架按插件实现把它挂到相应扩展点;只有实现 Score 的插件才参与下面的打分。没有显式设置插件权重的 VolumeBinding 使用 默认权重 1。表中的权重适用于默认 profile。下面各节的 $r(n)$ 表示插件原始分,$s(n)$ 表示乘插件权重之前的分数。
| 插件 | 权重 | 依据 |
|---|---|---|
TaintToleration | 3 | 不被容忍的 PreferNoSchedule 污点数 |
NodeAffinity | 2 | 节点标签匹配的软亲和条目 |
NodeResourcesFit | 1 | 放入 Pod 后的请求余量,默认 CPU、内存 |
VolumeBinding | 1 | 可绑定或可供给存储的容量 |
PodTopologySpread | 2 | hostname、zone 等域内的匹配 Pod 数 |
InterPodAffinity | 2 | 拓扑域内其他 Pod 的标签及亲和规则 |
DynamicResources | 2 | DRA claim 的设备子请求优先顺序 |
NodeResourcesBalancedAllocation | 1 | 放入 Pod 前后资源比例的均衡变化 |
ImageLocality | 1 | 候选节点已缓存镜像的大小及其分布 |
TaintToleration:未容忍的软污点越多,分数越低
节点上的 PreferNoSchedule 污点表示「尽量别把不容忍这个污点的 Pod 放过来」。插件把节点的软污点和 Pod 的 tolerations 逐一比对。每多一个未容忍的污点,节点的原始计数就加一,最后反过来给分:计数越少,分数越高。容忍一个污点只是让它不计入;所有候选都没有未容忍的软污点时,这个插件不会改变排名。
把未容忍的污点数记作 $r(n)$,候选中的最大计数记作 $M$。$M=0$ 时所有节点得 100 分;否则反向归一化:
$$ s(n)=100-\left\lfloor 100r(n)/M \right\rfloor. $$这里先做整数除法,再用 100 减去结果。比如未容忍数为 0、1、3 时,分别得到 100、67、0 分,乘权重 3 后是 300、201、0。NoSchedule、NoExecute 的硬性检查在 Filter 阶段完成。见 TaintToleration 源码。
NodeAffinity:匹配节点标签的软亲和条目加分
一个 Pod 可以偏好有 SSD 的节点,同时对某个可用区也有偏好。NodeAffinity 用节点标签逐条检查这些偏好,匹配一条就加上这条的权重。这样,一条更重要的偏好可以比几条次要偏好加起来还值钱。所有偏好都是软条件:没匹配的节点仍能参选,只是少拿分。换算到 0–100 分时,匹配权重之和最高的候选拿满分,其他候选按比例给分。
PreScore 解析 Pod 的 preferredDuringSchedulingIgnoredDuringExecution;profile 的 addedAffinity 如果也配置了软偏好,Score 会一起累计。把两组命中条目的权重之和记作 $r(n)$,候选中的最大值记作 $M$:
例如 A 匹配权重为 80 的条目,B 只匹配权重为 30 的条目,得到 100 和 37 分;乘插件权重 2 后是 200 和 74。没有可用的软亲和条目时,PreScore 会跳过该插件。nodeSelector 和 required node affinity 仍由 Filter 判断。见 NodeAffinity 源码。
NodeResourcesFit:按放入 Pod 后的请求余量打分
先假设把新 Pod 放上去,再看节点的 CPU 和内存各剩多少可分配份额。默认的 LeastAllocated 偏好余量比例更高的节点,避免在其他条件相近时继续把请求压到更满的节点上。它比较的是请求占预算的比例,所以不能只看节点还剩几个 CPU 核,也不能从监控上的实时使用率推断这项分数。默认 CPU 和内存权重相同。
对每种资源 $i$,$C_i$ 是节点的 allocatable,$R_i$ 是节点已有请求与待调度 Pod 请求之和,$w_i$ 是配置中的资源权重。先算每种资源的余量分 $q_i$,再取加权平均:
只对纳入评分且 $C_i>0$ 的资源求和;没有可计入的资源时得 0。两层除法分别取整。比如放入 Pod 后 CPU 还剩 50%、内存还剩 25%,插件分是 37,乘默认插件权重 1 后仍是 37。CPU 和内存的打分路径会为缺少请求的容器使用非零估算,常量是 100m 和 200 MiB;这个估算不会修改 Pod,也不代表 Filter 使用同一个数。见 LeastAllocated 源码。
改用 MostAllocated 时,单项变为 $\lfloor 100\min(R_i,C_i)/C_i\rfloor$,仍按资源权重取整平均。RequestedToCapacityRatio 则先算整数利用率,在配置的分段线性曲线上取分,再对正分项做加权平均并四舍五入。这两种策略均需要配置,不属于默认打分。
NodeResourcesBalancedAllocation:奖励放入 Pod 后的均衡改善
有的节点已经用了大半 CPU 请求预算,内存请求却占得很少。一个相对更吃内存的 Pod 放上去,可能正好让 CPU 和内存的请求比例更接近;一个相对更吃 CPU 的 Pod 则可能拉大差距。这个插件比较的是同一节点放入前后的变化:差距缩小就加分,差距拉大就减分。原本比较空闲、或者原本很均衡的节点,并不会因此自动拿高分。
这个插件用实际请求,不使用 NodeResourcesFit 的非零估算。对每种可分配量大于 0 的资源,计算请求比例 $f_i=\min(R_i/C_i,1)$。令 $\bar f$ 为这些比例的均值,$\sigma$ 为标准差。先计算放入前的均衡度 $B_0$ 和放入后的均衡度 $B_1$,再用差值打分:
CPU 和内存两项的标准差就是两比例差的绝对值除以 2;零项或一项时,代码中的标准差为 0。均衡度不变时得 75 分;改善会加分,恶化会减分。待调度 Pod 对参与评分的资源均无请求时,PreScore 跳过。见 BalancedAllocation 源码。
假设 A、B 都可分配 8 CPU、8 GiB;已有请求分别是 A 的 4 CPU、2 GiB,B 的 2 CPU、2 GiB;新 Pod 请求 2 CPU、4 GiB。只比较这两个插件:
| 节点 | 放入后的请求 | 余量分 | 均衡分 | 合计 |
|---|---|---|---|---|
| A | 6 CPU、6 GiB | 25 | 81(87 → 100) | 106 |
| B | 4 CPU、6 GiB | 37 | 68(100 → 87) | 105 |
B 的余量更大,A 却以 1 分领先:这个 Pod 让 A 的 CPU 和内存请求比例更接近。两节点都必须先通过资源过滤。
InterPodAffinity:把 Pod 的亲和关系换算成拓扑域分数
如果新 Pod 希望和一组已有 Pod 在同一个可用区,插件先用亲和规则找出匹配的已有 Pod,再看它们所在节点的可用区标签。候选节点只要位于同一区,就能拿到这部分加分,即使这些已有 Pod 不在候选节点上。软反亲和做相反的事:匹配的已有 Pod 越多,所在域被扣的分越多。插件把这些关系累加起来,再按本轮候选的最高和最低原始分换算;候选之间没有差别时,这项不会改变排名。
PreScore 扫描相关已有 Pod。每当某条规则匹配,就把它的权重记到对应的 (topologyKey, 拓扑值):软亲和取正,软反亲和取负。待调度 Pod 指向已有 Pod 的规则会计入;按插件配置,也会计入已有 Pod 指向它的规则。已有 Pod 的 required affinity 命中时可额外加 hardPodAffinityWeight(默认 1)。Score 用候选节点自己的拓扑标签取出这些累积值并相加,得到原始分 $r(n)$,可以为负。
令 $m$、$M$ 分别为候选原始分的最小值和最大值。$M=m$ 时所有节点得 0 分;否则:
$$ s(n)=\left\lfloor \frac{100(r(n)-m)}{M-m} \right\rfloor. $$源码先用 float64 算比例、乘 100,再截断为整数。例如三个候选原始分为 −20、0、30,换算后是 0、40、100;加权后是 0、80、200。相同 zone 的候选会共享该 zone 的累积值,但还可能因 hostname 等其他拓扑键而得分不同。见 InterPodAffinity 源码。
PodTopologySpread:统计域内副本,再反向换算
要把同一组副本分散到两个可用区,插件先数每个区里已有多少个匹配 selector 的 Pod。一个区副本多、另一个区副本少时,通常给副本少的区里的候选节点更高的分,这样新副本更有机会补到少的一侧。如果同时按节点和可用区分散,插件会把两种尺度的统计合起来,不能只看某台机器上有几个副本。ScheduleAnyway 表达这种偏好,其他打分项仍可改变落点;DoNotSchedule 的偏斜限制在 Filter 阶段处理。
对每条软约束 $c$,$N_c(n)$ 是候选节点所属域内、同命名空间且匹配 selector 的已有 Pod 数,$D_c$ 是候选节点覆盖的域数量。hostname 用未被忽略的候选节点数作 $D_c$,并在 Score 时统计该节点上的 Pod;其他拓扑域在 PreScore 聚合,计数时可纳入同域内的非候选节点,具体受节点纳入策略约束。
令 $m_c$ 为这条约束的 maxSkew。每个候选先算各条有对应拓扑标签的约束分 $v_c$,再求和并四舍五入:
用未被忽略的候选原始分的最小值 $m$、最大值 $M$ 做反向换算。因缺失必要拓扑标签被忽略的候选得 0;其余候选在 $M=0$ 时得 100,否则:
$$ s(n)=\left\lfloor \frac{100(M+m-r(n))}{M}\right\rfloor. $$例如只有一条 maxSkew: 1 的 hostname 软约束,两个候选分别已有 1 个和 4 个匹配 Pod,$D_c=2$,原始分为 1 和 6,换算后是 100 和 16。较拥挤的节点仍可能得分;这里的分母是 $M$,不是 $M-m$。见 PreScore 与归一化、拓扑权重与单项公式。
没有显式约束时,系统默认值是 hostname 的 maxSkew: 3、zone 的 maxSkew: 5,均为 ScheduleAnyway。推不出匹配 selector 时,例如没有匹配 Service 或受支持控制器选择器的裸 Pod,插件跳过这组默认规则。
ImageLocality:按命中镜像的大小和分布打分
节点已经缓存了 Pod 要用的镜像,可能省掉一次下载;缓存的大镜像比小镜像更值钱。不过,某个镜像只缓存于极少数节点时,插件会压低这项加分,避免大量使用同一镜像的 Pod 都被少数有缓存的节点吸引。因此它既看本节点命中了什么,也看这些镜像在集群快照里有多普遍。
Score 从节点快照查找普通容器、init 容器和镜像卷所需的镜像。每个命中项 $j$ 的贡献是 $\lfloor \mathrm{size}_j \times K_j/K \rfloor$:$K_j$ 是快照中报告有该镜像的节点数,$K$ 是插件读取的快照节点总数。未命中项贡献 0。累加得到 $X$,令 $I$ 为 Pod 的镜像项数(包括未命中项),$L=23\,\mathrm{MiB}$,$H=1000\,\mathrm{MiB}\times I$,则:
一个只用 200 MiB 镜像的 Pod,若一半节点有缓存,命中节点的 $X$ 是 100 MiB,得 7 分;未命中节点得 0。插件不再做跨节点归一化;命中也不保证 kubelet 不拉取镜像。见 ImageLocality 源码。
VolumeBinding:按可用卷方案计算容量分
两个节点都能满足 Pod 的卷请求时,默认曲线偏好这批请求占容量比例更低的卷方案。同一个 StorageClass 下,同样的请求放到容量更充裕的方案里,占用的比例更小,因而得分更高。插件先分 StorageClass 算占比,再平均各类的分数:一种存储类容量再大,也不能替另一种存储类承接请求。
Filter 先判断 PVC 在节点上能否绑定或供给。Score 用该节点的卷方案:有静态绑定时按 StorageClass 汇总 PVC 请求与对应 PV 容量;没有静态绑定时才读取动态供给的容量信息。同一类的多个动态 PVC 叠加请求,共用的容量不会按 PVC 数重复相加。没有可计入的卷方案或类时得 0。v1.37 的 StorageCapacityScoring 默认开启;关闭时评分器不启用。
对每个有数据的类 $c$,$R_c$ 为请求总量,$C_c$ 为容量。先算整数利用率 $u_c=\lfloor100R_c/C_c\rfloor$;$C_c=0$ 或请求超过容量时用 100。默认曲线为 $g(u)=100-u$,即 0% 对应 100 分,100% 对应 0 分。各类等权,$J$ 为纳入计算的 StorageClass 数量:
$$ s(n)=\operatorname{round}\left(\frac{\sum_c g(u_c)}{J}\right). $$例如两类的利用率为 33% 和 80%,类得分为 67 和 20,平均 43.5,最终得 44。自定义 shape 时,配置分数从 0–10 放大到 0–100,按利用率做分段线性插值。这里使用 PV 或 CSI 容量信息,不读节点磁盘实时空闲量。见 VolumeBinding 取数、默认曲线及容量打分器。
DynamicResources:设备分配越接近首选,分数越高
Pod 可以通过 ResourceClaim 给出一组有先后顺序的设备请求:先试首选,做不到再尝试后备选项。如果两个节点都能完成分配,一个能满足首选,另一个只能满足后备选项,这条请求会给前者更多分。这样后备设备仍然可用,用户声明的优先顺序也会反映在打分上。比较依据是每个节点实际能得到的分配方案,空闲设备多本身不会加分。
动态资源分配(DRA)的 Score 读取候选节点的 ResourceClaim 分配结果。令 $A(n)$ 为各 claim 的 firstAvailable 列表中实际分配到的子请求集合。每个子请求 $q$ 按它在所属列表中的下标 $i_q$ 计分,下标从 0 开始;8 是该版本允许的列表最大长度,不是实际列表长度。同一个子请求匹配多台设备也只计一次;exactly 请求不贡献这项偏好分。
令 $M$ 为所有候选的最高原始分。$M=0$ 时得 0 分,否则 $s(n)=\lfloor 100r(n)/M \rfloor$。比如 A 分配到首选、B 分配到第三选,原始分为 8 和 6,换算后是 100 和 75,加权后是 200 和 150。没有 claim、没有可计分分配,或优先列表打分不可用时,这项不产生偏好。它不读取 GPU 的实时利用率。见 DynamicResources 源码。
实测:软污点抵消节点亲和
在本机隔离 kind 控制面里,API Server 和原版 kube-scheduler 都是 v1.37.2。实验在 API 中创建两个同容量的合成节点 A、B,只给 A 加 PreferNoSchedule 污点。两个 Pod 都用软亲和偏好 A;第一个容忍该污点,第二个不容忍。合成节点不运行 kubelet,因此只观察调度器日志和 Pod 的 spec.nodeName,不验证容器启动。
| Pod | A 总分 | B 总分 | 实际选中 |
|---|---|---|---|
| 容忍 A 的软污点 | 597 | 397 | A |
| 不容忍 A 的软污点 | 296 | 397 | B |
把第二个 Pod 的分项还原成乘权重前的分数,可以逐项复算总分:
| 插件 | A 分项 | B 分项 |
|---|---|---|
TaintToleration | 0 × 3 | 100 × 3 |
NodeAffinity | 100 × 2 | 0 × 2 |
NodeResourcesFit | 96 × 1 | 97 × 1 |
VolumeBinding、DynamicResources、ImageLocality | 都是 0 | 都是 0 |
PodTopologySpread、InterPodAffinity、NodeResourcesBalancedAllocation | 跳过 | 跳过 |
| 总分 | 0 + 200 + 96 = 296 | 300 + 0 + 97 = 397 |
两个候选都通过 Filter;B 领先来自 300 分的软污点分差超过了 A 的 200 分节点亲和优势。第一个 Pod 容忍污点,TaintToleration 在两边同得 300 分,NodeAffinity 才把 A 推到第一。第一轮资源分两边都是 97;第二轮 A 上已经计入第一个 Pod,虽然它没显式填写 requests,非零估算仍使资源分从 97 变为 96。
复现步骤、脚本与原始分项日志见 k8sdev 打分实验。另外运行了 v1.37.2 上游集成测试中的资源、节点亲和、污点三个用例组,以及默认配置、调度框架和八个相关插件包的测试。上面的 kind 对照验证的是两条默认偏好叠加后的落点;卷和 DRA 的非零分支以源码和上游测试为依据。
想改变落点,先确认改的是哪一层
- 节点不能运行这个 Pod:检查 Filter 的失败原因。需要隔离、硬件必需或存储拓扑限制时,配置硬约束,别试图用高分代替。
- 希望把请求集中到少数节点:评估
NodeResourcesFit.scoringStrategy的MostAllocated或RequestedToCapacityRatio,并单独考虑NodeResourcesBalancedAllocation、拓扑分布等仍生效的偏好。 - 只是希望某条偏好赢过另一条:检查实际生效的
KubeSchedulerConfiguration和 Pod 的spec.schedulerName。用 profile 的plugins.score调整打分权重时,先确认未误关同一插件的过滤能力。 - 有多个可行节点却没选到预期节点:先看候选集合和 profile,再读调度器高日志级别下的
Plugin scored node for pod。日志里的分数已经乘过插件权重,200 或 300 都是正常值。不能从 Kubernetes Event 反推出所有分项;日志里没有分项时也要检查单候选、插件跳过和批处理快路径。