使用 Descheduler 重新平衡 KubeVirt 虚拟机

Alauda Container Platform Descheduler 插件可以请求驱逐 virt-launcher Pod。KubeVirt 会拦截该请求,并对 VMI 执行实时迁移,而不是停止 VM。请将其用作次要且有界的重新平衡机制。对于发生故障或需要有意维护的节点,请改为遵循 KubeVirt 节点维护。

本页面介绍 ACP ModulePlugin 软件包。该软件包通过自身的资源进行配置——策略 ConfigMap 和 CronJob(或 Deployment)——不会安装 descheduler CRD;也不存在 operator 级别的 API 可用于对其进行配置。

以下操作步骤已在 ACP 4.4(Kubernetes v1.35)以及 KubeVirt v1.8.4(HCO 1.18.1)上完成验证,使用的 Descheduler 软件包构建版本默认渲染 EvictionsInBackground 功能门(任何新于 v0.36.0-4 的软件包版本)。请从运行中的集群读取已安装的版本;软件包和 KubeVirt 独立演进。

Descheduler 驱逐如何变为实时迁移

  1. Descheduler 选择一个 virt-launcher Pod,并调用 Eviction API。Eviction 对象携带注解 requested-by: sigs.k8s.io/descheduler。
  2. KubeVirt 会在创建 每个 virt-launcher Pod 时添加注解 descheduler.alpha.kubernetes.io/request-evict-only: "",无论驱逐策略是什么。Descheduler 只会对携带此注解的 Pod 应用其后台驱逐处理。
  3. KubeVirt webhook virt-launcher-eviction-interceptor.kubevirt.io 根据 VMI 的有效驱逐策略决定后续行为: 如果已设置,则使用 VMI 的 spec.evictionStrategy;否则使用 KubeVirt 资源中的集群默认值(spec.configuration.evictionStrategy,由 HCO 根据 HyperConverged.spec.evictionStrategy 渲染)。
有效策略VMI 可实时迁移Webhook 响应结果
LiveMigrate是HTTP 429 Eviction triggered evacuation of VMI "<ns>/<name>"VMI 被标记为待疏散并执行实时迁移
LiveMigrate否HTTP 429 ... is not live-migratable不执行任何操作;Descheduler 记录错误并在下一周期重试
LiveMigrateIfPossible是HTTP 429 Eviction triggered evacuation ...VMI 被标记为待疏散并执行实时迁移
LiveMigrateIfPossible否允许Pod 被删除;VM 在另一个节点上重启
None 或未设置任意允许Pod 被删除;VM 在另一个节点上重启

当 VMI 被标记为待疏散时,KubeVirt 会在 VMI 上设置 status.evacuationNodeName,向 VMI 添加注解 kubevirt.io/eviction-source: descheduler,并创建一个名为 kubevirt-evacuation-<suffix> 的 VirtualMachineInstanceMigration,其带有注解 kubevirt.io/evacuationMigration: <source-node>。迁移处于等待状态期间,KubeVirt 还会使用 descheduler.alpha.kubernetes.io/eviction-in-progress: "true" 对源 Pod 添加注解。迁移完成后, virt-handler 会移除 eviction-source 并清除 evacuationNodeName,因此这两个标记仅在疏散进行期间可见。

启用 EvictionsInBackground=true 后,Descheduler 会将该特定的 429 视为成功的驱逐请求:它会记录 Eviction in background assumed,将该请求计入 maxNoOfPodsToEvict*,并记住该 Pod。在下一周期中,它会看到 eviction-in-progress 注解,并记录 Eviction in background already requested (ignoring),而不是再次发起请求。没有该功能门时,429 会被记录为驱逐失败,并且同一个 launcher 可能在每个周期再次被提名。

WARNING

当 VM 的有效策略为 None,或为 LiveMigrateIfPossible 且 VMI 不可迁移时,Descheduler 驱逐其 launcher 后,VM 会重启而不会迁移。在允许驱逐 VM launcher 之前,请验证其策略。

先决条件

软件包渲染功能门

当前软件包会根据自身的 values 渲染功能门;无需管理员配置。在运行 VM 的业务集群上验证:

