Kubernetes Scheduler 扩展与自定义调度算法实现

Kubernetes Scheduler 扩展与自定义调度算法:让你的 Pod 住进“豪宅”🏡

各位观众老爷,各位技术大佬,大家好!我是你们的老朋友,爱写代码的段子手,今天咱们来聊聊 Kubernetes 调度器(Scheduler)的扩展与自定义调度算法。

话说这 Kubernetes 集群啊,就像一个大型社区,里面住满了各种各样的 Pod,它们就像社区里的居民。而 Kubernetes Scheduler,就是这个社区的“房屋中介”,负责给这些 Pod 安排“房子”,也就是节点(Node)。

默认情况下,Scheduler 已经足够智能,能根据资源需求、污点容忍等因素,把 Pod 安排到合适的节点上。但是,人嘛,总是会有一些“特殊癖好”的,Pod 也一样。比如,有的 Pod 偏爱 SSD 硬盘,有的 Pod 喜欢靠近 GPU 节点,还有的 Pod 就是想和它的“好基友”Pod 住在一起。

这时候,默认的 Scheduler 就有点力不从心了。怎么办呢?别慌,Kubernetes 提供了强大的扩展机制,允许我们自定义调度算法,满足 Pod 的各种“任性”需求,让它们住进心仪的“豪宅”!

一、Scheduler 的“内心世界”:了解调度流程

想要自定义调度算法,首先得了解 Scheduler 的工作原理,摸清它的“脾气”。咱们先来简单了解一下 Scheduler 的调度流程:

  1. 监听(Watch): Scheduler 就像一个兢兢业业的“房产中介”,时刻关注着 Kubernetes API Server,监听着新建的、未调度的 Pod。
  2. 预选(Predicate): 收到 Pod 后,Scheduler 开始“筛选房源”,也就是对集群中的所有 Node 进行过滤,排除那些不满足 Pod 要求的节点。这就像中介会根据你的预算、户型偏好,初步筛选出几套房源。
  3. 优选(Priority): 经过预选,剩下的 Node 都是满足 Pod 基本要求的。接下来,Scheduler 会对这些 Node 进行打分,选出最合适的那个。这就像中介会根据采光、交通、学区等因素,对初步筛选的房源进行综合评估,选出性价比最高的。
  4. 绑定(Bind): 最终,Scheduler 会将 Pod 绑定到得分最高的 Node 上,告诉 Kubernetes API Server:“就它了,这 Pod 就住这儿了!”API Server 收到指令后,就会通知 Kubelet 在该 Node 上创建 Pod。

可以用一个表格来总结:

阶段 功能 对应“房屋中介”行为
监听 监听新建的、未调度的 Pod 时刻关注房产市场,了解最新的房源信息和客户需求
预选 根据 Pod 的要求,过滤不满足条件的 Node 根据客户的预算、户型偏好,初步筛选出几套房源
优选 对满足条件的 Node 进行打分,选出最合适的 根据采光、交通、学区等因素,对初步筛选的房源进行综合评估,选出性价比最高的
绑定 将 Pod 绑定到得分最高的 Node 上 确定最终的房源,并与客户签订合同,办理入住手续

了解了 Scheduler 的调度流程,我们就知道在哪里可以“动手动脚”,实现自定义调度算法了。

二、Scheduler 扩展的“十八般武艺”:总有一款适合你

Kubernetes 提供了多种扩展机制,允许我们自定义 Scheduler 的行为。常见的扩展方式有以下几种:

  1. Pod 亲和性与反亲和性(Affinity & Anti-Affinity): 这是一种最常用的调度策略,允许我们指定 Pod 应该或者不应该与哪些 Pod 部署在同一个 Node 上。这就像你想和你的“好基友”住在一起,或者不想和你的“死对头”住得太近。
  2. Node 选择器(Node Selector): 通过 Node Selector,我们可以指定 Pod 只能部署在具有特定标签(Label)的 Node 上。这就像你想住在带花园的别墅,或者靠近地铁站的公寓。
  3. 污点(Taint)与容忍度(Toleration): Taint 用于标记 Node,表示该 Node 不适合运行某些 Pod。而 Toleration 则用于 Pod,表示该 Pod 可以容忍某些 Taint。这就像某些“豪宅”只允许特定身份的人入住,而你正好拥有可以“容忍”这些“特殊要求”的“通行证”。
  4. 扩展调度器(Extender): 扩展调度器是一种更高级的扩展方式,允许我们编写自定义的调度器程序,与默认的 Scheduler 协同工作。这就像你觉得默认的“房屋中介”不够专业,自己找了一个更厉害的“私人顾问”。
  5. Scheduling Framework: Kubernetes 1.19 引入了 Scheduling Framework,它是一种更加灵活、可插拔的调度器扩展机制。通过 Scheduling Framework,我们可以编写自定义的调度插件,插入到 Scheduler 的各个阶段,实现更精细化的调度控制。这就像你把“房屋中介”的各个环节都进行了定制,打造了一个完全符合你需求的“专属服务”。

