介绍 kube-scheduler-simulator

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

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

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

动机

调度器通常看起来像一个黑匣子,由许多插件组成,每个插件都从独特的角度参与调度决策过程。由于它考虑的因素众多,理解其行为可能具有挑战性。

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

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

拥有一个用于测试调度器(或任何 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 的 annotations 中,如下面的 manifest 所示,而 Web 前端则根据这些 annotations 格式化/可视化调度结果。

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

然后,你可以在 http://localhost:3000 访问模拟器的 Web UI。

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

参与贡献

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

kube-scheduler-simulator 仓库提交 issue 或 PR。加入 #sig-scheduling Slack 频道参与讨论。

致谢

模拟器一直由敬业的志愿工程师维护,他们克服了许多挑战才使其达到目前的状态。

特别鸣谢所有出色的贡献者