kubectl -n descheduler get cronjob descheduler \
  -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[0].args}{"\n"}'

预期输出:

["--policy-config-file=/policy-dir/policy.yaml","--feature-gates=EvictionsInBackground=true","--v=3"]

如果缺少该参数,说明已安装的软件包早于此变更: 升级 Alauda Container Platform Descheduler 插件。不要手动编辑生成的 CronJob 或 Deployment。要查看下文使用的 Eviction in background ... 日志行,还需要 --v=3 详细程度。

VM 使用迁移驱逐策略时支持实时迁移

VM 必须使用可从目标节点访问的共享存储(例如 RWX 块存储),报告 LiveMigratable=True,并将 LiveMigrate 或 LiveMigrateIfPossible 作为有效驱逐策略。 ACP 随附的 HCO 示例会设置 spec.evictionStrategy: LiveMigrateIfPossible;在 ACP 上,使用空 spec 创建的 HyperConverged 默认为 None。检查这两个层级:

kubectl -n kubevirt get kubevirt kubevirt-kubevirt-hyperconverged \
  -o jsonpath='{.spec.configuration.evictionStrategy}{"\n"}'
kubectl get vmi <vmi-name> -n <vm-namespace> \
  -o jsonpath='{.spec.evictionStrategy} {.status.conditions[?(@.type=="LiveMigratable")].status}{"\n"}'

VMI 值会在创建 VMI 时固定。更改 HyperConverged.spec.evictionStrategy 会立即传播到 KubeVirt 资源, 但只会影响之后创建的 VMI;现有 VMI 会保留其记录的值(例如 None),直到 VM 重启。

发现插件

按照工作负载重新平衡(Descheduler)中的说明,从 管理员 > Marketplace > 集群插件 安装 Alauda Container Platform Descheduler。

ModuleInfo 仅由 global(管理)集群提供;在业务集群上运行 该命令会失败,并显示 the server doesn't have a resource type "moduleinfo"。策略 ConfigMap 和 CronJob 位于业务集群上,位于软件包创建的 descheduler 命名空间中。

在 global 集群上:

kubectl get moduleinfo -l cpaas.io/module-name=descheduler \
  -o custom-columns='NAME:.metadata.name,CLUSTER:.metadata.labels.cpaas\.io/cluster-name,PHASE:.status.phase,VERSION:.spec.version'

在业务集群上:

kubectl -n descheduler get cronjob,configmap,job -l app.kubernetes.io/name=descheduler
kubectl -n descheduler get configmap descheduler -o jsonpath='{.data.policy\.yaml}'

该策略是原始的 descheduler/v1alpha2 DeschedulerPolicy:使用本页面所述的上游插件和参数进行配置;它没有预定义的 KubeVirt 配置文件或解除调度模式设置。 (LowNodeUtilization.args.evictionLimits.node 是常规的上游字段,可以与顶层 maxNoOfPodsToEvict* 限制结合使用。)

为什么默认策略永远不会迁移 VM

该软件包提供一个 default 配置文件,其 DefaultEvictor 配置为:

- name: DefaultEvictor
  args:
    podProtections:
      defaultDisabled:
        - PodsWithLocalStorage
      extraEnabled:
        - PodsWithPVC
  • PodsWithPVC 会保护挂载 PersistentVolumeClaim 的每个 Pod。因此,PVC 支持的 VM 的所有 virt-launcher Pod 都会被跳过,所以默认策略对 VM 是安全的,但无法对其进行再平衡。
  • PodsWithLocalStorage 必须保持禁用:每个 virt-launcher Pod 都包含多个 emptyDir 卷(private、public、sockets、ephemeral-disks、container-disks、hotplug-disks,……)。重新启用该保护后,即使没有 PVC,launcher 也无法被驱逐。

要对 VM 进行再平衡,必须在限定为目标 VM 的配置文件中,从 extraEnabled 中移除 PodsWithPVC。

配置有界 VM 再平衡策略

