介绍 kube-scheduler-simulator

Kubernetes 调度器是控制平面中一个至关重要的组件,它决定了 Pod 将在哪个节点上运行。因此,任何使用 Kubernetes 的人都依赖于调度器。

kube-scheduler-simulator 是 Kubernetes 调度器的一个**模拟器**,它最初是我(Kensei Nakada)作为 Google Summer of Code 2021 项目开发的,后来收到了许多贡献。这个工具允许用户仔细检查调度器的行为和决策。

它对于那些使用调度约束(例如,Pod 间亲和性)的普通用户以及用自定义插件扩展调度器的专家都很有用。

动机

调度器通常像一个黑盒,由许多插件组成,每个插件都从其独特的角度为调度决策过程做出贡献。由于它考虑了众多因素,理解其行为可能具有挑战性。

即使在一个简单的测试集群中,Pod 看起来被正确调度了,它也可能是基于与预期不同的计算结果被调度的。当部署到大型生产环境时,这种差异可能导致意想不到的调度结果。

此外,测试调度器是一个复杂的挑战。在一个真实的集群中执行的操作模式有无数种,使得用有限数量的测试来预测每一种情况变得不可行。更多时候,只有在调度器部署到实际集群中时才会发现错误。实际上,即使是上游的 kube-scheduler,许多错误也是在发布后由用户发现的。

拥有一个用于测试调度器(或者实际上是任何 Kubernetes 控制器)的开发或沙箱环境是一种常见的做法。然而,这种方法无法捕捉到生产集群中可能出现的所有潜在场景,因为开发集群通常要小得多,并且在工作负载大小和扩展动态方面存在显著差异。它永远不会经历与生产环境完全相同的使用情况或表现出相同的行为。

kube-scheduler-simulator 旨在解决这些问题。它使用户能够测试他们的调度约束、调度器配置和自定义插件,同时检查调度决策的每个详细部分。它还允许用户创建一个模拟的集群环境,在那里他们可以使用与生产集群相同的资源来测试他们的调度器,而不会影响实际的工作负载。

kube-scheduler-simulator 的特性

kube-scheduler-simulator 的核心特性是它能够揭示调度器内部的决策过程。调度器基于调度框架运行,在不同的扩展点使用各种插件,筛选节点(Filter 阶段)、为节点打分(Score 阶段),并最终确定 Pod 的最佳节点。

模拟器允许用户创建 Kubernetes 资源,并观察每个插件如何影响 Pod 的调度决策。这种可见性帮助用户理解调度器的工作原理,并定义适当的调度约束。

Screenshot of the simulator web frontend that shows the detailed scheduling results per node and per extension point

模拟器 Web 前端

在模拟器内部,运行的是一个可调试的调度器,而不是普通的调度器。这个可调试的调度器会将每个调度器插件在每个扩展点的结果输出到 Pod 的注解中,如下面的清单所示,然后 Web 前端会根据这些注解格式化和可视化调度结果。