这么多扩展方式,是不是有点眼花缭乱?别担心,咱们一个个来讲解,保证你听得明明白白,用得得心应手。

1. Pod 亲和性与反亲和性:抱团取暖,远离是非

Pod 亲和性(Affinity)允许我们指定 Pod 应该与哪些 Pod 部署在同一个 Node 上。例如,我们可以让 Web 应用 Pod 和缓存 Pod 部署在同一个 Node 上,以减少网络延迟。

Pod 反亲和性(Anti-Affinity)则允许我们指定 Pod 不应该与哪些 Pod 部署在同一个 Node 上。例如,我们可以让同一个应用的多个实例分散部署在不同的 Node 上,以提高应用的可用性。

下面是一个使用 Pod 亲和性的例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  selector:
    matchLabels:
      app: web-app
  replicas: 3
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: nginx
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - cache
            topologyKey: kubernetes.io/hostname

上面的 YAML 文件定义了一个名为 web-app 的 Deployment,它包含 3 个副本。affinity 字段指定了 Pod 亲和性,podAffinity 表示 Pod 亲和性规则。

requiredDuringSchedulingIgnoredDuringExecution 表示该亲和性规则是强制性的,如果在调度时无法满足,则 Pod 将无法被调度。labelSelector 用于选择需要亲和的 Pod,matchExpressions 定义了选择条件,这里表示选择所有具有 app: cache 标签的 Pod。topologyKey 指定了亲和性的范围,这里表示 Node 的 hostname,也就是说,web-app Pod 必须与具有 app: cache 标签的 Pod 部署在同一个 Node 上。

再来看一个使用 Pod 反亲和性的例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  selector:
    matchLabels:
      app: web-app
  replicas: 3
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: nginx
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-app
            topologyKey: kubernetes.io/hostname

这个 YAML 文件与上面的例子类似,只是将 podAffinity 替换为了 podAntiAffinity,表示 Pod 反亲和性规则。这里表示 web-app Pod 不应该与具有 app: web-app 标签的 Pod 部署在同一个 Node 上,也就是同一个应用的多个实例应该分散部署在不同的 Node 上。

2. Node 选择器:指哪打哪,精准定位

Node 选择器(Node Selector)允许我们指定 Pod 只能部署在具有特定标签(Label)的 Node 上。这就像你想住在带花园的别墅,只需要告诉“房屋中介”:“我要带花园的!”

首先,我们需要给 Node 打上标签:

kubectl label nodes <node-name> disktype=ssd

上面的命令给名为 <node-name> 的 Node 打上了 disktype=ssd 的标签。

然后,我们可以在 Pod 的 YAML 文件中使用 Node Selector:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx
  nodeSelector:
    disktype: ssd

上面的 YAML 文件指定了 my-pod 只能部署在具有 disktype=ssd 标签的 Node 上。

3. 污点与容忍度:门槛与通行证

污点(Taint)用于标记 Node,表示该 Node 不适合运行某些 Pod。而容忍度(Toleration)则用于 Pod,表示该 Pod 可以容忍某些 Taint。这就像某些“豪宅”只允许特定身份的人入住,而你正好拥有可以“容忍”这些“特殊要求”的“通行证”。

首先,我们需要给 Node 添加污点:

kubectl taint nodes <node-name> special=true:NoSchedule

上面的命令给名为 <node-name> 的 Node 添加了一个名为 special=true:NoSchedule 的污点。NoSchedule 表示如果没有容忍该污点的 Pod,则该 Node 不会被调度任何 Pod。

