作为 Kubernetes 控制平面的核心,kube-scheduler 的职责看似单一——决定每个 Pod 落在哪个节点上,但其调度策略的优劣直接影响着集群资源利用率、应用性能和高可用性。随着 Kubernetes 1.19 版本后全面转向 Scheduling Framework 插件化架构,我们有了前所未有的能力来干预和定制调度决策。本文将带你深入调度器内部,从内置的 Score 插件原理剖析,到动手开发一个自定义调度器。
一、概述
1.1 调度框架与流程
从 Filter 到 Score,再到最终的 Bind,每个 Pod 的调度旅程都遵循一条清晰的插件链流水线:调度队列 → PreFilter → Filter → PostFilter → PreScore → Score → Reserve → Permit → PreBind → Bind → PostBind。各阶段分工明确,共同决定了 Pod 的最终归宿。
1.2 内置调度插件一览
调度器的能力由插件构成,主要分为两类:
- Filter 插件(节点过滤):负责执行硬性检查,排除不满足条件的节点。
- Score 插件(节点打分):负责对通过筛选的节点进行评分,分数越高,被选中的概率越大。
1.3 扩展方式对比
当默认调度策略无法满足需求时,我们有多种扩展路径,选择哪种取决于你的具体场景和可接受的复杂度。
| 方式 |
侵入性 |
灵活性 |
性能 |
适用场景 |
| KubeSchedulerConfiguration |
无 |
低 |
高 |
调整内置插件权重和参数 |
| Scheduler Extender |
低 |
中 |
低(HTTP 调用) |
简单扩展,不想编译调度器 |
| Scheduling Framework Plugin |
中 |
高 |
高(进程内) |
自定义调度逻辑,生产推荐 |
| 独立自定义调度器 |
高 |
最高 |
高 |
完全不同的调度策略 |
1.4 典型应用场景
深入理解调度机制,能帮助我们解决诸多实际问题:
- 资源优化:通过调整
Score 插件权重,实现资源装箱(BinPacking)以提高利用率,或均衡分布以提高稳定性。
- GPU等特殊硬件调度:基于 GPU 型号、显存利用率等指标进行自定义打分。
- 拓扑感知调度:实现 NUMA 感知、跨可用区亲和等,优化应用性能与容灾能力。
- 批处理与组调度:实现 Gang Scheduling,确保一组
Pod 同时成功调度或同时失败。
- 多租户隔离:为不同团队或业务线配置不同的调度策略集(Profile)。
二、调度流程深度解析
2.1 Filter 阶段:节点过滤
Filter 阶段对每个候选节点执行一系列“一票否决”检查。核心判断逻辑包括:
- 资源是否充足:
NodeResourcesFit 插件对比 Pod 的 requests 与节点 Allocatable 减去已分配量。
- 亲和性是否满足:
NodeAffinity 检查节点标签是否匹配 nodeSelector 和 nodeAffinity 规则。
- 污点是否容忍:
TaintToleration 检查 Pod 的 tolerations 是否覆盖节点所有 NoSchedule 污点。
- 端口是否冲突:
NodePorts 检查 hostPort 在目标节点上是否已被占用。
- 拓扑约束是否满足:
PodTopologySpread 检查调度后是否违反 maxSkew。
当所有节点都被淘汰时,调度器会进入 PostFilter 阶段尝试抢占——驱逐部分低优先级 Pod 以腾出资源。
2.2 Score 阶段:节点打分
通过 Filter 的节点进入 Score 阶段。每个 Score 插件为节点打出一个 0-100 的分数,最终加权求和,得分最高者胜出。这是调度器做“选择题”的关键环节。
| 插件 |
算法 |
权重默认值 |
| NodeResourcesFit (LeastAllocated) |
(Allocatable - Requested) / Allocatable * 100 |
1 |
| NodeResourcesFit (MostAllocated) |
Requested / Allocatable * 100 |
1 |
| NodeResourcesBalancedAllocation |
100 - abs(cpuFraction - memFraction) * 100 |
1 |
| ImageLocality |
按已存在镜像大小占比打分 |
1 |
| InterPodAffinity |
满足的 preferredDuringScheduling 规则数加权 |
1 |
| TaintToleration |
未匹配的 PreferNoSchedule 污点越少分越高 |
3 |
| PodTopologySpread |
调度后 skew 越小分越高 |
2 |
2.3 调度器配置文件
调度行为可以通过 KubeSchedulerConfiguration 进行精细控制。以下是一个生产环境配置示例,展示了如何调整插件权重和策略。
# /etc/kubernetes/scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
parallelism: 16
leaderElection:
leaderElect: true
resourceNamespace: kube-system
resourceName: kube-scheduler
profiles:
- schedulerName: default-scheduler
plugins:
score:
enabled:
# 启用资源均衡打分,权重设为 2
- name: NodeResourcesBalancedAllocation
weight: 2
# 镜像本地性,减少拉取时间
- name: ImageLocality
weight: 1
# 拓扑分散,确保跨故障域分布
- name: PodTopologySpread
weight: 3
disabled:
# 禁用不需要的插件减少调度延迟
- name: NodeResourcesFit
filter:
enabled:
- name: NodeResourcesFit
- name: NodeAffinity
- name: TaintToleration
pluginConfig:
- name: NodeResourcesFit
args:
# 评分策略:LeastAllocated 倾向空闲节点
# MostAllocated 倾向装箱,适合缩容场景
scoringStrategy:
type: LeastAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
三、高级调度策略实战
3.1 亲和性与反亲和性
利用亲和性规则,你可以像“磁铁”一样将 Pod 吸引到特定节点或彼此靠近,也可以像“同极相斥”一样让它们分开。
节点亲和性示例:将数据库调度到带 SSD 的节点,并优先选择某个可用区。
apiVersion: v1
kind: Pod
metadata:
name: postgres-primary
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk-type
operator: In
values: [ssd]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [us-east-1a]
containers:
- name: postgres
image: postgres:16
resources:
requests:
cpu: "2"
memory: 4Gi
Pod 间亲和与反亲和示例:Web 应用与 Redis 缓存保持同可用区,同时 Web 实例自身尽量分散在不同节点。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: [redis]
topologyKey: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: [web]
topologyKey: kubernetes.io/hostname
containers:
- name: web
image: web-app:v2.1
3.2 污点与容忍
污点(Taint)和容忍(Toleration)为节点和 Pod 提供了一种排斥机制,常用于守护节点、专用硬件调度等场景。
GPU 节点专用调度配置:
# 为 GPU 节点打污点
kubectl taint nodes gpu-node-01 nvidia.com/gpu=present:NoSchedule
# 为 GPU 节点添加标签
kubectl label nodes gpu-node-01 accelerator=nvidia-a100
对应的 GPU 训练任务 Pod:
apiVersion: v1
kind: Pod
metadata:
name: training-job
spec:
tolerations:
- key: nvidia.com/gpu
operator: Equal
value: present
effect: NoSchedule
nodeSelector:
accelerator: nvidia-a100
containers:
- name: trainer
image: pytorch-training:2.3
resources:
limits:
nvidia.com/gpu: 2
3.3 拓扑分散约束
TopologySpreadConstraints 是控制 Pod 在拓扑域(如可用区、节点)间均匀分布的首选方案,比 podAntiAffinity 更灵活高效。
多维度拓扑分散示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 9
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
topologySpreadConstraints:
# 可用区级别:最大偏差 1,硬约束
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
# 节点级别:最大偏差 2,软约束
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
containers:
- name: api
image: api-server:v3.0
四、开发自定义调度插件
4.1 实现自定义 Score 插件
当内置插件无法满足需求时,例如需要根据 GPU 利用率进行调度,我们可以开发自定义插件。以下是一个根据节点 GPU 利用率打分的 Score 插件示例。
// gpu_score_plugin.go
// 基于节点 GPU 利用率的自定义打分插件
package gpuscore
import (
"context"
"fmt"
"math"
"strconv"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
const Name = "GPUUtilizationScore"
type GPUScorePlugin struct {
handle framework.Handle
}
var _ framework.ScorePlugin = &GPUScorePlugin{}
func (pl *GPUScorePlugin) Name() string {
return Name
}
func (pl *GPUScorePlugin) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
if err != nil {
return 0, framework.AsStatus(fmt.Errorf("获取节点信息失败: %v", err))
}
node := nodeInfo.Node()
// 从节点注解读取 GPU 利用率(假设由监控组件定期更新)
gpuUtil, ok := node.Annotations["gpu-monitor/utilization"]
if !ok {
return 50, nil // 没有信息,给中间分
}
utilization, err := strconv.ParseFloat(gpuUtil, 64)
if err != nil {
return 50, nil
}
// 利用率越低,分数越高(0-100 范围)
score := int64(math.Round(100 - utilization))
if score < 0 {
score = 0
}
if score > 100 {
score = 100
}
return score, nil
}
func (pl *GPUScorePlugin) ScoreExtensions() framework.ScoreExtensions {
return nil
}
func New(_ context.Context, _ runtime.Object, h framework.Handle) (framework.Plugin, error) {
return &GPUScorePlugin{handle: h}, nil
}
4.2 注册并构建自定义调度器
编写插件的 main 函数,将其注册到调度框架中。
// cmd/scheduler/main.go
package main
import (
"os"
"k8s.io/component-base/cli"
"k8s.io/kubernetes/cmd/kube-scheduler/app"
gpuscore "example.com/gpu-scheduler/pkg/gpuscore"
)
func main() {
// 创建调度器命令,注册自定义插件
command := app.NewSchedulerCommand(
app.WithPlugin(gpuscore.Name, gpuscore.New),
)
code := cli.Run(command)
os.Exit(code)
}
4.3 部署与使用
编译并部署你的自定义调度器,Pod 通过 spec.schedulerName 字段指定使用它。
#!/bin/bash
# build-scheduler.sh
set -euo pipefail
SCHEDULER_NAME="gpu-aware-scheduler"
IMAGE_TAG="v1.0.0"
REGISTRY="registry.internal.com/infra"
echo "=== 编译自定义调度器 ==="
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w" \
-o bin/${SCHEDULER_NAME} \
./cmd/scheduler/
echo "=== 构建容器镜像 ==="
docker build -t "${REGISTRY}/${SCHEDULER_NAME}:${IMAGE_TAG}" \
--build-arg BINARY=${SCHEDULER_NAME} .
echo "=== 部署到集群 ==="
kubectl apply -f deploy/scheduler-deployment.yaml
五、生产环境最佳实践与避坑指南
5.1 资源请求与限制:调度的基石
务必牢记:调度器只看 requests,不看 limits。requests 是调度和节点资源分配的凭据,limits 是运行时限制。
- 设置合理的比例:对于在线服务,CPU
requests:limits 可按 1:2 到 1:4 设置以允许突发,内存建议 1:1 到 1:1.5 以防止 OOM。
- 使用 LimitRange 设置默认值:防止因未设置
requests 而创建大量 BestEffort Pod,导致资源竞争和不稳定。
- 利用 ResourceQuota 进行总量控制:在命名空间级别限制资源总量,实现多租户间的资源隔离。
5.2 性能优化与大规模集群
当集群节点数量超过 500 时,默认配置可能成为瓶颈。
- 调整
percentageOfNodesToScore:该参数控制打分阶段评估的节点比例。对于大规模集群,设置为 10-30 可显著降低调度延迟,同时基本不影响调度质量。
- 善用 PriorityClass:建立清晰的优先级体系,确保系统组件和核心业务在资源紧张时优先被调度,甚至可以通过抢占低优先级任务获得资源。
5.3 常见调度失败与排查
Pod 处于 Pending 状态是常见问题,可通过 kubectl describe pod 查看事件快速定位。
Insufficient cpu/memory:检查 Pod 的 requests 是否设置过高,或集群整体资源是否真的不足。
node(s) didn‘t match Pod’s node affinity:核对 Pod 的 nodeSelector 或 nodeAffinity 规则与节点实际标签是否匹配。
node(s) had taint:确认 Pod 是否配置了对应的 tolerations。
Too many pods:节点已达 kubelet 的 --max-pods 限制,需调整该参数或将 Pod 分散到其他节点。
5.4 需要警惕的陷阱
podAntiAffinity 的性能代价:requiredDuringScheduling 类型的反亲和规则在大规模集群中可能导致 O(n²) 的计算复杂度,尽量使用 preferredDuringScheduling 或 TopologySpreadConstraints。
- Request 设置过低:虽然能轻松调度,但实际运行时若资源使用远超
requests,在节点压力大时该 Pod 会优先被驱逐(kubelet 依据 (实际使用 - requests) 排序)。
- 配置的版本兼容性:注意
KubeSchedulerConfiguration 的 API 版本(v1beta1, v1beta2, v1),不同 Kubernetes 版本支持情况不同,错误版本会导致调度器启动失败。
六、总结与展望
深入理解 Kubernetes Pod 调度,是从“会用”到“精通”容器化和集群管理的关键一步。从内置的 Filter/Score 插件机制,到利用亲和性、污点、拓扑约束进行精细控制,再到通过 Scheduling Framework 开发自定义插件,我们拥有强大的工具来塑造集群的调度行为,使其更好地服务于多样的工作负载和业务目标。
在运维大规模、多租户的生产集群时,结合 ResourceQuota、LimitRange、PriorityClass 以及监控告警,可以构建一个既高效又稳定的调度体系。当然,社区也在不断演进,像 Kueue(批处理作业队列)、Volcano(高性能计算调度)等高级调度器,以及 Descheduler(重新平衡集群)等工具,为我们解决更复杂的场景提供了方案。如果你在实践过程中有更多心得或遇到了有趣的调度难题,欢迎来到云栈社区与大家一起探讨。