Skip to content

Commit ea6bf35

Browse files
Add 'weighted' DRS algorithm
Opt-in per cluster via drs.algorithm. balanced and condensed are untouched and the default is unchanged. The existing algorithms balance one metric chosen by drs.metric, so whichever is not chosen goes unwatched: a cluster can sit inside its imbalance threshold on memory while CPU load varies several fold, and DRS correctly does nothing. Allocation is also a poor stand-in for load under overprovisioning, where a saturated host still reports a small percentage allocated. 'weighted' blends four figures per host - CPU and memory allocated, CPU and memory in use - and balances the result. Imbalance keeps its existing definition, standard deviation over the mean, so drs.imbalance still means what it did. - allocation is measured against what a host can hand out, so the overcommit ratio is applied - a migration is only worth making if the cluster ends up more even, and one that has to move storage costs more - hosts without utilisation samples fall back to allocation figures needsDrs decides whether to look at a cluster at all, and it was handed lists with no host identity, so measured load could not be attributed to a host. Added a map form alongside it; algorithms that only need the values keep the default and are unaffected. Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent 7040b48 commit ea6bf35

19 files changed

Lines changed: 603 additions & 18 deletions

File tree

PendingReleaseNotes

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,21 @@ example.ver.1 > example.ver.2:
5858
utilisation comes from a moving average configured with host.load.sample.interval and
5959
host.load.half.life. Until a management server has collected samples, ranking falls back to
6060
allocation figures alone.
61+
62+
* New DRS algorithm 'weighted' for drs.algorithm, alongside 'balanced' and 'condensed'. The
63+
default is unchanged; the new one is opt-in per cluster.
64+
65+
The existing algorithms balance a single metric chosen by drs.metric, so choosing one leaves
66+
the other unwatched: a cluster can be even on memory while its CPU load varies several fold and
67+
nothing moves. Allocation is also a poor stand-in for load under overprovisioning, where a
68+
saturated host can still report a small percentage allocated.
69+
70+
'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.
74+
75+
* DRS plan generation is considerably faster on large clusters. Working out where a VM could go
76+
is now done once for each group of VMs that would get the same answer rather than once per VM,
77+
and affinity is re-evaluated only for VMs affected by the migration just planned rather than
78+
for every VM on every iteration.

server/src/main/java/com/cloud/agent/manager/allocator/impl/HostLoad.java renamed to api/src/main/java/com/cloud/host/HostLoad.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// KIND, either express or implied. See the License for the
1515
// specific language governing permissions and limitations
1616
// under the License.
17-
package com.cloud.agent.manager.allocator.impl;
17+
package com.cloud.host;
1818

