Add 'balancedweighted' VM allocation algorithm - #14109
Open
bhouse-nexthop wants to merge 5 commits into
Open
Conversation
Counts VMs occupying each host in a zone, pod or cluster, optionally only those that changed state recently. One query for the whole scope rather than one per host. Signed-off-by: Brad House <bhouse@nexthop.ai>
StatsCollector already polls CPU and memory utilisation for every host, but it keeps only the newest sample and nothing uses it for placement. - fold those samples into an exponentially weighted moving average - weight by elapsed time, so a missed poll decays correctly instead of over-weighting the previous value - report nothing usable until a host has been sampled, so callers can fall back to allocation figures Two settings: host.load.sample.interval and host.load.half.life. Signed-off-by: Brad House <bhouse@nexthop.ai>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #14109 +/- ##
============================================
+ Coverage 19.79% 19.82% +0.03%
- Complexity 20015 20069 +54
============================================
Files 6371 6375 +4
Lines 575954 576287 +333
Branches 70521 70562 +41
============================================
+ Hits 113997 114262 +265
- Misses 449530 449577 +47
- Partials 12427 12448 +21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
9 tasks
Opt-in via vm.allocation.algorithm. Existing algorithms and the default are untouched. Allocated capacity alone is a poor ranking under heavy overprovisioning: it is measured against a total already multiplied by the overprovisioning factor, so a host under real strain still reports a low percentage and keeps being chosen. Anything allocation cannot see - VMs the scheduler has lost track of, guests using more than they asked for - is invisible. Ranks on a blend, lower is better: | term | source | |---------------------|-------------------------------------------| | CPU allocated | op_host_capacity, over the inflated total | | CPU utilisation | moving average of real usage | | memory allocated | op_host_capacity, over the inflated total | | memory utilisation | moving average of real usage | | VM count | VMs on the host | | recent starts | VMs started within the last few minutes | A dominant resource term is added on top so a host nearly out of any one resource does not rank well on a good average. Hosts measurably too busy are held back, unless that would leave nowhere to deploy. Selection is random among the best few rather than strictly ordered: capacity is only charged once a VM starts, so concurrent deployments all read the same figures and strict ordering makes them agree on one host. All weights and thresholds are settings, most cluster scoped. Signed-off-by: Brad House <bhouse@nexthop.ai>
Allocated fraction was measured against the wrong total. op_host_capacity stores totals raw and overprovisioning is applied when they are read, so dividing by the stored total made the fraction reach 1 at the host's physical size. On a cluster overcommitted 10 times every host clamped to 1, killing both the allocation term and the dominant resource term - on exactly the clusters this algorithm is for. - apply the cluster ratio to the denominator - drop hosts missing a CPU or memory capacity row instead of scoring the missing resource as untouched, which made them rank first Utilisation thresholds could be bypassed. Held-back hosts were appended before the random spread was applied, so the spread could shuffle a busy host into the lead. With 1 healthy host and a spread of 3, two thirds of deployments picked an over-threshold host. - spread over healthy hosts only, before anything else is appended A host with no load samples was treated as idle. It was exempt from the thresholds and its dominant resource term fell back to allocation, so a host with broken stats outranked every measured host and collected the deployments. - rank hosts we cannot measure behind every host we can - when nothing can be measured, ranking falls back to allocation as before Signed-off-by: Brad House <bhouse@nexthop.ai>
Utilisation average
- a host whose agent stops reporting kept vouching for itself forever:
StatsCollector hands back the previous entry when a poll fails, and
that unchanged reading was folded again every minute. Detect the
repeat, and expire an average that stops being updated
- sample on a scheduled executor catching Throwable, not a Timer, which
dies permanently and silently on one escaping error
- only collect when an algorithm that reads the figures is selected
- read the half life once per sample rather than inside the map update
- document what getCpuUtilization means per hypervisor: it is what this
assumes on KVM, a reservation figure on VMware, and scaled by core
count on XenServer
Scoring
- a negative weight would rank the most loaded host first; floor at
zero and say so
- zeroing all six terms no longer discards the dominant resource term
- read weights once per ranking instead of once per host
Queries
- one query per ranking instead of two, returning both counts
- count Stopping VMs, which still hold their host
- correct the doc: every host in scope is returned, including empty ones
Tests
- cover rank() end to end, which is where the defects were: the
capacity denominator, the thresholds, the spread and the ordering of
measured against unmeasured hosts
- the distribution simulation drew different random streams per arm, so
the arms saw different workloads. Fix the workload up front and add
an allocation-only-plus-spread control, which shows the scoring and
not the spread is what evens out real load
Signed-off-by: Brad House <bhouse@nexthop.ai>
bhouse-nexthop
force-pushed
the
weighted-placement
branch
from
September 10, 2026 01:37
87c06e3 to
726e632
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR adds an opt-in VM allocation algorithm,
balancedweighted, forvm.allocation.algorithm. Existing algorithms and the default are untouched.The problem. The existing algorithms rank hosts on allocated capacity alone. Under a large overprovisioning factor that reads badly: allocation is measured against a total that has already been multiplied by the factor, so a host under real strain can still report a low percentage allocated and keep attracting new VMs. Anything allocation cannot see - guests using far more than they asked for, VMs the management server has lost track of - is invisible.
Concurrent deployments make it worse. Capacity is only charged once a VM starts, so every decision taken in the same moment reads the same figures, and strict ordering makes them all agree on one host.
What it ranks on. A blend, lower is better:
op_host_capacity, over the overprovisioned totalop_host_capacity, over the overprovisioned totalRecent starts exist because a VM that started moments ago is invisible to both allocation lag and a moving average, while often working hardest.
A dominant-resource term is added on top of the weighted mean, taking the larger of allocated and measured per resource, so that a host nearly out of any one resource does not rank well on a good average.
Then two things happen that ranking alone does not do:
The algorithm
Every candidate host gets a score in
[0, 1]; lower is better.Terms. Each is a fraction of that host's capacity for the resource, so the weights are directly comparable:
All six are clamped to
[0, 1]. The allocated terms divide by the overprovisioned total because that is what a host can hand out; dividing by the raw total makes every host on an overcommitted cluster read as full.Score.
dominantis the host's most stressed resource, and it takes the larger of allocated and measured per resource — memory reclaimed from idle guests is taken back the moment those guests get busy, so the optimistic reading should not win. Adding it on top of the mean stops a host that is fine on average but nearly out of one resource from ranking well. Both parts are convex combinations of values in[0, 1], so the score stays in[0, 1].When a host has no usable utilisation samples,
w_cuandw_mudrop out of both numerator and denominator, and the host is ranked behind every host that can be measured rather than being assumed idle.Moving average. Weighted by elapsed time, so a missed poll decays by the right amount instead of over-weighting the previous value:
Selection. Ranking alone is not enough: capacity is only charged once a VM starts, so concurrent deployments all read the same figures and strict ordering makes them agree on one host.
Settings
Weights are relative to each other; only their ratios matter.
0disables a term.The first four are defined in
apirather than alongside the allocator, because they describe howloaded a host is rather than anything specific to placement, and a DRS algorithm that ranks hosts
the same way should read the same settings instead of carrying a second copy that could disagree.
host.weighted.cpu.allocated.weight1.0host.weighted.cpu.used.weight2.0host.weighted.memory.allocated.weight1.0host.weighted.memory.used.weight2.0host.weighted.vm.count.weight1.0host.weighted.recent.start.weight2.0host.weighted.dominant.resource.weight1.0host.weighted.recent.start.window300host.weighted.expected.vms.per.host50host.weighted.cpu.utilisation.threshold0.851disableshost.weighted.memory.utilisation.threshold0.901disableshost.weighted.selection.spread31restores strict orderinghost.load.sample.interval60host.stats.intervalhost.load.half.life300host.load.stale.after600All are dynamic except
host.load.sample.interval.Measured effect. Simulation over a churning, heavily overprovisioned fleet where part of the real load is invisible to allocation, measuring how unevenly real load ends up distributed (max/mean across hosts, lower is better):
The middle row is a control: it isolates what the scoring contributes from what the randomisation contributes.
Utilisation figures come from a moving average of what
StatsCollectoralready polls but nothing used for placement. It is per management server and not persisted; every management server polls every host, so they converge. Until a server has samples, ranking falls back to allocation figures, and hosts that cannot be measured rank behind hosts that can rather than being assumed idle.Note the utilisation terms are only meaningful as intended on KVM.
getCpuUtilizationreports a reservation figure on VMware and is scaled by core count on XenServer; this is documented on the collector.All weights and thresholds are settings, most cluster scoped.
Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
How Has This Been Tested?
Unit tests, 39 new cases:
WeightedHostScorerTest- the scoring function: each term's direction, the dominant-resource term, unmeasured hosts, the utilisation gates, the selection spread.WeightedHostScorerRankTest-rank()end to end as the allocator calls it, with the capacity and VM-count queries mocked. Covers the overprovisioned denominator, a busy host ranking behind a quiet one at equal allocation, an over-threshold host never leading while a healthy one exists, unmeasured hosts ranking last, fallback to allocation when nothing is measured, and a host missing a capacity row.HostLoadTrackerTest- the moving average: first sample, a single spike not dominating, convergence, half-life, missed samples decaying by elapsed time rather than sample count, unchanged readings not folded twice, and a host that stops reporting becoming unusable.WeightedPlacementDistributionTest- the simulation above. Deterministic; the workload is fixed before any arm runs so all arms see identical VMs.Full
mvn testonapi,engine/schemaandserver, checkstyle and license checks enabled: 0 failures.How did you try to break this feature and the system with this change?
This went through two rounds of adversarial review. The defects found and fixed are worth listing, since they are the interesting part:
op_host_capacitystores totals raw and applies overprovisioning when they are read. Dividing by the stored total made the fraction reach 1 at the host's physical size, so on a cluster overcommitted ten times every host clamped to 1 and both the allocation term and the dominant-resource term went dead - on exactly the clusters this is for.StatsCollectorhands back the previous entry when a poll fails, and that unchanged reading was folded again every interval.Timer, which dies permanently and silently on one escaping error, leaving placement quietly back on allocation alone. It also ran regardless of whether the algorithm was selected.rank(), which is why it caught none of the above. Both fixed, and the spread-only control arm added.Other things checked: hosts missing a capacity row, all-zero weights,
podId/clusterIdbeing null, concurrent access to the shared average, and that the two per-ranking queries became one.