第一篇提到 scheduler 在内存里跑完 Filter 和 Score 两个阶段就把结果写回 apiserver,但没展开这两个阶段具体在算什么。这一篇把黑箱打开:Filter 是一道道"能不能"的判断题,任何一条不满足就直接淘汰;Score 是给活下来的节点打分,分数从哪来、为什么同样的 Pod 在不同节点上打分会不一样,这篇用一个最常见的打分插件(NodeResourcesFit 的 LeastAllocated 策略)讲清楚。
跟上一篇 kube-proxy 一样的做法:本机没有真实多节点集群可以拿来调度,直接从 kube-scheduler v1.37.0 真实源码的官方单元测试里取出它自己断言过的真实打分结果(不是编的例子),用 Python 把打分公式原样实现一遍,逐数字核对。
逐个节点跑一遍所有 Filter 插件,任何一个插件说"不行"就直接淘汰这个节点,不再往下看。资源不够、端口冲突、亲和性不满足,都在这一步过滤掉。
只有通过全部 Filter 的节点才会进入这一步。每个 Score 插件给每个候选节点打一个 0-100 的分,加权汇总,分数最高的节点(或按权重随机选一个高分节点)胜出。
NodeResourcesFit 插件的 Filter 部分,判断逻辑就是一行比较。
pkg/scheduler/framework/plugins/noderesources/fit.go · kubernetes @ v1.37.0, L733 if podRequest.MilliCPU > 0 && deltaMilliCPU > (nodeInfo.GetAllocatable().GetMilliCPU()-nodeInfo.GetRequested().GetMilliCPU()) { insufficientResources = append(insufficientResources, InsufficientResource{ ResourceName: v1.ResourceCPU, Reason: "Insufficient cpu", ... }) }
pkg/scheduler/framework/plugins/noderesources/fit.go · kubernetes @ v1.37.0, L654 func (f *Fit) Filter(ctx context.Context, cycleState fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status { ... insufficientResources := fitsRequest(s, nodeInfo, ...) if len(insufficientResources) != 0 { // 把所有不满足的原因都收集起来,一次性返回,不是发现第一个就退出 return fwk.NewStatus(fwk.Unschedulable, failureReasons...) } return nil }
判断是"Pod 请求的 CPU 是否超过节点剩余可分配的 CPU"(allocatable - requested),内存、临时存储走的是同一个模式。只要有一项不满足,这个节点就被打上 Unschedulable 状态,不再进入 Score 阶段——这就是为什么"资源不够的节点"永远不会因为其他维度分数高就被选中:它压根没有资格参与打分。
pkg/scheduler/framework/plugins/noderesources/least_allocated.go · kubernetes @ v1.37.0, L49 // The unused capacity is calculated on a scale of 0-MaxNodeScore // 0 being the lowest priority and `MaxNodeScore` being the highest. // The more unused resources the higher the score is. func leastRequestedScore(requested, capacity int64) int64 { if capacity == 0 { return 0 } if requested > capacity { return 0 } return ((capacity - requested) * fwk.MaxNodeScore) / capacity }
pkg/scheduler/framework/plugins/noderesources/least_allocated.go · kubernetes @ v1.37.0, L24 // leastResourceScorer favors nodes with fewer requested resources. func leastResourceScorer(resources []config.ResourceSpec) func([]int64, []int64, []int64) int64 { return func(requested, _, allocable []int64) int64 { var nodeScore, weightSum int64 for i := range requested { weight := resources[i].Weight resourceScore := leastRequestedScore(requested[i], allocable[i]) nodeScore += resourceScore * weight weightSum += weight } return nodeScore / weightSum } }
核心就是剩余容量的百分比,不是剩余容量的绝对值:(容量-已用)*100/容量,每种资源(CPU、内存……)各算一个百分比分数,按权重(默认 CPU 和内存权重都是 1)取加权平均,就是这个节点在这一个插件下的最终分数。fwk.MaxNodeScore 就是 100——每个 Score 插件的输出都被统一映射到 0-100 这个区间,方便跟其他打分插件加权汇总。
直接取 kube-scheduler 官方测试文件里的真实节点配置和真实期望分数,用 Python 实现上面两段公式,看算不算得出一样的结果。
$ python3 -c "
def least_requested_score(requested, capacity):
if capacity == 0 or requested > capacity: return 0
return ((capacity - requested) * 100) // capacity
# 真实测试用例:Pod 请求 cpu=3000, memory=5000(两个容器的请求求和)
# node1: capacity cpu=4000, memory=10000(小节点)
# node2: capacity cpu=6000, memory=10000(大节点,只有 CPU 容量不同)
n1 = (least_requested_score(3000, 4000) + least_requested_score(5000, 10000)) // 2
n2 = (least_requested_score(3000, 6000) + least_requested_score(5000, 10000)) // 2
print('node1:', n1, ' node2:', n2)
"
node1: 37 node2: 50 ← 跟 kube-scheduler 官方测试用例的真实期望分数逐位一致
同一个 Pod、同样的请求量,node2 只是把 CPU 容量从 4000 加到 6000(内存容量没变),分数就从 37 涨到 50——因为打的是比例分不是绝对值:node1 的 CPU 用掉了 75%(3000/4000),node2 只用掉了 50%(3000/6000)。这也是这个策略叫 LeastAllocated(偏爱占用比例更低的节点)的原因:大节点天然更容易在这个维度上得高分,倾向把负载摊薄,而不是把小节点先塞满。
再取一组真实测试用例:这次新 Pod 本身不请求任何资源,但两个节点上都已经有 Pod 在跑。
| 节点 | capacity | 已有 Pod 用量 | CPU 分 | 内存分 | 最终分 |
|---|---|---|---|---|---|
| node1 | cpu:10000 mem:20000 | cpu:6000 mem:0 | 40 | 100 | 70 |
| node2 | cpu:10000 mem:20000 | cpu:6000 mem:5000 | 40 | 75 | 57 |
两个节点容量完全一样,CPU 占用也一样(都是 6000),差别只在内存——node2 上已有的 Pod 多占了 5000 内存,内存分从 100 掉到 75,最终分从 70 掉到 57。requested 这个输入不是"这个新 Pod 要多少",是"这个节点已经承诺出去的总量,加上这个新 Pod 要的量"——已经在跑的 Pod 一直在拉低这个节点后续的打分,这正是负载均衡效果的来源。
把上面全部真实数据和真实公式按发生顺序串成一条演示。
判断逻辑由 Python 脚本按真实源码逐行转写:Filter 的判断直接照抄 fitsRequest 的"请求超过剩余可分配量就拒绝"规则;Score 的公式跟 leastRequestedScore/leastResourceScorer 一致,3 组真实测试用例的分数全部逐位核对通过,另有一套用浮点除法而不是整数除法重新实现的独立版本交叉核对过。