kind: Pod
apiVersion: v1
metadata:
  # The JSONs within these annotations are manually formatted for clarity in the blog post. 
  annotations:
    kube-scheduler-simulator.sigs.k8s.io/bind-result: '{"DefaultBinder":"success"}'
    kube-scheduler-simulator.sigs.k8s.io/filter-result: >-
      {
        "node-jjfg5":{
            "NodeName":"passed",
            "NodeResourcesFit":"passed",
            "NodeUnschedulable":"passed",
            "TaintToleration":"passed"
        },
        "node-mtb5x":{
            "NodeName":"passed",
            "NodeResourcesFit":"passed",
            "NodeUnschedulable":"passed",
            "TaintToleration":"passed"
        }
      }      
    kube-scheduler-simulator.sigs.k8s.io/finalscore-result: >-
      {
        "node-jjfg5":{
            "ImageLocality":"0",
            "NodeAffinity":"0",
            "NodeResourcesBalancedAllocation":"52",
            "NodeResourcesFit":"47",
            "TaintToleration":"300",
            "VolumeBinding":"0"
        },
        "node-mtb5x":{
            "ImageLocality":"0",
            "NodeAffinity":"0",
            "NodeResourcesBalancedAllocation":"76",
            "NodeResourcesFit":"73",
            "TaintToleration":"300",
            "VolumeBinding":"0"
        }
      }       
    kube-scheduler-simulator.sigs.k8s.io/permit-result: '{}'
    kube-scheduler-simulator.sigs.k8s.io/permit-result-timeout: '{}'
    kube-scheduler-simulator.sigs.k8s.io/postfilter-result: '{}'
    kube-scheduler-simulator.sigs.k8s.io/prebind-result: '{"VolumeBinding":"success"}'
    kube-scheduler-simulator.sigs.k8s.io/prefilter-result: '{}'
    kube-scheduler-simulator.sigs.k8s.io/prefilter-result-status: >-
      {
        "AzureDiskLimits":"",
        "EBSLimits":"",
        "GCEPDLimits":"",
        "InterPodAffinity":"",
        "NodeAffinity":"",
        "NodePorts":"",
        "NodeResourcesFit":"success",
        "NodeVolumeLimits":"",
        "PodTopologySpread":"",
        "VolumeBinding":"",
        "VolumeRestrictions":"",
        "VolumeZone":""
      }      
    kube-scheduler-simulator.sigs.k8s.io/prescore-result: >-
      {
        "InterPodAffinity":"",
        "NodeAffinity":"success",
        "NodeResourcesBalancedAllocation":"success",
        "NodeResourcesFit":"success",
        "PodTopologySpread":"",
        "TaintToleration":"success"
      }      
    kube-scheduler-simulator.sigs.k8s.io/reserve-result: '{"VolumeBinding":"success"}'
    kube-scheduler-simulator.sigs.k8s.io/result-history: >-
      [
        {
            "kube-scheduler-simulator.sigs.k8s.io/bind-result":"{\"DefaultBinder\":\"success\"}",
            "kube-scheduler-simulator.sigs.k8s.io/filter-result":"{\"node-jjfg5\":{\"NodeName\":\"passed\",\"NodeResourcesFit\":\"passed\",\"NodeUnschedulable\":\"passed\",\"TaintToleration\":\"passed\"},\"node-mtb5x\":{\"NodeName\":\"passed\",\"NodeResourcesFit\":\"passed\",\"NodeUnschedulable\":\"passed\",\"TaintToleration\":\"passed\"}}",
            "kube-scheduler-simulator.sigs.k8s.io/finalscore-result":"{\"node-jjfg5\":{\"ImageLocality\":\"0\",\"NodeAffinity\":\"0\",\"NodeResourcesBalancedAllocation\":\"52\",\"NodeResourcesFit\":\"47\",\"TaintToleration\":\"300\",\"VolumeBinding\":\"0\"},\"node-mtb5x\":{\"ImageLocality\":\"0\",\"NodeAffinity\":\"0\",\"NodeResourcesBalancedAllocation\":\"76\",\"NodeResourcesFit\":\"73\",\"TaintToleration\":\"300\",\"VolumeBinding\":\"0\"}}",
            "kube-scheduler-simulator.sigs.k8s.io/permit-result":"{}",
            "kube-scheduler-simulator.sigs.k8s.io/permit-result-timeout":"{}",
            "kube-scheduler-simulator.sigs.k8s.io/postfilter-result":"{}",
            "kube-scheduler-simulator.sigs.k8s.io/prebind-result":"{\"VolumeBinding\":\"success\"}",
            "kube-scheduler-simulator.sigs.k8s.io/prefilter-result":"{}",
            "kube-scheduler-simulator.sigs.k8s.io/prefilter-result-status":"{\"AzureDiskLimits\":\"\",\"EBSLimits\":\"\",\"GCEPDLimits\":\"\",\"InterPodAffinity\":\"\",\"NodeAffinity\":\"\",\"NodePorts\":\"\",\"NodeResourcesFit\":\"success\",\"NodeVolumeLimits\":\"\",\"PodTopologySpread\":\"\",\"VolumeBinding\":\"\",\"VolumeRestrictions\":\"\",\"VolumeZone\":\"\"}",
            "kube-scheduler-simulator.sigs.k8s.io/prescore-result":"{\"InterPodAffinity\":\"\",\"NodeAffinity\":\"success\",\"NodeResourcesBalancedAllocation\":\"success\",\"NodeResourcesFit\":\"success\",\"PodTopologySpread\":\"\",\"TaintToleration\":\"success\"}",
            "kube-scheduler-simulator.sigs.k8s.io/reserve-result":"{\"VolumeBinding\":\"success\"}",
            "kube-scheduler-simulator.sigs.k8s.io/score-result":"{\"node-jjfg5\":{\"ImageLocality\":\"0\",\"NodeAffinity\":\"0\",\"NodeResourcesBalancedAllocation\":\"52\",\"NodeResourcesFit\":\"47\",\"TaintToleration\":\"0\",\"VolumeBinding\":\"0\"},\"node-mtb5x\":{\"ImageLocality\":\"0\",\"NodeAffinity\":\"0\",\"NodeResourcesBalancedAllocation\":\"76\",\"NodeResourcesFit\":\"73\",\"TaintToleration\":\"0\",\"VolumeBinding\":\"0\"}}",
            "kube-scheduler-simulator.sigs.k8s.io/selected-node":"node-mtb5x"
        }
      ]      
    kube-scheduler-simulator.sigs.k8s.io/score-result: >-
      {
        "node-jjfg5":{
            "ImageLocality":"0",
            "NodeAffinity":"0",
            "NodeResourcesBalancedAllocation":"52",
            "NodeResourcesFit":"47",
            "TaintToleration":"0",
            "VolumeBinding":"0"
        },
        "node-mtb5x":{
            "ImageLocality":"0",
            "NodeAffinity":"0",
            "NodeResourcesBalancedAllocation":"76",
            "NodeResourcesFit":"73",
            "TaintToleration":"0",
            "VolumeBinding":"0"
        }
      }      
    kube-scheduler-simulator.sigs.k8s.io/selected-node: node-mtb5x