然后,我们可以在 Pod 的 YAML 文件中使用容忍度:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx
  tolerations:
  - key: "special"
    operator: "Equal"
    value: "true"
    effect: "NoSchedule"

上面的 YAML 文件指定了 my-pod 可以容忍 special=true:NoSchedule 的污点,因此可以被调度到具有该污点的 Node 上。

4. 扩展调度器:私人定制,量身打造

扩展调度器(Extender)是一种更高级的扩展方式,允许我们编写自定义的调度器程序,与默认的 Scheduler 协同工作。这就像你觉得默认的“房屋中介”不够专业,自己找了一个更厉害的“私人顾问”。

扩展调度器通常需要实现以下几个接口:

  • Filter: 用于过滤不满足条件的 Node,类似于预选阶段。
  • Prioritize: 用于对满足条件的 Node 进行打分,类似于优选阶段。
  • Bind: 用于将 Pod 绑定到 Node 上,类似于绑定阶段。

编写扩展调度器需要一定的编程基础,这里就不展开讲解了。

5. Scheduling Framework:灵活插拔,自由组合

Kubernetes 1.19 引入了 Scheduling Framework,它是一种更加灵活、可插拔的调度器扩展机制。通过 Scheduling Framework,我们可以编写自定义的调度插件,插入到 Scheduler 的各个阶段,实现更精细化的调度控制。这就像你把“房屋中介”的各个环节都进行了定制,打造了一个完全符合你需求的“专属服务”。

Scheduling Framework 定义了以下几个扩展点:

  • QueueSort: 用于对调度队列中的 Pod 进行排序。
  • PreFilter: 用于在过滤 Node 之前执行一些预处理操作。
  • Filter: 用于过滤不满足条件的 Node。
  • PostFilter: 用于在过滤 Node 之后执行一些后处理操作。
  • PreScore: 用于在对 Node 进行打分之前执行一些预处理操作。
  • Score: 用于对 Node 进行打分。
  • Reserve: 用于在将 Pod 绑定到 Node 之前预留资源。
  • Permit: 用于在将 Pod 绑定到 Node 之前进行权限检查。
  • PreBind: 用于在将 Pod 绑定到 Node 之前执行一些预处理操作。
  • Bind: 用于将 Pod 绑定到 Node 上。
  • PostBind: 用于在将 Pod 绑定到 Node 之后执行一些后处理操作。

通过实现不同的扩展点,我们可以自定义 Scheduler 的行为,满足各种复杂的调度需求。

三、自定义调度算法的“葵花宝典”:从入门到精通

了解了 Scheduler 的扩展机制,接下来咱们来聊聊如何自定义调度算法。

自定义调度算法的步骤大致如下:

  1. 确定调度目标: 首先,我们需要明确我们想要实现什么样的调度目标。例如,我们想要优先将 Pod 调度到具有空闲 GPU 资源的 Node 上,或者我们想要将同一个应用的多个实例分散部署在不同的可用区(Availability Zone)上。
  2. 选择合适的扩展方式: 根据调度目标,选择合适的扩展方式。如果只是简单的亲和性或反亲和性需求,可以使用 Pod 亲和性与反亲和性。如果需要更精细化的控制,可以使用扩展调度器或 Scheduling Framework。
  3. 编写自定义调度逻辑: 根据选择的扩展方式,编写自定义的调度逻辑。如果是使用扩展调度器或 Scheduling Framework,需要编写相应的代码。
  4. 部署和配置: 将自定义的调度器程序部署到 Kubernetes 集群中,并配置 Scheduler 使用自定义的调度器。
  5. 测试和验证: 测试自定义的调度算法是否能够正常工作,并验证是否能够达到预期的调度目标。

这里以一个简单的例子来说明如何使用 Scheduling Framework 自定义调度算法,实现优先将 Pod 调度到具有空闲 GPU 资源的 Node 上。

  1. 编写调度插件:
package main

import (
    "context"
    "fmt"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/kubernetes/pkg/scheduler/framework"
    "k8s.io/klog/v2"
    "sigs.k8s.io/scheduler-plugins/pkg/apis/config"
)

const (
    Name = "GpuPriority"
)

type GpuPriority struct {
    handle framework.Handle
}