编辑 ConfigMap 是受支持的配置路径。ACP 会将此编辑记录为插件命名空间中的 ResourcePatch(API server 会打印 Warning: The operation is allowed, and ResourcePatch rp-<suffix> is created),因此图表协调不会覆盖此更改。每次 CronJob 运行时都会在启动时读取 ConfigMap;无需重启。

首先备份生成的策略:

kubectl -n descheduler get configmap descheduler -o yaml \
  > descheduler-policy.before-vm-rebalance.yaml
kubectl -n descheduler edit configmap descheduler

以下 data.policy.yaml 用于验证运行。它将 Descheduler 限制为测试中的两个工作节点,仅处理测试 VM 的 launcher,并允许每个周期执行一次迁移:

apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
nodeSelector: "kubernetes.io/hostname in (<worker-a>,<worker-b>)"
maxNoOfPodsToEvictPerNode: 1
maxNoOfPodsToEvictPerNamespace: 1
maxNoOfPodsToEvictTotal: 1
profiles:
- name: vm-rebalance
  pluginConfig:
  - name: DefaultEvictor
    args:
      nodeFit: true
      labelSelector:
        matchLabels:
          app: desched-test
      podProtections:
        defaultDisabled:
        - PodsWithLocalStorage
  - name: LowNodeUtilization
    args:
      thresholds:
        cpu: 70
        memory: 60
        pods: 20
      targetThresholds:
        cpu: 75
        memory: 68
        pods: 30
  plugins:
    balance:
      enabled:
      - LowNodeUtilization

字段说明:

  • nodeSelector 同时限制被分类的节点和驱逐来源。测试时不要包含 control-plane 节点。
  • DefaultEvictor.args.labelSelector 限制候选 Pod。VM 的 spec.template.metadata.labels 中设置的标签会复制到其 virt-launcher Pod,因此只需设置 VM 标签即可限定测试范围。
  • LowNodeUtilization.args.evictableNamespaces 仅接受 exclude。include 列表会导致每次运行都因 only Exclude namespaces can be set, inclusion is not supported 失败,并且 CronJob 会持续重试失败的 Job,直到修复策略。
  • LowNodeUtilization 根据资源请求而非使用量对节点进行分类。virt-launcher 会请求 VM 内存加上 KubeVirt 开销(对于 512Mi guest,约为 270Mi),以及 100m CPU,除非 VM 设置了 CPU 请求。应从 Descheduler 日志中的分类行(见下文)获取阈值,而不是从 kubectl top 获取。
  • maxNoOfPodsToEvict* 限制(以及设置了 LowNodeUtilization.args.evictionLimits.node 时的该限制)会统计后台驱逐请求,因此可以限制每个周期的并发迁移数量。

仅通过此安装的策略 ConfigMap 和 CronJob(或 Deployment)更改行为。

验证重新平衡

