硬件没有扩容,工作负载也没有缩水,集群利用率却提高了 33 个百分点。来源标题给出的关键线索只有一个:变化发生在任务处理顺序上。由于摘要未提供具体调度器、负载类型和利用率口径,本文不推断原案例的实现细节,而是拆解这种结果背后的通用机制,并给出一个可以运行的装箱模拟。
利用率不仅取决于容量,也取决于到达顺序
集群调度可以看成带约束的装箱问题:节点是箱子,Pod、作业或虚拟机是待装入的物品。真实环境通常至少包含 CPU、内存、GPU、临时存储、拓扑和亲和性等维度。
当调度器按队列顺序逐个处理任务时,早期决策会改变后续任务可用的空间形状。例如,一个节点可能还剩 8 核 CPU,却只剩 1 GiB 内存;另一个节点可能正好相反。两者在监控面板上都有空闲资源,但一个需要大内存的任务仍然无法落位。
这类无法组合使用的剩余容量通常称为资源碎片。顺序会影响碎片何时产生、产生在哪些节点上,以及后续任务是否还能利用它。
需要特别区分两个说法:
- “提高 33%”表示相对增长。例如从 60% 提高到 79.8%。
- “提高 33 个百分点”表示绝对差值。例如从 50% 提高到 83%。
来源标题使用的是后者,意味着差异很大,更值得检查统计口径、任务完成量和服务质量是否保持一致。
为什么简单调整顺序就可能有效
一种常见策略是优先处理更难放置的任务。所谓“难”,可以由多种信号定义:
- 请求量接近单节点容量的大任务。
- GPU、特定机型或可用区要求严格的任务。
- CPU 与内存比例明显偏斜的任务。
- 带有反亲和、端口或拓扑约束的任务。
- 等待时间较长,继续延迟会违反服务目标的任务。
如果容易放置的小任务先占据关键节点,后续受约束任务可能只能等待,或者迫使平台启动新节点。反过来,先安置稀缺资源需求,再用灵活的小任务填补空隙,往往能减少碎片。
不过,“大任务优先”不是普遍最优解。它可能增加短任务等待时间,甚至造成队首阻塞。生产策略通常需要同时考虑资源匹配、等待时间、租户配额和优先级,而不是只按任务大小排序。
用一个可运行的装箱实验观察顺序效应
下面是一个简化的一维模型:每个节点容量为 10,调度器采用 First Fit,按输入顺序把任务放进第一个容得下的节点。它忽略了 Kubernetes 的过滤、打分和抢占机制,因此只是用于解释原理,并不代表来源案例的具体算法。
将代码保存为 packing.py 后直接运行 python packing.py:
from dataclasses import dataclass
@dataclass
class Result:
bins: list[list[int]]
utilization: float
def first_fit(tasks: list[int], capacity: int) -> Result:
bins: list[list[int]] = []
for task in tasks:
for bucket in bins:
if sum(bucket) + task <= capacity:
bucket.append(task)
break
else:
bins.append([task])
used = sum(tasks)
total = len(bins) * capacity
return Result(bins=bins, utilization=used / total)
def show(name: str, tasks: list[int]) -> None:
result = first_fit(tasks, capacity=10)
print(f"{name}: {tasks}")
print(f" nodes: {result.bins}")
print(f" node count: {len(result.bins)}")
print(f" utilization: {result.utilization:.1%}")
if __name__ == "__main__":
arrival_order = [4, 4, 4, 6, 6, 6]
hard_first = sorted(arrival_order, reverse=True)
show("arrival order", arrival_order)
show("larger tasks first", hard_first)
预期结果中,原始顺序需要 4 个节点,利用率为 75%;大任务优先只需要 3 个节点,利用率达到 100%。任务总量、节点容量和放置算法都没有变化,变化的只有输入顺序。
这个实验不能证明任何生产集群都能获得同样的提升。它说明的是:在线调度中的局部决策具有路径依赖,同一批任务可能因为排序不同而形成完全不同的碎片分布。
在 Kubernetes 中可以怎样实践
Kubernetes 原生优先级主要解决“谁更重要”,并不等同于自动执行最佳装箱排序。可以这样实践:先用 PriorityClass 明确业务优先级,再在批处理队列、工作流控制器或自定义调度器中加入“难放置程度”评分。
下面的 YAML 可以直接创建两个优先级。应用前应根据集群已有的 PriorityClass 调整名称和数值,避免与现有策略冲突:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: constrained-workload
value: 100000
preemptionPolicy: Never
globalDefault: false
description: "Workloads with scarce-resource or topology constraints"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: flexible-batch
value: 1000
preemptionPolicy: Never
globalDefault: false
description: "Flexible batch workloads that can fill remaining capacity"
应用并检查:
kubectl apply -f priority-classes.yaml
kubectl get priorityclass
kubectl get pods -A --field-selector=status.phase=Pending \
-o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,PRIORITY:.spec.priorityClassName'
Pod 通过 priorityClassName 引用相应等级:
apiVersion: v1
kind: Pod
metadata:
name: gpu-batch-job
spec:
priorityClassName: constrained-workload
restartPolicy: Never
containers:
- name: worker
image: ubuntu:24.04
command: ["sh", "-c", "sleep 300"]
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "2"
memory: "4Gi"
这里将 preemptionPolicy 设为 Never,是为了把“排队顺序”与“驱逐低优先级 Pod”分开评估。若启用抢占,利用率变化可能来自任务重排,也可能来自驱逐和重新调度,实验结果会更难解释。
上线前不要只看平均利用率
排序策略改变的是全局行为,不能只比较一张 CPU 曲线。一个可靠的评估至少应同时记录:
- CPU、内存和 GPU 的请求利用率与实际利用率。
- Pending Pod 数量及 P50、P95、P99 等待时间。
- 完成的任务数、吞吐量和任务失败率。
- 节点数量、扩缩容次数与云资源成本。
- 各租户或队列的等待时间,防止某类任务长期饥饿。
- 抢占、驱逐和重复计算造成的额外开销。
建议先回放一段真实任务轨迹,在相同容量下比较不同排序策略;随后选择单个队列做灰度,并保留最大等待时间或 aging 机制。只有在吞吐量、延迟和公平性都可接受时,利用率提升才真正有价值。
顺序不是免费的容量,但它决定现有容量能否被拼接起来。对于异构、约束密集或批处理占比较高的集群,调度队列往往和节点规格一样值得优化。