Rebalance KubeVirt VMs with Descheduler
The Alauda Container Platform Descheduler plugin can request eviction of a
virt-launcher Pod. KubeVirt intercepts that request and live-migrates the
VMI instead of stopping the VM. Use this as a secondary, bounded rebalancing
mechanism. For a failed or intentionally maintained node, follow
KubeVirt Node Maintenance instead.
This page describes the ACP ModulePlugin package. It is configured through its own resources — a policy ConfigMap and a CronJob (or Deployment) — and installs no descheduler CRD; there is no operator-level API to configure it through.
The procedure below was verified on ACP 4.4 (Kubernetes v1.35) with KubeVirt
v1.8.4 (HCO 1.18.1) and a Descheduler package build that renders the
EvictionsInBackground feature gate by default (any package version newer
than v0.36.0-4). Read the installed versions from the live cluster; both the
package and KubeVirt evolve independently.
TOC
How a Descheduler Eviction Becomes a Live MigrationPrerequisitesThe package renders the feature gateThe VM is live-migratable with a migrating eviction strategyDiscover the pluginWhy the Default Policy Never Migrates a VMConfigure a Bounded VM Rebalance PolicyValidate RebalancingStop and Roll BackCompatibility ChecksHow a Descheduler Eviction Becomes a Live Migration
- Descheduler selects a
virt-launcherPod and calls the Eviction API. The Eviction object carries the annotationrequested-by: sigs.k8s.io/descheduler. - KubeVirt adds the annotation
descheduler.alpha.kubernetes.io/request-evict-only: ""to everyvirt-launcherPod when the Pod is created, regardless of the eviction strategy. Descheduler only applies its background-eviction handling to Pods that carry this annotation. - The KubeVirt webhook
virt-launcher-eviction-interceptor.kubevirt.iodecides what happens based on the VMI's effective eviction strategy: the VMI'sspec.evictionStrategyif set, otherwise the cluster default from theKubeVirtresource (spec.configuration.evictionStrategy, which HCO renders fromHyperConverged.spec.evictionStrategy).
When the VMI is marked for evacuation, KubeVirt sets
status.evacuationNodeName on the VMI, adds the annotation
kubevirt.io/eviction-source: descheduler to the VMI, and creates a
VirtualMachineInstanceMigration named kubevirt-evacuation-<suffix> with
the annotation kubevirt.io/evacuationMigration: <source-node>. While the
migration is pending, KubeVirt also annotates the source Pod with
descheduler.alpha.kubernetes.io/eviction-in-progress: "true". After the
migration completes, virt-handler removes eviction-source and clears
evacuationNodeName, so those two markers are only visible while the
evacuation is in flight.
With EvictionsInBackground=true, Descheduler treats that specific 429 as a
successful eviction request: it logs Eviction in background assumed,
counts the request against maxNoOfPodsToEvict*, and remembers the Pod. In
the next cycle it sees the eviction-in-progress annotation and logs
Eviction in background already requested (ignoring) instead of asking
again. Without the feature gate, the 429 is logged as a failed eviction and
the same launcher can be nominated again in every cycle.
A VM whose effective strategy is None, or LiveMigrateIfPossible with a
VMI that is not migratable, is restarted, not migrated, when Descheduler
evicts its launcher. Verify the strategy before allowing VM launchers to be
evicted.
Prerequisites
The package renders the feature gate
The current package renders the feature gate from its own values; no administrator configuration is needed. Verify it on the workload cluster that runs the VMs:
Expected output:
If the argument is missing, the installed package predates the change:
upgrade the Alauda Container Platform Descheduler plugin. Do not hand-edit the
generated CronJob or Deployment. The --v=3 verbosity is also required to see
the Eviction in background ... log lines used below.
The VM is live-migratable with a migrating eviction strategy
The VM must use shared storage that is usable from the target node (for
example, RWX block storage), report LiveMigratable=True, and have
LiveMigrate or LiveMigrateIfPossible as its effective eviction strategy.
The HCO example shipped with ACP sets spec.evictionStrategy: LiveMigrateIfPossible; a HyperConverged created with an empty spec
defaults to None on ACP. Check both levels:
The VMI value is fixed when the VMI is created. Changing
HyperConverged.spec.evictionStrategy propagates to the KubeVirt resource
immediately but affects only VMIs created afterwards; existing VMIs keep
their recorded value (for example None) until the VM is restarted.
Discover the plugin
Install Alauda Container Platform Descheduler from Administrator > Marketplace > Cluster Plugins as described in Workload Rebalancing (Descheduler).
ModuleInfo is served only by the global (management) cluster; running the
command against a workload cluster fails with
the server doesn't have a resource type "moduleinfo". The policy ConfigMap
and the CronJob live on the workload cluster, in the namespace
descheduler that the package creates.
On the global cluster:
On the workload cluster:
The policy is a raw descheduler/v1alpha2 DeschedulerPolicy: configure it
with the upstream plugins and arguments described on this page; it has no
predefined KubeVirt profile or descheduling mode setting.
(LowNodeUtilization.args.evictionLimits.node is a regular upstream field and
may be used in addition to the top-level maxNoOfPodsToEvict* limits.)
Why the Default Policy Never Migrates a VM
The package ships one default profile whose DefaultEvictor is configured
as:
PodsWithPVCprotects every Pod that mounts a PersistentVolumeClaim. Allvirt-launcherPods of PVC-backed VMs are therefore skipped, so the default policy is safe for VMs but cannot rebalance them.PodsWithLocalStoragemust stay disabled: everyvirt-launcherPod has severalemptyDirvolumes (private,public,sockets,ephemeral-disks,container-disks,hotplug-disks, ...). Re-enabling that protection makes launchers non-evictable even without a PVC.
To rebalance VMs you must remove PodsWithPVC from extraEnabled in a
profile that is scoped to the VMs you want to move.
Configure a Bounded VM Rebalance Policy
Editing the ConfigMap is the supported configuration path. ACP records the
edit as a ResourcePatch in the plugin namespace (the API server prints
Warning: The operation is allowed, and ResourcePatch rp-<suffix> is created),
so the change survives chart reconciliation. Each CronJob run reads the
ConfigMap when it starts; no restart is needed.
Back up the generated policy first:
The following data.policy.yaml was used for the verification run. It
restricts Descheduler to the two workers under test, considers only the
launchers of the test VMs, and allows one migration per cycle:
Notes on the fields:
nodeSelectorlimits both the nodes that are classified and the eviction sources. Keep control-plane nodes out of a test.DefaultEvictor.args.labelSelectorlimits the candidate Pods. Labels set inspec.template.metadata.labelsof the VM are copied to itsvirt-launcherPod, so a VM label is enough to scope the test.LowNodeUtilization.args.evictableNamespacesaccepts onlyexclude. Anincludelist makes every run fail withonly Exclude namespaces can be set, inclusion is not supported, and the CronJob keeps retrying the failed Job until the policy is fixed.LowNodeUtilizationclassifies nodes by resource requests, not usage. Avirt-launcherrequests the VM memory plus KubeVirt overhead (about 270Mi for a 512Mi guest) and 100m CPU unless the VM sets CPU requests. Take the thresholds from the classification lines in the Descheduler log (see below) rather than fromkubectl top.- The
maxNoOfPodsToEvict*limits (andLowNodeUtilization.args.evictionLimits.node, if set) count background eviction requests, so they cap the number of concurrent migrations per cycle.
Change the behavior only through the policy ConfigMap and the CronJob (or Deployment) of this installation.
Validate Rebalancing
Run the test in a disposable namespace with at least two schedulable workers:
-
Create the test VMs with the scoping label, wait for
LiveMigratable=True, and record the source node and VMI UID: -
Create a bounded, reversible imbalance (for example, place the test VMs on one worker with a preferred node affinity) so that one worker is above
targetThresholdsand another is belowthresholds. -
Wait for the next CronJob run (every two minutes by default) and read its log:
A successful request looks like this:
totalEvicted=0 evictionRequests=1is the expected outcome: the Pod was not evicted, a migration was requested. -
Watch KubeVirt complete the migration:
The
VirtualMachineInstanceMigrationis created in the same second as the Descheduler request and is namedkubevirt-evacuation-<suffix>. A 512Mi test VM completed the pre-copy migration in about ten seconds. -
Pass only when the VMI is running on a different node, the
VirtualMachineInstanceMigrationreachesSucceededwith the expected source and target nodes, the VMI UID is unchanged, the VM remainsRunning/Ready, and the old launcher has terminated. A Descheduler log line, an Eviction API response, or a changed Pod name alone is not a migration result.
Descheduler evaluates the cluster again on every run. With the limits above
it moves one VM per cycle until no node is below thresholds, at which point
the log reads No node is underutilized, nothing to do here. If two nodes
keep swapping between the two categories, the thresholds are too close
together for the size of the workloads; widen the gap between thresholds
and targetThresholds instead of lowering the eviction limits.
To tell a Descheduler-triggered migration from a drain-triggered one while it is in flight, read the VMI annotation:
The annotation is removed once the migration completes; afterwards correlate
the kubevirt-evacuation-* migration timestamp with the
Eviction in background assumed line in the Descheduler Job log.
Stop and Roll Back
Suspend the CronJob if migrations repeat or remain pending, then remove the
policy override. Deleting the ResourcePatch restores the chart-managed
policy within seconds; alternatively re-apply the backup file.
Wait for all VirtualMachineInstanceMigration objects to reach Succeeded
or Failed before re-enabling the CronJob.
Compatibility Checks
The HCO compatibility suite backing this page asserts, when the optional plugin is installed, that:
- a labeled Descheduler ConfigMap exists and
data.policy.yamlparses as aDeschedulerPolicywith profiles; - the CronJob or Deployment that consumes the policy is rendered with
--feature-gates=EvictionsInBackground=true(a regression check on the package, not an administrator setting); - if the separate
KubeDeschedulerCRD is served, its profile and field schema are checked independently; - KubeVirt HCO and VM eviction-strategy paths remain discoverable.
The live rebalance result still requires a real two-worker KubeVirt cluster. Record the ACP release, Kubernetes server version, KubeVirt version, HCO version, and Descheduler plugin version with each run. Update this page and the compatibility test together when any API or argument changes.