var _ framework.ScorePlugin = &GpuPriority{}

func (gp *GpuPriority) Name() string {
    return Name
}

func (gp *GpuPriority) Score(ctx context.Context, state *framework.CycleState, pod *corev1.Pod, nodeName string) (int64, *framework.Status) {
    nodeInfo, err := gp.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
    if err != nil {
        return 0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q: %v", nodeName, err))
    }

    // 获取 Node 的 GPU 资源信息,这里假设 GPU 资源以 annotation 的形式存在
    gpuCount := getNodeGpuCount(nodeInfo.Node())
    gpuUsage := getNodeGpuUsage(nodeInfo.Node())

    // 计算空闲 GPU 资源数量
    freeGpu := gpuCount - gpuUsage

    // 空闲 GPU 资源越多,得分越高
    score := int64(freeGpu)

    klog.V(4).Infof("Node %s has free GPU: %d, score: %d", nodeName, freeGpu, score)

    return score, nil
}

func (gp *GpuPriority) ScoreExtensions() framework.ScoreExtensions {
    return nil
}

func NewGpuPriority(obj runtime.Object, handle framework.Handle) (framework.Plugin, error) {
    klog.V(3).Infof("Creating GpuPriority plugin")
    args := obj.(*config.GpuPriorityArgs)
    klog.V(3).Infof("GpuPriority args: %+v", *args)
    return &GpuPriority{
        handle: handle,
    }, nil
}

// Helper functions to get GPU count and usage from Node annotations
func getNodeGpuCount(node *corev1.Node) int {
    // TODO: implement logic to retrieve GPU count from Node annotations
    // Example:
    // if val, ok := node.ObjectMeta.Annotations["gpu-count"]; ok {
    //     count, err := strconv.Atoi(val)
    //     if err == nil {
    //         return count
    //     }
    // }
    return 0 // Default to 0 if annotation is not found or invalid
}

func getNodeGpuUsage(node *corev1.Node) int {
    // TODO: implement logic to retrieve GPU usage from Node annotations
    // Example:
    // if val, ok := node.ObjectMeta.Annotations["gpu-usage"]; ok {
    //     usage, err := strconv.Atoi(val)
    //     if err == nil {
    //         return usage
    //     }
    // }
    return 0 // Default to 0 if annotation is not found or invalid
}

上面的 Go 代码定义了一个名为 GpuPriority 的调度插件,它实现了 ScorePlugin 接口,用于对 Node 进行打分。Score 函数会获取 Node 的 GPU 资源信息,计算空闲 GPU 资源数量,并将其作为 Node 的得分。空闲 GPU 资源越多,得分越高。

注意: 上面的代码只是一个示例,实际应用中需要根据具体的 GPU 资源管理方式,修改 getNodeGpuCountgetNodeGpuUsage 函数,从 Node 的 annotations 或其他地方获取 GPU 资源信息。

  1. 注册调度插件:

需要在 Scheduler 的配置文件中注册该插件:

apiVersion: kubescheduler.config.k8s.io/v1beta2
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler
    plugins:
      Score:
        enabled:
        - name: GpuPriority
          weight: 1

上面的 YAML 文件指定了在 Score 阶段启用 GpuPriority 插件,并设置权重为 1。

  1. 部署和配置:

将编译好的调度插件部署到 Kubernetes 集群中,并重启 Scheduler。

  1. 测试和验证:

创建一个需要 GPU 资源的 Pod,并观察 Pod 是否被调度到具有空闲 GPU 资源的 Node 上。

四、总结:让 Pod 住进“豪宅”不是梦!

通过 Kubernetes 提供的各种扩展机制,我们可以自定义调度算法,满足 Pod 的各种“任性”需求,让它们住进心仪的“豪宅”。

当然,自定义调度算法并不是一件容易的事情,需要一定的编程基础和对 Kubernetes 调度器的深入了解。但是,只要你掌握了“葵花宝典”,勤加练习,相信你一定能够成为一名优秀的 Kubernetes 调度专家!

最后,希望这篇文章能够帮助你更好地理解 Kubernetes 调度器的扩展与自定义调度算法。如果有什么问题,欢迎在评论区留言,我们一起交流学习!

祝大家玩转 Kubernetes,让 Pod 都住进“豪宅”!🎉

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注