Kubernetes 中的自定义调度器开发

好嘞!各位观众老爷们,今天咱们不聊那些玄乎的分布式理论,咱们来点实在的——手把手教你打造一个 Kubernetes 的私人定制版调度器!保证让你从懵懂小白,变成调度界的一朵奇葩! 🌸

开场白:Kubernetes 的“红娘”烦恼

话说 Kubernetes 这位老大哥,手底下管着成千上万的 Pod,就像一个拥有无数儿女的大家长。这些 Pod 们都想找到一个舒适的家(节点),但资源有限啊!这时候,就需要一个“红娘”来牵线搭桥,这个“红娘”就是 Kubernetes 的调度器 (Scheduler)。

默认的调度器就像一个经验丰富的媒婆,会根据节点的资源、Pod 的需求、亲和性、反亲和性等等条件,尽力给每个 Pod 安排一个好归宿。但问题来了,这位媒婆太“大众化”了,考虑的都是普适性的需求。

如果你想让你的 Pod 们享受更特殊的待遇,比如:

  • 让某个 Pod 永远跑在 CPU 最强的节点上,享受 VIP 待遇!👑
  • 让某些 Pod 只能跑在特定地区的节点上,实现“地域保护”! 🌍
  • 让某些 Pod 只能跑在安装了特定 GPU 驱动的节点上,发挥硬件优势! 🚀

这个时候,你就需要一个私人定制版的调度器,一个更懂你的“红娘”,来满足你那些稀奇古怪的需求!

第一幕:剧本大纲——了解调度器的前世今生

在开始编写代码之前,我们先来了解一下 Kubernetes 调度器的基本概念,就像导演在开拍之前要先熟悉剧本一样。

  1. 调度器是干嘛的?

    简单来说,调度器就是负责把没有被分配节点的 Pod 绑定到合适的节点上。它的工作流程大致如下:

    • 监听 Pod: 调度器会不断地监听 Kubernetes API Server,寻找那些 spec.nodeName 字段为空的 Pod。
    • 过滤节点: 对于每个待调度的 Pod,调度器会过滤掉那些不符合条件的节点(比如资源不足、不满足亲和性等)。
    • 打分排序: 对剩余的节点进行打分,分数越高表示节点越适合运行该 Pod。
    • 绑定节点: 选择得分最高的节点,并将 Pod 的 spec.nodeName 字段设置为该节点的名称,完成绑定。
  2. 调度器的架构:两步走战略

    Kubernetes 调度器的架构可以分为两个主要阶段:

    • Predicate(预选): 过滤掉不满足条件的节点,这个过程称为预选。
    • Priority(优选): 对剩余的节点进行打分,选出最合适的节点,这个过程称为优选。

    你可以把预选想象成“海选”,把优选想象成“决赛”。

  3. 调度器的扩展方式:插拔式设计

    Kubernetes 调度器提供了灵活的扩展机制,你可以通过以下方式定制调度器的行为:

    • Pod 亲和性和反亲和性(Affinity/Anti-Affinity): 通过在 Pod 的 YAML 文件中设置 affinityantiAffinity 字段,可以控制 Pod 应该运行在哪些节点上,或者避免运行在哪些节点上。这就像给 Pod 贴上“喜欢”和“讨厌”的标签。
    • 节点选择器(Node Selector): 通过在 Pod 的 YAML 文件中设置 nodeSelector 字段,可以指定 Pod 只能运行在具有特定标签的节点上。这就像给 Pod 发出“定向邀请”。
    • 污点和容忍度(Taints/Tolerations): 通过在节点上设置 taint,可以阻止某些 Pod 运行在该节点上。Pod 需要设置相应的 toleration 才能容忍这些污点。这就像给节点设置“门槛”,只有满足条件的 Pod 才能进入。
    • 调度器插件(Scheduler Plugin): 这是我们今天要重点介绍的扩展方式,你可以编写自己的调度器插件,实现更复杂的调度逻辑。这就像给调度器安装一个“外挂”,让它拥有更强大的能力。

第二幕:主角登场——Scheduler Plugin 的魅力

Scheduler Plugin 是 Kubernetes 1.15 版本引入的新特性,它允许你以插件的形式扩展调度器的功能,而无需修改调度器的核心代码。这就像给汽车安装一个涡轮增压器,让它跑得更快!

  1. 插件的种类:各司其职

    Scheduler Plugin 提供了多种类型的插件接口,每种插件负责不同的调度任务:

    插件类型 功能描述
    QueueSort 对调度队列中的 Pod 进行排序,决定 Pod 的调度顺序。
    PreFilter 在过滤节点之前执行,可以用来检查 Pod 是否满足某些前提条件。
    Filter 过滤掉不满足条件的节点,相当于预选阶段。
    PostFilter 在所有节点都被过滤掉之后执行,可以用来进行一些补救措施,比如尝试抢占其他 Pod 的资源。
    PreScore 在对节点进行打分之前执行,可以用来收集一些节点的信息,供打分阶段使用。
    Score 对节点进行打分,相当于优选阶段。
    Reserve 在节点上预留资源,防止其他 Pod 抢占。
    Permit 允许或拒绝 Pod 绑定到节点,可以用来实现更复杂的准入控制。
    PreBind 在 Pod 绑定到节点之前执行,可以用来进行一些准备工作。
    Bind 将 Pod 绑定到节点,如果自定义了 Bind 插件,则默认的绑定逻辑会被覆盖。
    PostBind 在 Pod 绑定到节点之后执行,可以用来进行一些清理工作。
    Unreserve 在预留资源失败时,释放预留的资源。
  2. 插件的开发流程:一步一个脚印

    开发一个 Scheduler Plugin 的流程大致如下:

    • 定义插件的配置: 创建一个 YAML 文件,定义插件的名称、权重、以及其他配置参数。
    • 编写插件的代码: 使用 Go 语言编写插件的代码,实现相应的插件接口。
    • 注册插件: 将插件注册到 Kubernetes 调度器中。
    • 部署插件: 将插件部署到 Kubernetes 集群中。
    • 配置调度器: 修改调度器的配置文件,启用你的插件。

第三幕:实战演练——打造你的专属调度器

现在,让我们通过一个具体的例子,来演示如何开发一个 Scheduler Plugin。

目标: 创建一个名为 NodeAffinity 的插件,该插件可以根据节点的标签,将 Pod 调度到具有特定标签的节点上。例如,我们可以设置一个标签 node-type=gpu,然后让某些 Pod 只能运行在具有该标签的节点上。

  1. 定义插件的配置:

    创建一个名为 node-affinity-plugin.yaml 的文件,内容如下:

    apiVersion: kubescheduler.config.k8s.io/v1beta3
    kind: Configuration
    profiles:
      - schedulerName: custom-scheduler
        plugins:
          Filter:
            enabled:
              - name: NodeAffinity
          Score:
            enabled:
              - name: NodeAffinity
          PreScore:
            enabled:
              - name: NodeAffinity
          QueueSort:
            enabled:
              - name: PrioritySort
          Bind:
            enabled:
              - name: DefaultBinder
    pluginConfig:
      - name: NodeAffinity
        args:
          labelKey: node-type # 指定要匹配的标签的 key
          labelValue: gpu   # 指定要匹配的标签的 value

    这个配置文件指定了:

    • 插件的名称为 NodeAffinity
    • 该插件实现了 FilterScore 接口,分别用于过滤节点和打分。
    • 插件的配置参数包括 labelKeylabelValue,分别指定了要匹配的标签的 key 和 value。
  2. 编写插件的代码:

    创建一个名为 nodeaffinity/nodeaffinity.go 的文件,内容如下:

    package nodeaffinity
    
    import (
        "context"
        "fmt"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/klog/v2"
        "sigs.k8s.io/scheduler-plugins/pkg/apis/config"
        "sigs.k8s.io/scheduler-plugins/pkg/apis/config/validation"
    
        v1 "k8s.io/api/core/v1"
        "k8s.io/kubernetes/pkg/scheduler/framework"
    )
    
    const (
        Name = "NodeAffinity"
    )
    
    var _ framework.PreScorePlugin = &NodeAffinity{}
    var _ framework.FilterPlugin = &NodeAffinity{}
    var _ framework.ScorePlugin = &NodeAffinity{}
    
    // NodeAffinity defines the configuration for the plugin.
    type NodeAffinity struct {
        labelKey   string
        labelValue string
        handle     framework.Handle
    }
    
    // NodeAffinityArgs defines the arguments for the plugin.
    type NodeAffinityArgs struct {
        KubeConfig string `json:"kubeConfig,omitempty"`
        LabelKey   string `json:"labelKey,omitempty"`
        LabelValue string `json:"labelValue,omitempty"`
    }
    
    // New creates a new NodeAffinity plugin.
    func New(obj runtime.Object, handle framework.Handle) (framework.Plugin, error) {
        args := &NodeAffinityArgs{}
        if err := runtime.DefaultUnstructuredConverter.Convert(obj, args, nil); err != nil {
            return nil, err
        }
    
        if err := validation.ValidateNodeAffinityArgs(args); err != nil {
            return nil, fmt.Errorf("invalid arguments: %v", err)
        }
    
        klog.V(2).Infof("Creating NodeAffinity plugin with args: %+v", args)
    
        return &NodeAffinity{
            labelKey:   args.LabelKey,
            labelValue: args.LabelValue,
            handle:     handle,
        }, nil
    }
    
    // Name returns name of the plugin.
    func (pl *NodeAffinity) Name() string {
        return Name
    }
    
    // PreScore is called before scoring.
    func (pl *NodeAffinity) PreScore(ctx context.Context, pod *v1.Pod, nodes []*v1.Node) (*framework.PreScoreData, *framework.Status) {
        klog.V(5).Infof("PreScore: pod %v", pod.Name)
        return nil, nil
    }
    
    // Filter is called to filter out nodes that are not suitable for running the pod.
    func (pl *NodeAffinity) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
        node := nodeInfo.Node()
        if node == nil {
            return framework.NewStatus(framework.Error, "node not found")
        }
    
        labelValue, ok := node.Labels[pl.labelKey]
    
        if !ok || labelValue != pl.labelValue {
            klog.V(5).Infof("Filtering node %v because it does not have label %v=%v", node.Name, pl.labelKey, pl.labelValue)
            return framework.NewStatus(framework.Unschedulable, fmt.Sprintf("node %v does not have label %v=%v", node.Name, pl.labelKey, pl.labelValue))
        }
    
        klog.V(5).Infof("Node %v passed filter", node.Name)
        return framework.NewStatus(framework.Success, "")
    }
    
    // Score is called to score nodes that passed the filter.
    func (pl *NodeAffinity) 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.NewStatus(framework.Error, fmt.Sprintf("getting node %q from snapshot: %v", nodeName, err))
        }
    
        node := nodeInfo.Node()
        if node == nil {
            return 0, framework.NewStatus(framework.Error, "node not found")
        }
    
        labelValue, ok := node.Labels[pl.labelKey]
    
        if !ok || labelValue != pl.labelValue {
            klog.V(5).Infof("Scoring node %v with 0 because it does not have label %v=%v", node.Name, pl.labelKey, pl.labelValue)
            return 0, nil
        }
    
        klog.V(5).Infof("Scoring node %v with 100", node.Name)
        return 100, nil
    }
    
    // ScoreExtensions of the Score plugin.
    func (pl *NodeAffinity) ScoreExtensions() framework.ScoreExtensions {
        return nil
    }

    这段代码实现了 FilterScore 接口:

    • Filter 函数会检查节点是否具有指定的标签,如果没有,则返回 Unschedulable 状态,表示该节点不适合运行 Pod。
    • Score 函数会根据节点是否具有指定的标签进行打分,如果具有,则返回 100 分,否则返回 0 分。
  3. 编译插件:

    go build -buildmode=plugin -o nodeaffinity.so nodeaffinity/nodeaffinity.go
  4. 部署插件:

    将编译好的 nodeaffinity.so 文件放到 Kubernetes 调度器可以访问的目录中。通常,这个目录是 /plugins

  5. 配置调度器:

    修改 Kubernetes 调度器的配置文件,启用你的插件。你需要修改 kube-scheduler-config.yaml 文件,添加以下内容:

    apiVersion: kubescheduler.config.k8s.io/v1beta3
    kind: Configuration
    profiles:
      - schedulerName: custom-scheduler
        plugins:
          Filter:
            enabled:
              - name: NodeAffinity
          Score:
            enabled:
              - name: NodeAffinity
          PreScore:
            enabled:
              - name: NodeAffinity
          QueueSort:
            enabled:
              - name: PrioritySort
          Bind:
            enabled:
              - name: DefaultBinder
    pluginConfig:
      - name: NodeAffinity
        args:
          labelKey: node-type
          labelValue: gpu

    确保 profiles 中定义了 schedulerName: custom-scheduler, 并且plugins 包含了 NodeAffinity。 同时在 pluginConfig 中配置了 NodeAffinity 的参数。

    重要提示: 你需要根据你的实际情况修改 labelKeylabelValue 的值。

  6. 重启调度器:

    重启 Kubernetes 调度器,使配置生效。

  7. 验证插件:

    创建一个 Pod 的 YAML 文件,指定 schedulerNamecustom-scheduler,并设置 nodeSelector 字段,指定要匹配的标签。

    apiVersion: v1
    kind: Pod
    metadata:
      name: gpu-pod
    spec:
      schedulerName: custom-scheduler
      containers:
        - name: gpu-container
          image: nginx
      nodeSelector:
        node-type: gpu

    将 Pod 部署到 Kubernetes 集群中,观察 Pod 是否被调度到具有 node-type=gpu 标签的节点上。

    kubectl apply -f pod.yaml
    kubectl describe pod gpu-pod

    如果 Pod 成功被调度到具有 node-type=gpu 标签的节点上,那么恭喜你,你的自定义调度器插件已经成功生效了! 🎉

第四幕:画龙点睛——总结与展望

通过上面的例子,我们学习了如何开发一个 Kubernetes 调度器插件,实现了根据节点标签进行调度的功能。

当然,这只是一个简单的例子,你可以根据自己的需求,开发更复杂的调度器插件,比如:

  • 根据节点的 CPU 利用率进行调度。
  • 根据节点的 GPU 资源进行调度。
  • 根据节点的地理位置进行调度。
  • 等等…

Kubernetes 调度器插件为你提供了无限的可能性,让你能够定制一个更智能、更高效的调度系统。

最后,送给大家一句名言:

“代码是最好的魔术!” 🧙‍♂️

希望这篇文章能够帮助你打开 Kubernetes 调度器的大门,让你在云原生世界里自由驰骋! 🚀

发表回复

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