用户还可以将自己的自定义插件扩展器集成到可调试调度器中,并可视化其结果。

这个可调试的调度器也可以独立运行,例如,在任何 Kubernetes 集群或集成测试中。这对于希望测试其插件或在真实集群中以更好的可调试性检查其自定义调度器的自定义插件开发者来说非常有用。

作为更好的开发集群的模拟器

如前所述,通过有限的测试集,不可能预测真实集群中的所有可能场景。通常,用户会在将调度器部署到生产环境之前,在一个小型的开发集群中进行测试,并希望不会出现问题。

模拟器的导入功能提供了一个解决方案,它允许用户在类似生产的环境中模拟部署新版本的调度器,而不会影响他们的实时工作负载。

通过在生产集群和模拟器之间持续同步,用户可以安全地使用其生产集群处理的相同资源来测试新版本的调度器。一旦对其性能有信心,他们就可以进行生产部署,从而降低意外问题的风险。

有哪些使用场景?

  1. 集群用户:检查调度约束(例如 PodAffinity、PodTopologySpread)是否按预期工作。
  2. 集群管理员:评估更改调度器配置后集群的行为。
  3. 调度器插件开发者:测试自定义调度器插件或扩展器,在集成测试或开发集群中使用可调试的调度器,或使用同步功能在类似生产的环境中进行测试。

开始使用

模拟器只需要在机器上安装 Docker;不需要 Kubernetes 集群。

git clone git@github.com:kubernetes-sigs/kube-scheduler-simulator.git
cd kube-scheduler-simulator
make docker_up

然后你可以在 `https://:3000` 访问模拟器的 Web UI。

请访问 kube-scheduler-simulator 仓库了解更多详情!

参与进来

调度器模拟器由 Kubernetes SIG Scheduling 开发。欢迎你的反馈和贡献!

kube-scheduler-simulator 仓库中提出问题或 PR。在 #sig-scheduling Slack 频道上加入讨论。

致谢

模拟器一直由敬业的志愿工程师维护,克服了许多挑战才达到现在的形态。

非常感谢所有了不起的贡献者