今天的文章主要介绍调度过程中 Predicates 和 Priorities 调度策略发挥作用的关键阶段。
Predicates
我们先来看 Predicates。在调度过程中,Predicates 的作用可以理解为 Filter(过滤器),即:它会根据调度策略,从当前集群中的所有节点中“筛选”出一系列符合条件的节点。这些节点都是等待调度 Pod 的潜在宿主机。
在 Kubernetes 中,有四个默认的调度策略。
第一种称为 GeneralPredicates。
顾名思义,这组过滤规则负责最基本的调度策略。例如,PodFitsResources 计算宿主机 CPU 和内存资源是否充足。当然,如前所述,PodFitsResources 只检查 Pod 的 requests 字段。需要注意的是,Kubernetes 调度器并未为 GPU 等硬件资源定义特定的资源类型,而是采用一种名为 Extended Resource 的 Key-Value 格式来描述。例如:
apiVersion: v1
kind: Pod
metadata:
name: extended-resource-demo
spec:
containers:
- name: extended-resource-demo-ctr
image: nginx
resources:
requests:
alpha.kubernetes.io/nvidia-gpu: "2"
limits:
alpha.kubernetes.io/nvidia-gpu: "2"
可以看到,我们的 Pod 使用 alpha.kubernetes.io/nvidia-gpu=2 的定义方式声明使用了两个 NVIDIA 类型的 GPU。在 PodFitsResources 中,调度器实际上并不了解该字段 Key 的含义是 GPU,而是直接使用后面的 Value 进行计算。当然,在 Node 的 Capacity 字段中,也需要添加该主机上的 GPU 总数,例如:alpha.kubernetes.io/nvidia-gpu=4。这些过程将在后续讲解 Device Plugin 时详细介绍。
PodFitsHost 检查主机名是否与 Pod 的 spec.nodeName 匹配。PodFitsHostPorts 检查 Pod 申请的主机端口(spec.nodePort)是否与已使用的端口冲突。PodMatchNodeSelector 检查 Pod 的 nodeSelector 或 nodeAffinity 指定的节点是否与当前被检查的节点匹配,等等。
可以看到,这样一组 GeneralPredicates 是 Kubernetes 检查一个 Pod 能否在某个 Node 上运行的最基本过滤条件。因此,GeneralPredicates 也会被其他组件(如 kubelet)直接调用。
第二种是与 Volume 相关的过滤规则。
这组过滤规则负责容器持久化 Volume 相关的调度策略。其中,NoDiskConflict 检查多个 Pod 声明的持久化 Volume 之间是否存在冲突。例如,AWS EBS 类型的 Volume 不允许被两个 Pod 同时使用。所以,当一个名为 A 的 EBS Volume 已经挂载在某个节点上时,另一个同样声明使用该 A Volume 的 Pod 就不能被调度到该节点。MaxPDVolumeCountPredicate 检查节点上某种类型的持久化 Volume 是否已超过一定数量。如果是,那么声明使用该类型持久化 Volume 的 Pod 就不能再被调度到这个节点。VolumeZonePredicate 检查持久化 Volume 的 Zone(高可用域)标签是否与被检查节点的 Zone 标签匹配。
此外,还有一个名为 VolumeBindingPredicate 的规则。它负责检查 Pod 对应 PV 的 nodeAffinity 字段是否与某个节点的标签匹配。Local Persistent Volume(本地持久化卷)必须使用 nodeAffinity 绑定到特定节点。这实际上意味着在 Predicates 阶段,Kubernetes 必须能够根据 Pod 的 Volume 属性进行调度。另外,如果 Pod 的 PVC 尚未绑定到特定的 PV,调度器还负责检查所有待绑定的 PV。当存在可用的 PV 且该 PV 的 nodeAffinity 与被检查节点一致时,此规则会返回“成功”。例如:
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-local-pv
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/disks/vol1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- my-node
可以看到,该 PV 对应的持久化目录只会出现在名为 my-node 的主机上。因此,任何通过 PVC 使用该 PV 的 Pod 都必须被调度到 my-node 上才能正常工作。VolumeBindingPredicate 正是调度器做出这一判断的地方。
第三种是与宿主机相关的过滤规则。
这组规则主要检查待调度 Pod 是否满足 Node 本身的某些条件。例如,PodToleratesNodeTaints 检查我们经常使用的 Node 的“污点”机制。只有 Pod 的 Toleration 字段与 Node 的 Taint 字段匹配时,该 Pod 才能被调度到该 Node。NodeMemoryPressurePredicate 检查当前 Node 的内存是否不足。如果是,则待调度 Pod 不能被调度到该 Node。
第四种是与 Pod 相关的过滤规则。
这组规则与 GeneralPredicates 中的大部分重叠。特殊的是 PodAffinityPredicate。该规则的作用是检查待调度 Pod 与 Node 上已有 Pod 之间的亲和性和反亲和性关系。例如:
apiVersion: v1
kind: Pod
metadata:
name: with-pod-antiaffinity
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: kubernetes.io/hostname
containers:
- name: with-pod-affinity
image: docker.io/ocpqe/hello-pod
在这个例子中,podAntiAffinity 规则指定该 Pod 不希望与任何带有 security=S2 标签的 Pod 存在于同一 Node 上。需要注意的是,PodAffinityPredicate 有一个作用域,例如上述规则仅对带有 kubernetes.io/hostname 标签的 Node 有效。这就是 topologyKey 关键字的作用。
这四种 Predicates 构成了调度器判断一个 Node 能否运行待调度 Pod 的基本策略。执行时,当调度一个 Pod,Kubernetes 调度器会同时启动 16 个 Goroutine 并发地为集群中所有 Node 计算 Predicates,最后返回能够运行该 Pod 的主机列表。
需要注意的是,在为每个 Node 执行 Predicates 时,调度器会按照固定顺序进行检查。这个顺序是根据 Predicates 本身的意义决定的。例如,与宿主机相关的 Predicates 会优先检查。否则,在一个资源严重不足的主机上计算 PodAffinityPredicate 是没有意义的。
Priorities
在 Predicates 阶段对节点进行“过滤”之后,Priorities 阶段的工作就是对这些节点进行打分。这里的评分范围是 0-10 分,得分最高的节点就是最终绑定 Pod 的最佳节点。
Priorities 中最常用的评分规则是 LeastRequestedPriority。该算法实际上会选择资源(CPU 和内存)最空闲的主机。
此外,还有另外三种 Priorities:NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority。顾名思义,它们与之前的三种 Predicates(PodMatchNodeSelector、PodToleratesNodeTaints 和 PodAffinityPredicate)在含义和计算方法上类似。但作为 Priority,一个 Node 满足上述规则越多,其得分就越高。
在默认的 Priorities 中,还有一个名为 ImageLocalityPriority 的策略。这是 Kubernetes v1.12 中启用的新调度规则:如果待调度 Pod 所需的镜像很大并且已经存在于某些 Node 上,那么这些 Node 将获得更高的分数。当然,为了避免算法导致调度堆叠,调度器在计算分数时还会根据镜像的分布进行优化,即:如果大镜像分布的节点数量很少,那么这些节点的权重会降低,从而“抵消”引起调度堆叠的风险。
综上所述,这就是 Kubernetes 调度器中默认调度规则的主要工作原理。
在实际执行过程中,调度器中的集群和 Pod 信息已被缓存,因此这些算法的执行速度相对较快。
另外,对于更复杂的调度算法(如 PodAffinityPredicate),它们在计算时不仅关注待调度 Pod 和被检查的 Node,还需要关注整个集群的信息,例如遍历所有节点并读取它们的 Labels。此时,Kubernetes 调度器会在为每个待调度 Pod 执行调度算法之前,预先计算算法所需的集群信息,然后将其缓存。这样,在实际执行算法时,调度器只需读取缓存信息进行计算,从而避免了在计算每个 Node 的 Predicates 时反复检索和计算整个集群的信息。
总结
综上所述,本文讨论了 Kubernetes 默认调度器中的主要调度算法。需要注意的是,除了本文涉及的规则外,Kubernetes 调度器中实际上还有一些默认未启用的策略。你可以为 kube-scheduler 指定配置文件或创建 ConfigMap 来配置启用或禁用哪些规则。此外,你还可以通过设置 Priorities 的权重来控制调度器的行为。
至此,Kubernetes 调度器中默认调度机制的解释就结束了。值得一提的是,由于集群和 Pod 信息已被缓存,调度器运行速度很快;但对于更复杂的算法,调度器必须考虑整个集群的状态,而不仅仅是涉及的 Pod 和 Node。这包括在实际执行调度算法之前预先计算并缓存必要的集群信息,从而确保调度过程的效率和准确性。
Novita AI 是一个一体化云平台,助力您的 AI 雄心。通过无缝集成的 API、无服务器计算和 GPU 加速,我们提供经济高效的工具有助于快速构建和扩展您的 AI 业务。消除基础设施烦恼,免费开始 —— Novita AI 让您的 AI 梦想成为现实。
推荐阅读:
