K8s 调度器扩展与自定义调度策略

各位Kubernetes世界的探险家们,早上好!我是你们今天的向导,一位在云端和代码海洋中漂流多年的老水手。今天,我们将扬帆起航,共同探索 Kubernetes 调度器扩展与自定义调度策略这片神秘而充满力量的领域。

请允许我先抛出一个灵魂拷问:你是否曾经面对过这样的场景?你的 Kubernetes 集群规模越来越大,默认的调度器就像一位兢兢业业的老管家,勤勤恳恳,但总感觉少了点个性,无法完美满足你那“刁钻”的需求,比如:

  • 某个应用对GPU资源情有独钟,必须和特定的GPU节点“喜结良缘”;
  • 某些应用需要极低的延迟,恨不得住在数据中心隔壁;
  • 为了省钱,你想把任务“偷偷摸摸”地塞进那些空闲率较高的节点里。

如果你有以上种种需求,那么恭喜你,你已经走在了自定义 Kubernetes 调度策略的道路上!

第一章: 默认调度器的爱与愁 💖💔

Kubernetes 的默认调度器,就像一位经验丰富的媒人,它会根据 Pod 的资源需求和节点的可用资源,为 Pod 找到最合适的“归宿”。它考虑的因素包括:

  • 资源需求 (Resource Requirements): CPU、内存、GPU 等资源是硬性指标,调度器会确保节点有足够的资源满足 Pod 的需求。
  • 节点亲和性 (Node Affinity) 和反亲和性 (Node Anti-Affinity): 你可以告诉调度器,某个 Pod 喜欢或讨厌哪些节点。
  • 污点 (Taints) 和容忍度 (Tolerations): 这是节点拒绝某些 Pod 进入的“护城河”,而 Pod 需要有相应的“通行证”才能进入。
  • 节点选择器 (Node Selector) 和标签 (Labels): 通过标签和选择器,你可以精确地指定 Pod 运行在哪些节点上。

默认调度器功能强大,能够应对大多数场景。但它也有局限性:

  • 不够智能: 默认调度器无法感知业务逻辑,无法根据应用的特性进行优化。
  • 缺乏灵活性: 默认调度器的调度策略是固定的,无法根据实际需求进行调整。
  • 无法处理复杂场景: 对于一些复杂的调度需求,例如跨地域调度、异构资源调度等,默认调度器就显得力不从心了。

第二章: 揭开调度器扩展的神秘面纱 🎭

为了弥补默认调度器的不足,Kubernetes 提供了强大的调度器扩展机制,让我们可以像给汽车改装引擎一样,为调度器注入新的活力。

Kubernetes 提供了两种主要的调度器扩展方式:

  • 调度器插件 (Scheduler Plugins): 这是 Kubernetes 官方推荐的扩展方式,它允许你编写自定义的插件,嵌入到默认调度器的调度流程中。
  • 自定义调度器 (Custom Scheduler): 你可以完全抛弃默认调度器,编写一个全新的调度器,接管集群的调度工作。

2.1 调度器插件:小身材,大能量 💪

调度器插件就像乐高积木,你可以根据自己的需求,选择合适的积木,拼装成你想要的调度器。Kubernetes 定义了一系列的插件接口,每个接口负责调度流程的不同阶段:

插件接口 功能描述
QueueSort 负责对调度队列中的 Pod 进行排序,决定 Pod 的调度顺序。例如,你可以实现一个优先级队列,让重要的 Pod 优先调度。
PreFilter 在过滤节点之前执行,可以预先检查 Pod 是否满足某些条件。例如,你可以检查 Pod 是否需要特定的硬件加速器。
Filter 负责过滤不满足条件的节点。这是调度流程中最核心的环节,你可以根据自己的策略,过滤掉不合适的节点。例如,你可以根据节点的负载、健康状况、地理位置等因素进行过滤。
PostFilter 在过滤节点之后执行,可以对过滤后的节点进行一些额外的处理。例如,你可以记录节点的过滤结果,用于后续的分析和优化。
PreScore 在打分之前执行,可以预先计算一些打分所需的指标。例如,你可以计算节点与 Pod 之间的网络延迟。
Score 负责对通过过滤的节点进行打分,决定 Pod 应该调度到哪个节点。你可以根据自己的策略,为不同的节点分配不同的分数。例如,你可以根据节点的资源利用率、网络延迟、地理位置等因素进行打分。
NormalizeScore 负责对节点的得分进行归一化,防止某些节点因为某些指标的绝对值过大而占据优势。
Reserve 在绑定 Pod 之前执行,可以预留节点资源。例如,你可以预留节点上的 CPU 和内存,防止其他 Pod 抢占资源。
Permit 在绑定 Pod 之前执行,可以决定是否允许 Pod 绑定到节点。例如,你可以检查节点是否满足某些安全策略。
PreBind 在绑定 Pod 之前执行,可以执行一些预处理操作。例如,你可以配置 Pod 的网络策略。
Bind 负责将 Pod 绑定到节点。这是调度流程的最后一步,也是最关键的一步。
PostBind 在绑定 Pod 之后执行,可以执行一些清理操作。例如,你可以释放预留的资源。
Unreserve 在 Pod 调度失败或被删除时执行,可以释放预留的资源。

通过实现这些插件接口,你可以灵活地定制调度流程,满足各种复杂的调度需求。

2.2 自定义调度器:从零开始,掌控全局 🚀

如果你觉得插件的方式不够灵活,或者你想实现一些非常特殊的调度策略,那么你可以选择编写一个自定义调度器。自定义调度器就像一位艺术家,你可以自由地挥洒你的创意,创造出独一无二的调度策略。

编写自定义调度器需要更多的精力和知识,你需要深入了解 Kubernetes 的内部机制,包括:

  • Kubernetes API: 你需要使用 Kubernetes API 与集群进行交互,获取 Pod 和节点的信息。
  • 调度算法: 你需要设计自己的调度算法,决定 Pod 如何选择节点。
  • 事件处理: 你需要监听 Kubernetes 事件,例如 Pod 创建、节点状态变化等,并做出相应的处理。

虽然编写自定义调度器难度较高,但它可以让你完全掌控调度流程,实现各种天马行空的调度策略。

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

理论知识再丰富,不如动手实践一番。接下来,我们将通过一个简单的例子,演示如何使用调度器插件来实现一个自定义的调度策略:节点亲和性增强版:Pod必须调度到标签为“dedicated=true”的节点上。

3.1 插件代码:

package main

import (
    "context"
    "fmt"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/kubernetes/pkg/scheduler/framework"
    "k8s.io/klog/v2"
)

const (
    Name = "DedicatedNode"
)

// DedicatedNode 插件的配置结构体
type DedicatedNodeArgs struct {
    LabelKey string `json:"labelKey,omitempty"`
}

// DedicatedNode 插件
type DedicatedNode struct {
    args DedicatedNodeArgs
    handle framework.Handle
}

// 插件的构造函数
func NewDedicatedNode(configuration runtime.Object, handle framework.Handle) (framework.Plugin, error) {
    args := DedicatedNodeArgs{
        LabelKey: "dedicated", // 默认值
    }

    // 将配置反序列化到 DedicatedNodeArgs 结构体中
    if err := runtime.DefaultUnstructuredConverter.Convert(configuration, &args, nil); err != nil {
        return nil, fmt.Errorf("converting configuration to DedicatedNodeArgs: %w", err)
    }

    klog.V(2).InfoS("Creating DedicatedNode plugin", "args", args)

    return &DedicatedNode{
        args: args,
        handle: handle,
    }, nil
}

// Filter 插件接口的实现
func (d *DedicatedNode) Filter(ctx context.Context, state *framework.CycleState, pod *corev1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    node := nodeInfo.Node()
    if node == nil {
        return framework.NewStatus(framework.Error, "node not found")
    }

    labelValue, ok := node.Labels[d.args.LabelKey]
    if !ok || labelValue != "true" {
        klog.V(3).InfoS("Filtering node", "node", node.Name, "pod", pod.Name)
        return framework.NewStatus(framework.Unschedulable, fmt.Sprintf("node %s doesn't have label %s=true", node.Name, d.args.LabelKey))
    }

    klog.V(3).InfoS("Node passes filter", "node", node.Name, "pod", pod.Name)
    return framework.NewStatus(framework.Success, "")
}

// 插件的名称
func (d *DedicatedNode) Name() string {
    return Name
}

3.2 插件注册:

我们需要将插件注册到调度器中,让调度器知道我们的插件的存在。这需要在调度器的配置文件中进行配置。

apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler
    plugins:
      Filter:
        enabled:
          - name: DedicatedNode
        disabled:
          - name: "*" # Disable all other filters, for demonstration purposes.  In a real setup, you'd likely want to selectively disable or reorder them.
    pluginConfig:
      - name: DedicatedNode
        args:
          labelKey: dedicated

3.3 部署插件:

将编译好的插件二进制文件放到所有 Kubernetes Master 节点的指定目录下(例如 /opt/kubernetes/scheduler-plugins),并将配置文件更新到调度器。

3.4 验证结果:

创建一个 Pod,不指定任何节点亲和性,然后观察 Pod 的调度结果。如果 Pod 被调度到标签为“dedicated=true”的节点上,那么恭喜你,你的插件已经成功生效了!🎉

第四章: 调度策略的艺术与哲学 🎨🧠

自定义调度策略不仅仅是技术活,更是一门艺术,一门哲学。你需要深入理解你的业务需求,才能设计出真正有效的调度策略。

以下是一些设计调度策略的原则:

  • 目标明确: 你的调度策略想要解决什么问题?提高资源利用率?降低延迟?还是满足特定的业务需求?
  • 简单有效: 尽量选择简单的调度策略,避免过度复杂,增加维护成本。
  • 可观测性: 你的调度策略是否容易监控和调试?你需要收集哪些指标来评估调度策略的效果?
  • 可扩展性: 你的调度策略是否容易扩展?当业务需求发生变化时,你是否可以轻松地调整调度策略?

第五章: 避坑指南:那些年,我们踩过的坑 🕳️

自定义调度策略虽然强大,但也充满陷阱。以下是一些常见的坑,希望能帮助你避开:

  • 过度优化: 不要为了追求极致的性能而过度优化调度策略,这可能会导致调度算法过于复杂,难以维护。
  • 缺乏监控: 如果你没有监控调度策略的效果,你可能无法发现潜在的问题,甚至适得其反。
  • 忽略兼容性: 你的调度策略是否与 Kubernetes 的其他组件兼容?例如,你的调度策略是否会影响自动伸缩?
  • 安全漏洞: 你的调度策略是否存在安全漏洞?例如,你的调度策略是否会允许恶意 Pod 占用关键资源?

总结:

Kubernetes 调度器扩展与自定义调度策略是一片充满机遇和挑战的领域。通过掌握这些技术,你可以为你的 Kubernetes 集群注入新的活力,让你的应用更好地运行在云端。

希望今天的分享能够帮助你在 Kubernetes 的世界里走得更远,飞得更高!🚀

现在,请允许我接受各位探险家的提问,让我们一起深入探讨 Kubernetes 调度的奥秘!🤔

发表回复

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