1919
/**
2020
* Smoothed view of what a host is actually doing, as opposed to what has been allocated on it.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
package com.cloud.host;
18+
19+
/**
20+
* A smoothed view of what each host is actually doing, as opposed to what has been allocated on it.
21+
*
22+
* Allocation figures say what was promised; under overprovisioning they can be far from what a host
23+
* is really carrying. Placement and rebalancing both need the second view.
24+
*/
25+
public interface HostLoadService {
26+
27+
/**
28+
* @return the host's smoothed load, or a value reporting itself unusable when the host has not
29+
* been sampled recently enough to rank on
30+
*/
31+
HostLoad getLoad(long hostId);
32+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ public interface ClusterDrsAlgorithm extends Adapter {
6262
boolean needsDrs(Cluster cluster, List<Ternary<Long, Long, Long>> cpuList,
6363
List<Ternary<Long, Long, Long>> memoryList) throws ConfigurationException;
6464

65+
/**
66+
* As above, but keeping host identity.
67+
*
68+
* An algorithm that considers anything beyond the figures in the two maps - measured load, for
69+
* instance - cannot attribute it without knowing which host each entry belongs to. Algorithms
70+
* that only need the values keep the default.
71+
*
72+
* @param hostCpuMap
73+
* host id to a Ternary of used, reserved and total CPU
74+
* @param hostMemoryMap
75+
* host id to a Ternary of used, reserved and total memory
76+
*/
77+
default boolean needsDrs(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
78+
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) throws ConfigurationException {
79+
return needsDrs(cluster, new ArrayList<>(hostCpuMap.values()), new ArrayList<>(hostMemoryMap.values()));
80+
}
81+
6582
/**
6683
* Calculates the metrics (improvement, cost, benefit) for migrating a VM to a destination host. Improvement is
6784
* calculated based on the change in cluster imbalance before and after the migration.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public interface ClusterDrsService extends Manager, Configurable, Scheduler {
5151
true, ConfigKey.Scope.Cluster, null, "Maximum number of migrations for DRS", null, null, null);
5252

5353
ConfigKey<String> ClusterDrsAlgorithm = new ConfigKey<>(String.class, "drs.algorithm",
54-
ConfigKey.CATEGORY_ADVANCED, "balanced", "The DRS algorithm to be executed on the cluster. Possible values are condensed, balanced.",
54+
ConfigKey.CATEGORY_ADVANCED, "balanced", "The DRS algorithm to be executed on the cluster. Possible values are condensed, balanced, weighted. 'weighted' balances CPU and memory together, and on measured load as well as allocation, tuned with the drs.weighted.* settings.",
5555
true, ConfigKey.Scope.Cluster, null, "DRS algorithm", null, null,
56-
null, ConfigKey.Kind.Select, "condensed,balanced");
56+
null, ConfigKey.Kind.Select, "condensed,balanced,weighted");
5757

5858
ConfigKey<Float> ClusterDrsImbalanceThreshold = new ConfigKey<>(Float.class, "drs.imbalance",
5959
ConfigKey.CATEGORY_ADVANCED, "0.4",

client/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@
592592
<artifactId>cloud-plugin-cluster-drs-condensed</artifactId>
593593
<version>${project.version}</version>
594594
</dependency>
595+
<dependency>
596+
<groupId>org.apache.cloudstack</groupId>
597+
<artifactId>cloud-plugin-cluster-drs-weighted</artifactId>
598+
<version>${project.version}</version>
599+
</dependency>
595600
<dependency>
596601
<groupId>org.apache.cloudstack</groupId>
597602
<artifactId>cloud-plugin-database-quota</artifactId>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one
4+
~ or more contributor license agreements. See the NOTICE file
5+
~ distributed with this work for additional information
6+
~ regarding copyright ownership. The ASF licenses this file
7+
~ to you under the Apache License, Version 2.0 (the
8+
~ "License"); you may not use this file except in compliance
9+
~ with the License. You may obtain a copy of the License at
10+
~
11+
~ http://www.apache.org/licenses/LICENSE-2.0
12+
~
13+
~ Unless required by applicable law or agreed to in writing,
14+
~ software distributed under the License is distributed on an
15+
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
~ KIND, either express or implied. See the License for the
17+
~ specific language governing permissions and limitations
18+
~ under the License.
19+
-->
20+
21+
<project xmlns="http://maven.apache.org/POM/4.0.0"
22+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
23+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
24+
<modelVersion>4.0.0</modelVersion>
25+
<name>Apache CloudStack Plugin - Cluster DRS Algorithm - Weighted</name>
26+
<artifactId>cloud-plugin-cluster-drs-weighted</artifactId>
27+
<parent>
28+
<groupId>org.apache.cloudstack</groupId>
29+
<artifactId>cloudstack-plugins</artifactId>
30+
<version>24.0.0-SNAPSHOT</version>
31+
<relativePath>../../../pom.xml</relativePath>
32+
</parent>
33+
</project>
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
package org.apache.cloudstack.cluster;
18+
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import javax.inject.Inject;
24+
import javax.naming.ConfigurationException;
25+
26+
import org.apache.cloudstack.framework.config.ConfigKey;
27+
import org.apache.cloudstack.framework.config.Configurable;
28+
import org.apache.logging.log4j.LogManager;
29+
import org.apache.logging.log4j.Logger;
30+
31+
import com.cloud.dc.ClusterDetailsDao;
32+
import com.cloud.dc.ClusterDetailsVO;
33+
import com.cloud.host.Host;
34+
import com.cloud.host.HostLoad;
35+
import com.cloud.host.HostScoringWeights;
36+
import com.cloud.host.HostLoadService;
37+
import com.cloud.offering.ServiceOffering;
38+
import com.cloud.org.Cluster;
39+
import com.cloud.utils.Ternary;
40+
import com.cloud.utils.component.AdapterBase;
41+
import com.cloud.vm.VirtualMachine;
42+
import com.cloud.vm.VmDetailConstants;
43+
44+
/**
45+
* Balances a cluster on CPU and memory together, and on what hosts are really doing rather than
46+
* only on what has been allocated to them.
47+
*
48+
* The existing algorithms balance a single metric chosen by drs.metric. Choosing one leaves the
49+
* other unwatched: a cluster can be even on memory while its CPU load varies several fold, and
50+
* nothing moves. Allocation is also a poor stand-in for load under overprovisioning, where a
51+
* saturated host can still report a small percentage allocated.
52+
*
53+
* This blends four figures per host - CPU and memory allocated, CPU and memory in use - and
54+
* balances the result. The weights are the same host.weighted.* settings initial placement uses, on
55+
* 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.
58+
*/
59+
public class Weighted extends AdapterBase implements ClusterDrsAlgorithm, Configurable {
60+
61+
private static final Logger LOGGER = LogManager.getLogger(Weighted.class);
62+
63+
@Inject
64+
private HostLoadService hostLoadService;
65+
66+
@Inject
67+
private ClusterDetailsDao clusterDetailsDao;
68+
69+
@Override
70+
public String getName() {
71+
return "weighted";
72+
}
73+
74+
@Override
75+
public boolean needsDrs(Cluster cluster, List<Ternary<Long, Long, Long>> cpuList,
76+
List<Ternary<Long, Long, Long>> memoryList) throws ConfigurationException {
77+
// without host identity, measured load cannot be attributed; the map form is what DRS calls
78+
Map<Long, Ternary<Long, Long, Long>> cpuMap = new HashMap<>();
79+
Map<Long, Ternary<Long, Long, Long>> memoryMap = new HashMap<>();
80+
for (int i = 0; i < cpuList.size() && i < memoryList.size(); i++) {
81+
cpuMap.put((long) -(i + 1), cpuList.get(i));
82+
memoryMap.put((long) -(i + 1), memoryList.get(i));
83+
}
84+
return needsDrs(cluster, cpuMap, memoryMap);
85+
}
86+
87+
@Override
88+
public boolean needsDrs(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
89+
Map<Long, Ternary<Long, Long, Long>> hostMemoryMap) throws ConfigurationException {
90+
double threshold = 1.0 - ClusterDrsService.ClusterDrsImbalanceThreshold.valueIn(cluster.getId());
91+
double imbalance = imbalanceOf(blendByHost(cluster, hostCpuMap, hostMemoryMap).values());
92+
boolean needed = imbalance > threshold;
93+
LOGGER.debug("Cluster {} {} DRS. Imbalance: {} Threshold: {} Algorithm: {}",
94+
cluster, needed ? "needs" : "does not need", imbalance, threshold, getName());
95+
return needed;
96+
}
97+
98+
@Override
99+
public Ternary<Double, Double, Double> getMetrics(Cluster cluster, VirtualMachine vm,
100+
ServiceOffering serviceOffering, Host destHost,
101+
Map<Long, Ternary<Long, Long, Long>> hostCpuMap, Map<Long, Ternary<Long, Long, Long>> hostMemoryMap,
102+
Boolean requiresStorageMotion, Double preImbalance,
103+
double[] baseMetricsArray, Map<Long, Integer> hostIdToIndexMap) throws ConfigurationException {
104+
105+
double before = imbalanceOf(blendByHost(cluster, hostCpuMap, hostMemoryMap).values());
106+
107+
long vmCpu = (long) serviceOffering.getCpu() * serviceOffering.getSpeed();
108+
long vmMemory = serviceOffering.getRamSize() * 1024L * 1024L;
109+
Map<Long, Ternary<Long, Long, Long>> cpuAfter = withVmMoved(hostCpuMap, vm.getHostId(), destHost.getId(), vmCpu);
110+
Map<Long, Ternary<Long, Long, Long>> memoryAfter = withVmMoved(hostMemoryMap, vm.getHostId(), destHost.getId(), vmMemory);
111+
112+
double after = imbalanceOf(blendByHost(cluster, cpuAfter, memoryAfter).values());
113+
114+
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;
119+
120+
LOGGER.trace("Cluster {} imbalance {} -> {} moving {} to {}", cluster, before, after, vm, destHost);
121+
return new Ternary<>(improvement, cost, benefit);
122+
}
123+
124+
/**
125+
* One figure per host, blending what is allocated with what is in use.
126+
*/
127+
protected Map<Long, Double> blendByHost(Cluster cluster, Map<Long, Ternary<Long, Long, Long>> hostCpuMap,
128+
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());
136+
137+
Map<Long, Double> blended = new HashMap<>();
138+
for (Map.Entry<Long, Ternary<Long, Long, Long>> entry : hostCpuMap.entrySet()) {
139+
long hostId = entry.getKey();
140+
Ternary<Long, Long, Long> memory = hostMemoryMap.get(hostId);
141+
if (memory == null) {
142+
continue;
143+
}
144+
double cpuAllocated = fractionOf(entry.getValue(), cpuOvercommit);
145+
double memoryAllocated = fractionOf(memory, memoryOvercommit);
146+
147+
HostLoad load = hostLoadService == null ? HostLoad.UNKNOWN : hostLoadService.getLoad(hostId);
148+
double usedCpuWeight = load.isUsable() ? cpuUsedWeight : 0;
149+
double usedMemoryWeight = load.isUsable() ? memoryUsedWeight : 0;
150+
151+
double sum = cpuAllocatedWeight + memoryAllocatedWeight + usedCpuWeight + usedMemoryWeight;
152+
if (sum <= 0) {
153+
blended.put(hostId, 0.0);
154+
continue;
155+
}
156+
blended.put(hostId, (cpuAllocatedWeight * cpuAllocated
157+
+ memoryAllocatedWeight * memoryAllocated
158+
+ usedCpuWeight * load.getCpuUtilisation()
159+
+ usedMemoryWeight * load.getMemoryUtilisation()) / sum);
160+
}
161+
return blended;
162+
}
163+
164+
private Map<Long, Ternary<Long, Long, Long>> withVmMoved(Map<Long, Ternary<Long, Long, Long>> original,
165+
Long sourceHostId, long destHostId, long amount) {
166+
Map<Long, Ternary<Long, Long, Long>> copy = new HashMap<>();
167+
for (Map.Entry<Long, Ternary<Long, Long, Long>> entry : original.entrySet()) {
168+
Ternary<Long, Long, Long> value = entry.getValue();
169+
long used = value.first();
170+
if (entry.getKey().equals(sourceHostId)) {
171+
used -= amount;
172+
} else if (entry.getKey() == destHostId) {
173+
used += amount;
174+
}
175+
copy.put(entry.getKey(), new Ternary<>(used, value.second(), value.third()));
176+
}
177+
return copy;
178+
}
179+
180+
/**
181+
* Used over what the host can hand out, which is its real total scaled by the overcommit ratio.
182+
*/
183+
private double fractionOf(Ternary<Long, Long, Long> capacity, float overcommit) {
184+
double allocatable = (capacity.third() - capacity.second()) * (double) overcommit;
185+
if (allocatable <= 0) {
186+
return 0;
187+
}
188+
return clamp(capacity.first() / allocatable);
189+
}
190+
191+
protected float overcommitRatio(long clusterId, String key) {
192+
ClusterDetailsVO detail = clusterDetailsDao.findDetail(clusterId, key);
193+
if (detail == null || detail.getValue() == null) {
194+
return 1f;
195+
}
196+
try {
197+
float ratio = Float.parseFloat(detail.getValue());
198+
return ratio > 0 ? ratio : 1f;
199+
} catch (NumberFormatException e) {
200+
return 1f;
201+
}
202+
}
203+
204+
private double weight(ConfigKey<Double> key, long clusterId) {
205+
Double value = key.valueIn(clusterId);
206+
if (value == null || value < 0) {
207+
return 0;
208+
}
209+
return value;
210+
}
211+
212+
/**
213+
* Standard deviation over the mean, the same definition the other algorithms use, so that
214+
* drs.imbalance keeps its meaning.
215+
*/
216+
protected double imbalanceOf(java.util.Collection<Double> values) {
217+
if (values == null || values.isEmpty()) {
218+
return 0;
219+
}
220+
double[] array = values.stream().mapToDouble(Double::doubleValue).toArray();
221+
double mean = MEAN_CALCULATOR.evaluate(array);
222+
if (mean == 0) {
223+
return 0;
224+
}
225+
return STDDEV_CALCULATOR.evaluate(array, mean) / mean;
226+
}
227+
228+
private static double clamp(double value) {
229+
if (Double.isNaN(value) || value < 0) {
230+
return 0;
231+
}
232+
return Math.min(value, 1);
233+
}
234+
235+
@Override
236+
public String getConfigComponentName() {
237+
return Weighted.class.getSimpleName();
238+
}
239+
240+
@Override
241+
public ConfigKey<?>[] getConfigKeys() {
242+
// the four host.weighted.* weights are shared with initial placement, which registers them
243+
return new ConfigKey<?>[] {StorageMotionCost};
244+
}
245+
}

0 commit comments

Comments
 (0)