Skip to content

Commit a154255

Browse files
Fix defects found in review of the weighted DRS algorithm
Two of these meant the algorithm could not run at all. The plugin had no module.properties, so it was never discovered, never instantiated and never registered. Selecting it would have failed every DRS run with "Invalid algorithm configured". The plugin injected a service that lives in a sibling Spring module and is therefore not visible to it, so once the module did load, the management server would fail to start. - the caller now works out the load once per plan and passes it in - which also fixes the cost of getting it: getMetrics runs for every candidate VM and host, and it was reading two cluster details and four settings from the database on each call Grouping VMs to share one candidate-host lookup was unsound. The key listed the inputs it thought mattered, and missed several that are held per VM rather than per offering - a custom offering's size, boot mode, device settings. Two VMs could then share a host list neither of them should have had. - only group VMs that carry none of those, rather than trying to enumerate everything that could matter - key on disk offering as well as pool, since storage tags come from the offering Reserved capacity was multiplied by the overcommit ratio instead of being subtracted from the scaled total, so a host reading 11% fuller than its peer read 67% fuller, and DRS would evacuate hosts merely for holding reserved capacity. Hosts were compared on different bases. A host with no utilisation samples was measured on allocation alone while its peers were measured on a blend, reporting a difference that was an artefact of the monitoring. Utilisation is now used only when every host has it. The cost and benefit terms cancelled out, so storage motion was free despite the claim otherwise. A migration that has to move storage now has to earn more than one that does not, by a configurable margin. Also: allocation beyond what a host can hand out is no longer flattened to a single value, and the settings that shape the other algorithms' single metric are documented as not applying here. Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent ea6bf35 commit a154255

8 files changed

Lines changed: 391 additions & 70 deletions

File tree

PendingReleaseNotes

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,18 @@ example.ver.1 > example.ver.2:
6868
saturated host can still report a small percentage allocated.
6969

7070
'weighted' blends CPU and memory allocated with CPU and memory in use, and balances the result.
71-
Imbalance keeps its existing meaning - standard deviation over the mean - so drs.imbalance
72-
still means what it did. Tuned with the drs.weighted.* settings, all cluster scoped. Where no
73-
utilisation samples are available it falls back to allocation figures.
71+
Imbalance keeps its existing shape - standard deviation over the mean - but it is computed over
72+
a blend rather than over one metric, so drs.imbalance is not calibrated the same way and is
73+
worth re-checking after switching. drs.metric, drs.metric.type and drs.metric.use.ratio choose
74+
and shape the single metric the other algorithms balance; they do not apply to 'weighted' and
75+
are ignored.
76+
77+
Utilisation is only used when every host in the cluster has been sampled. Comparing a host
78+
measured on utilisation against one measured on allocation alone would report a difference that
79+
is an artefact of the monitoring rather than of the load, so the cluster falls back to
80+
allocation figures until every host can be measured.
81+
82+
Tuned with the drs.weighted.* settings, all cluster scoped.
7483

7584
* DRS plan generation is considerably faster on large clusters. Working out where a VM could go
7685
is now done once for each group of VMs that would get the same answer rather than once per VM,

api/src/main/java/org/apache/cloudstack/cluster/ClusterDrsAlgorithm.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.apache.cloudstack.cluster;
2121