在至少有两个可调度工作节点的临时命名空间中运行测试:

  1. 使用范围标签创建测试 VM,等待 LiveMigratable=True, 并记录源节点和 VMI UID:

    kubectl get vmi -n <vm-namespace> \
      -o custom-columns='NAME:.metadata.name,UID:.metadata.uid,NODE:.status.nodeName,STRATEGY:.spec.evictionStrategy'
  2. 创建有界且可逆的不平衡状态(例如,使用首选节点亲和性将测试 VM 放置在 一个工作节点上),使一个工作节点高于 targetThresholds,另一个低于 thresholds。

  3. 等待下一次 CronJob 运行(默认每两分钟一次)并读取其 日志:

    kubectl -n descheduler logs -l app.kubernetes.io/name=descheduler --tail=200 \
      | grep -E 'classified|removablePods|Eviction in background|Number of evictions'

    成功的请求如下所示:

    "Node has been classified" category="underutilized" node="<worker-b>" usagePercentage={"cpu":66,"memory":57,"pods":16}
    "Node has been classified" category="overutilized" node="<worker-a>" usagePercentage={"cpu":66,"memory":72,"pods":18}
    "Pods on node" node="<worker-a>" allPods=46 nonRemovablePods=44 removablePods=2
    "Eviction in background assumed" pod="<vm-namespace>/virt-launcher-<vm>-<suffix>"
    "Number of evictions/requests" totalEvicted=0 evictionRequests=1

    totalEvicted=0 evictionRequests=1 是预期结果:Pod 未被驱逐,而是请求了迁移。

  4. 观察 KubeVirt 完成迁移:

    kubectl get vmim -n <vm-namespace> \
      -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,VMI:.spec.vmiName,SOURCE:.status.migrationState.sourceNode,TARGET:.status.migrationState.targetNode'
    kubectl get vmi -n <vm-namespace> \
      -o custom-columns='NAME:.metadata.name,UID:.metadata.uid,NODE:.status.nodeName,EVACUATION:.status.evacuationNodeName'
    kubectl get events -n <vm-namespace> --sort-by=.lastTimestamp | grep -i -E 'evacuation|migrat'

    VirtualMachineInstanceMigration 与 Descheduler 请求在同一秒创建, 名称为 kubevirt-evacuation-<suffix>。一个 512Mi 的测试 VM 约在十秒内完成预拷贝迁移。

  5. 仅当 VMI 在不同节点上运行、VirtualMachineInstanceMigration 达到 Succeeded 且源节点和目标节点符合预期、VMI UID 未改变、 VM 仍处于 Running/Ready 状态,并且旧 launcher 已终止时,测试才算通过。单独的 Descheduler 日志 行、Eviction API 响应或发生变化的 Pod 名称,都不能作为迁移结果。

Descheduler 会在每次运行时重新评估集群。使用上述限制时, 它每个周期移动一个 VM,直到没有节点低于 thresholds,此时 日志会显示 No node is underutilized, nothing to do here。如果两个节点 持续在两个类别之间来回切换,说明对于工作负载的大小而言阈值过于接近;应扩大 thresholds 与 targetThresholds 之间的差距, 而不是降低驱逐限制。

要在迁移进行期间区分由 Descheduler 触发的迁移和由 drain 触发的迁移,请读取 VMI 注解:

kubectl get vmi <vmi-name> -n <vm-namespace> \
  -o jsonpath='{.metadata.annotations.kubevirt\.io/eviction-source}{"\n"}'

迁移完成后会移除该注解;之后,将 kubevirt-evacuation-* 迁移时间戳与 Descheduler Job 日志中的 Eviction in background assumed 行进行关联。

停止并回滚

如果迁移重复发生或一直处于待处理状态,请暂停 CronJob,然后移除策略覆盖。删除 ResourcePatch 会在几秒内恢复由 chart 管理的策略;或者重新应用备份文件。

kubectl -n descheduler patch cronjob descheduler \
  --type=merge -p '{"spec":{"suspend":true}}'
kubectl -n descheduler get resourcepatch
kubectl -n descheduler delete resourcepatch <rp-name>
kubectl -n descheduler get configmap descheduler -o jsonpath='{.data.policy\.yaml}' | head -5
kubectl get vmim -A
kubectl -n descheduler patch cronjob descheduler \
  --type=merge -p '{"spec":{"suspend":false}}'

在重新启用 CronJob 之前,请等待所有 VirtualMachineInstanceMigration 对象达到 Succeeded 或 Failed。

兼容性检查

作为本页面依据的 HCO 兼容性套件会在安装可选插件后断言以下内容:

  • 存在带标签的 Descheduler ConfigMap,并且 data.policy.yaml 可解析为带有 profile 的 DeschedulerPolicy;
  • 使用该策略的 CronJob 或 Deployment 会以 --feature-gates=EvictionsInBackground=true 渲染(这是针对 package 的回归检查,而非管理员设置);
  • 如果提供了独立的 KubeDescheduler CRD,则会独立检查其 profile 和字段 schema;
  • KubeVirt HCO 和 VM 驱逐策略路径仍可被发现。

实际的重新平衡结果仍需要一个真实的双工作节点 KubeVirt 集群。 每次运行都记录 ACP release、Kubernetes server version、KubeVirt version、HCO version 和 Descheduler plugin version。如果任何 API 或参数发生变化,请同时更新此页面和 兼容性测试。