2222
import com.cloud.host.Host;
23+
import com.cloud.host.HostLoad;
2324
import com.cloud.offering.ServiceOffering;
2425
import com.cloud.org.Cluster;
2526
import com.cloud.utils.Ternary;
@@ -75,10 +76,22 @@ boolean needsDrs(Cluster cluster, List<Ternary<Long, Long, Long>> cpuList,
7576
* host id to a Ternary of used, reserved and total memory
7677
*/
7778
default boolean needsDrs(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
78-
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) throws ConfigurationException {
79+
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap, Map<Long, HostLoad> hostLoadMap)
80+
throws ConfigurationException {
7981
return needsDrs(cluster, new ArrayList<>(hostCpuMap.values()), new ArrayList<>(hostMemoryMap.values()));
8082
}
8183

84+
/**
85+
* Called once per plan, before any migration is considered, so that an algorithm can do work
86+
* that would otherwise be repeated for every candidate VM and host.
87+
*
88+
* @param hostLoadMap
89+
* measured load per host, empty when nothing has been sampled
90+
*/
91+
default void prepare(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
92+
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap, Map<Long, HostLoad> hostLoadMap) {
93+
}
94+
8295
/**
8396
* Calculates the metrics (improvement, cost, benefit) for migrating a VM to a destination host. Improvement is
8497
* calculated based on the change in cluster imbalance before and after the migration.

plugins/drs/cluster/weighted/src/main/java/org/apache/cloudstack/cluster/Weighted.java

Lines changed: 104 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import com.cloud.host.Host;
3434
import com.cloud.host.HostLoad;
3535
import com.cloud.host.HostScoringWeights;
36-
import com.cloud.host.HostLoadService;
3736
import com.cloud.offering.ServiceOffering;
3837
import com.cloud.org.Cluster;
3938
import com.cloud.utils.Ternary;
@@ -53,19 +52,92 @@
5352
* This blends four figures per host - CPU and memory allocated, CPU and memory in use - and
5453
* balances the result. The weights are the same host.weighted.* settings initial placement uses, on
5554
* purpose: if the two weighted them differently they would disagree about which host is the better
56-
* one, and rebalancing could move VMs off hosts that placement had just chosen. Imbalance keeps the same meaning as the other algorithms: the standard
57-
* deviation of the per-host figure over its mean, so drs.imbalance still means what it did.
55+
* one, and rebalancing could move VMs off hosts that placement had just chosen. Imbalance keeps the same shape as the other algorithms - the standard
56+
* deviation of the per-host figure over its mean - but it is computed over a blend rather than over
57+
* one metric, so drs.imbalance is not calibrated the same way and is worth re-checking after
58+
* switching.
59+
*
60+
* drs.metric, drs.metric.type and drs.metric.use.ratio choose and shape the single metric the other
61+
* algorithms balance. They do not apply here and are ignored.
5862
*/
5963
public class Weighted extends AdapterBase implements ClusterDrsAlgorithm, Configurable {
6064

6165
private static final Logger LOGGER = LogManager.getLogger(Weighted.class);
6266

63-
@Inject
64-
private HostLoadService hostLoadService;
67+
public static final ConfigKey<Double> StorageMotionCost = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
68+
Double.class, "drs.weighted.storage.motion.cost", "0.02",
69+
"How much a migration must improve the cluster's imbalance to be worth also moving the VM's "
70+
+ "storage. Migrations that do not need storage moved only have to improve it at all.",
71+
true, ConfigKey.Scope.Cluster);
6572

6673
@Inject
6774
private ClusterDetailsDao clusterDetailsDao;
6875

76+
/**
77+
* Everything that is constant for one plan. getMetrics is called for every candidate VM and
78+
* host - up to hundreds of thousands of times for a large cluster - so nothing in that path may
79+
* hit the database or re-read settings.
80+
*/
81+
private static final class PlanContext {
82+
private final float cpuOvercommit;
83+
private final float memoryOvercommit;
84+
private final double cpuAllocatedWeight;
85+
private final double memoryAllocatedWeight;
86+
private final double cpuUsedWeight;
87+
private final double memoryUsedWeight;
88+
private final Map<Long, HostLoad> hostLoadMap;
89+
private final boolean everyHostMeasured;
90+
91+
private PlanContext(float cpuOvercommit, float memoryOvercommit, double cpuAllocatedWeight,
92+
double memoryAllocatedWeight, double cpuUsedWeight, double memoryUsedWeight,
93+
Map<Long, HostLoad> hostLoadMap, boolean everyHostMeasured) {
94+
this.everyHostMeasured = everyHostMeasured;
95+
this.cpuOvercommit = cpuOvercommit;
96+
this.memoryOvercommit = memoryOvercommit;
97+
this.cpuAllocatedWeight = cpuAllocatedWeight;
98+
this.memoryAllocatedWeight = memoryAllocatedWeight;
99+
this.cpuUsedWeight = cpuUsedWeight;
100+
this.memoryUsedWeight = memoryUsedWeight;
101+
this.hostLoadMap = hostLoadMap;
102+
}
103+
104+
private HostLoad loadOf(long hostId) {
105+
HostLoad load = hostLoadMap.get(hostId);
106+
return load == null ? HostLoad.UNKNOWN : load;
107+
}
108+
}
109+
110+
private final ThreadLocal<PlanContext> context = new ThreadLocal<>();
111+
112+
@Override
113+
public void prepare(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
114+
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap, Map<Long, HostLoad> hostLoadMap) {
115+
long clusterId = cluster.getId();
116+
context.set(new PlanContext(
117+
overcommitRatio(clusterId, VmDetailConstants.CPU_OVER_COMMIT_RATIO),
118+
overcommitRatio(clusterId, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO),
119+
weight(HostScoringWeights.CpuAllocatedWeight, clusterId),
120+
weight(HostScoringWeights.MemoryAllocatedWeight, clusterId),
121+
weight(HostScoringWeights.CpuUsedWeight, clusterId),
122+
weight(HostScoringWeights.MemoryUsedWeight, clusterId),
123+
hostLoadMap == null ? new HashMap<>() : hostLoadMap,
124+
hostLoadMap != null && !hostLoadMap.isEmpty()
125+
&& hostLoadMap.values().stream().allMatch(HostLoad::isUsable)));
126+
}
127+
128+
/**
129+
* Falls back to reading everything when prepare has not been called, so the algorithm still
130+
* works for a caller that does not know about it.
131+
*/
132+
private PlanContext contextFor(Cluster cluster) {
133+
PlanContext prepared = context.get();
134+
if (prepared != null) {
135+
return prepared;
136+
}
137+
prepare(cluster, null, null, null);
138+
return context.get();
139+
}
140+
69141
@Override
70142
public String getName() {
71143
return "weighted";
@@ -81,12 +153,13 @@ public boolean needsDrs(Cluster cluster, List<Ternary<Long, Long, Long>> cpuList
81153
cpuMap.put((long) -(i + 1), cpuList.get(i));
82154
memoryMap.put((long) -(i + 1), memoryList.get(i));
83155
}
84-
return needsDrs(cluster, cpuMap, memoryMap);
156+
return needsDrs(cluster, cpuMap, memoryMap, new HashMap<>());
85157
}
86158

87159
@Override
88160
public boolean needsDrs(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
89-
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) throws ConfigurationException {
161+
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap, Map<Long, HostLoad> hostLoadMap)
162+
throws ConfigurationException {
90163
double threshold = 1.0 - ClusterDrsService.ClusterDrsImbalanceThreshold.valueIn(cluster.getId());
91164
double imbalance = imbalanceOf(blendByHost(cluster, hostCpuMap, hostMemoryMap).values());
92165
boolean needed = imbalance > threshold;
@@ -111,11 +184,12 @@ public Ternary<Double, Double, Double> getMetrics(Cluster cluster, VirtualMachin
111184

112185
double after = imbalanceOf(blendByHost(cluster, cpuAfter, memoryAfter).values());
113186

187+
// the caller migrates when benefit > cost, so expressing both in units of imbalance makes
188+
// that comparison mean "is this worth what it costs". A migration that has to move storage
189+
// has to earn more than one that does not.
114190
double improvement = before - after;
115-
// moving a VM costs something and buys nothing unless the cluster ends up more even, so a
116-
// migration is only worth making when it measurably helps
117-
double cost = Boolean.TRUE.equals(requiresStorageMotion) ? 1.0 : 0.0;
118-
double benefit = improvement > 0 ? 1.0 + improvement : 0.0;
191+
double cost = Boolean.TRUE.equals(requiresStorageMotion) ? weight(StorageMotionCost, cluster.getId()) : 0.0;
192+
double benefit = improvement;
119193

120194
LOGGER.trace("Cluster {} imbalance {} -> {} moving {} to {}", cluster, before, after, vm, destHost);
121195
return new Ternary<>(improvement, cost, benefit);
@@ -126,13 +200,7 @@ public Ternary<Double, Double, Double> getMetrics(Cluster cluster, VirtualMachin
126200
*/
127201
protected Map<Long, Double> blendByHost(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
128202
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) {
129-
float cpuOvercommit = overcommitRatio(cluster.getId(), VmDetailConstants.CPU_OVER_COMMIT_RATIO);
130-
float memoryOvercommit = overcommitRatio(cluster.getId(), VmDetailConstants.MEMORY_OVER_COMMIT_RATIO);
131-
132-
double cpuAllocatedWeight = weight(HostScoringWeights.CpuAllocatedWeight, cluster.getId());
133-
double memoryAllocatedWeight = weight(HostScoringWeights.MemoryAllocatedWeight, cluster.getId());
134-
double cpuUsedWeight = weight(HostScoringWeights.CpuUsedWeight, cluster.getId());
135-
double memoryUsedWeight = weight(HostScoringWeights.MemoryUsedWeight, cluster.getId());
203+
PlanContext ctx = contextFor(cluster);
136204

137205
Map<Long, Double> blended = new HashMap<>();
138206
for (Map.Entry<Long, Ternary<Long, Long, Long>> entry : hostCpuMap.entrySet()) {
@@ -141,20 +209,24 @@ protected Map<Long, Double> blendByHost(Cluster cluster, Map<Long, Ternary<Long,
141209
if (memory == null) {
142210
continue;
143211
}
144-
double cpuAllocated = fractionOf(entry.getValue(), cpuOvercommit);
145-
double memoryAllocated = fractionOf(memory, memoryOvercommit);
212+
double cpuAllocated = fractionOf(entry.getValue(), ctx.cpuOvercommit);
213+
double memoryAllocated = fractionOf(memory, ctx.memoryOvercommit);
146214

147-
HostLoad load = hostLoadService == null ? HostLoad.UNKNOWN : hostLoadService.getLoad(hostId);
148-
double usedCpuWeight = load.isUsable() ? cpuUsedWeight : 0;
149-
double usedMemoryWeight = load.isUsable() ? memoryUsedWeight : 0;
215+
// utilisation is only used when every host has it. Imbalance compares hosts against
216+
// each other, so mixing hosts measured on utilisation with hosts measured on allocation
217+
// alone would report a difference that is an artefact of the monitoring, not the load -
218+
// and would evacuate whichever host stopped reporting.
219+
HostLoad load = ctx.loadOf(hostId);
220+
double usedCpuWeight = ctx.everyHostMeasured ? ctx.cpuUsedWeight : 0;
221+
double usedMemoryWeight = ctx.everyHostMeasured ? ctx.memoryUsedWeight : 0;
150222

151-
double sum = cpuAllocatedWeight + memoryAllocatedWeight + usedCpuWeight + usedMemoryWeight;
223+
double sum = ctx.cpuAllocatedWeight + ctx.memoryAllocatedWeight + usedCpuWeight + usedMemoryWeight;
152224
if (sum <= 0) {
153225
blended.put(hostId, 0.0);
154226
continue;
155227
}
156-
blended.put(hostId, (cpuAllocatedWeight * cpuAllocated
157-
+ memoryAllocatedWeight * memoryAllocated
228+
blended.put(hostId, (ctx.cpuAllocatedWeight * cpuAllocated
229+
+ ctx.memoryAllocatedWeight * memoryAllocated
158230
+ usedCpuWeight * load.getCpuUtilisation()
159231
+ usedMemoryWeight * load.getMemoryUtilisation()) / sum);
160232
}
@@ -181,11 +253,14 @@ private Map<Long, Ternary<Long, Long, Long>> withVmMoved(Map<Long, Ternary<Long,
181253
* Used over what the host can hand out, which is its real total scaled by the overcommit ratio.
182254
*/
183255
private double fractionOf(Ternary<Long, Long, Long> capacity, float overcommit) {
184-
double allocatable = (capacity.third() - capacity.second()) * (double) overcommit;
256+
// overcommit scales the host's total; reserved is then taken off that, which is how
257+
// CapacityManager computes free capacity everywhere else. Multiplying reserved by the ratio
258+
// instead would make a host look fuller the more capacity it merely has reserved.
259+
double allocatable = capacity.third() * (double) overcommit - capacity.second();
185260
if (allocatable <= 0) {
186261
return 0;
187262
}
188-
return clamp(capacity.first() / allocatable);
263+
return capacity.first() / allocatable;
189264
}
190265

191266
protected float overcommitRatio(long clusterId, String key) {
@@ -239,7 +314,7 @@ public String getConfigComponentName() {
239314

240315
@Override
241316
public ConfigKey<?>[] getConfigKeys() {
242-
// the four host.weighted.* weights are shared with initial placement, which registers them
317+
// the four host.weighted.* weights are shared with initial placement and registered there
243318
return new ConfigKey<?>[] {StorageMotionCost};
244319
}
245320
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
name=weighted
18+
parent=cluster

0 commit comments

Comments
 (0)