Skip to content

Commit 726e632

Browse files
Address the rest of the weighted scoring review
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>
1 parent f150476 commit 726e632

8 files changed

Lines changed: 499 additions & 87 deletions

File tree

engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,16 @@ public interface VMInstanceDao extends GenericDao<VMInstanceVO, Long>, StateDao<
132132
List<Long> listHostIdsByVmCount(long dcId, Long podId, Long clusterId, long accountId);
133133

134134
/**
135-
* Counts the VMs occupying each host in a zone, pod or cluster.
135+
* Counts the VMs occupying each host in a zone, pod or cluster, in one query.
136136
*
137-
* @param startedAfter
138-
* when set, counts only VMs that last changed state after this time, which approximates
139-
* the VMs that have recently started and are still working through their startup load
140-
* @return host id to VM count, hosts with no VMs omitted
137+
* @param changedStateAfter
138+
* cut-off for the second count: VMs whose state last changed after this. Approximates
139+
* the VMs still working through their startup load, which neither allocation nor a
140+
* utilisation average has caught up with yet.
141+
* @return host id to {total VMs, VMs that changed state recently}. Every host in scope appears,
142+
* including hosts with no VMs.
141143
*/
142-
Map<Long, Long> countVmsByHost(long dcId, Long podId, Long clusterId, Date startedAfter);
144+
Map<Long, Pair<Long, Long>> countVmsByHost(long dcId, Long podId, Long clusterId, Date changedStateAfter);
143145

144146
Long countRunningAndStartingByAccount(long accountId);
145147

engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,10 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem
155155
private static final String COUNT_VMS_BASED_ON_VGPU_TYPES2 =
156156
"GROUP BY gpu_card.name, vgpu_profile.name";
157157

158-
private static final String COUNT_VMS_BY_HOST = "SELECT host.id, COUNT(vm.id) FROM `cloud`.`host` host " +
159-
"LEFT JOIN `cloud`.`vm_instance` vm ON vm.host_id = host.id AND vm.state IN ('Running', 'Starting', 'Migrating') " +
160-
"AND vm.removed IS NULL %s WHERE host.type = 'Routing' AND host.removed IS NULL AND host.data_center_id = ? ";
158+
private static final String COUNT_VMS_BY_HOST = "SELECT host.id, COUNT(vm.id), SUM(IF(vm.update_time > ?, 1, 0)) " +
159+
"FROM `cloud`.`host` host LEFT JOIN `cloud`.`vm_instance` vm " +
160+
"ON vm.host_id = host.id AND vm.state IN ('Running', 'Starting', 'Stopping', 'Migrating') " +
161+
"AND vm.removed IS NULL WHERE host.type = 'Routing' AND host.removed IS NULL AND host.data_center_id = ? ";
161162
private static final String COUNT_VMS_BY_HOST_PART2 = " GROUP BY host.id ";
162163

163164
private static final String UPDATE_SYSTEM_VM_TEMPLATE_ID_FOR_HYPERVISOR = "UPDATE `cloud`.`vm_instance` SET vm_template_id = ? WHERE type <> 'User' AND hypervisor_type = ? AND removed is NULL";
@@ -803,10 +804,10 @@ public Pair<List<Long>, Map<Long, Double>> listPodIdsInZoneByVmCount(long dataCe
803804

804805

805806
@Override
806-
public Map<Long, Long> countVmsByHost(long dcId, Long podId, Long clusterId, Date startedAfter) {
807+
public Map<Long, Pair<Long, Long>> countVmsByHost(long dcId, Long podId, Long clusterId, Date changedStateAfter) {
807808
TransactionLegacy txn = TransactionLegacy.currentTxn();
808-
Map<Long, Long> result = new HashMap<>();
809-
String sql = String.format(COUNT_VMS_BY_HOST, startedAfter != null ? " AND vm.update_time > ? " : "");
809+
Map<Long, Pair<Long, Long>> result = new HashMap<>();
810+
String sql = COUNT_VMS_BY_HOST;
810811
if (podId != null) {
811812
sql = sql + " AND host.pod_id = ? ";
812813
}
@@ -817,19 +818,19 @@ public Map<Long, Long> countVmsByHost(long dcId, Long podId, Long clusterId, Dat
817818
try {
818819
PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql);
819820
int index = 1;
820-
if (startedAfter != null) {
821-
pstmt.setTimestamp(index++, new Timestamp(startedAfter.getTime()));
822-
}
821+
// a cut-off in the future counts nothing as recent, which is what a null asks for
822+
long cutOff = changedStateAfter != null ? changedStateAfter.getTime() : Long.MAX_VALUE;
823+
pstmt.setTimestamp(index++, new Timestamp(cutOff));
823824
pstmt.setLong(index++, dcId);
824825
if (podId != null) {
825826
pstmt.setLong(index++, podId);
826827
}
827828
if (clusterId != null) {
828-
pstmt.setLong(index, clusterId);
829+
pstmt.setLong(index++, clusterId);
829830
}
830831
ResultSet rs = pstmt.executeQuery();
831832
while (rs.next()) {
832-
result.put(rs.getLong(1), rs.getLong(2));
833+
result.put(rs.getLong(1), new Pair<>(rs.getLong(2), rs.getLong(3)));
833834
}
834835
return result;
835836
} catch (SQLException e) {

server/src/main/java/com/cloud/agent/manager/allocator/impl/HostLoadTracker.java

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,26 @@
1717
package com.cloud.agent.manager.allocator.impl;
1818

1919
import java.util.Map;
20-
import java.util.Timer;
21-
import java.util.TimerTask;
2220
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.concurrent.Executors;
22+
import java.util.concurrent.ScheduledExecutorService;
23+
import java.util.concurrent.TimeUnit;
2324

2425
import javax.inject.Inject;
2526

2627
import org.apache.cloudstack.framework.config.ConfigKey;
2728
import org.apache.cloudstack.framework.config.Configurable;
28-
import org.apache.cloudstack.managed.context.ManagedContextTimerTask;
29+
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
2930

3031
import com.cloud.host.HostStats;
3132
import com.cloud.host.HostVO;
3233
import com.cloud.host.Status;
3334
import com.cloud.host.dao.HostDao;
3435
import com.cloud.server.StatsCollector;
36+
import com.cloud.deploy.DeploymentClusterPlanner;
37+
import com.cloud.deploy.DeploymentPlanner.AllocationAlgorithm;
3538
import com.cloud.utils.component.ManagerBase;
39+
import com.cloud.utils.concurrency.NamedThreadFactory;
3640

3741
/**
3842
* Keeps a smoothed view of how hard each host is actually working.
@@ -45,6 +49,19 @@
4549
* The average is per management server and is not persisted. Every management server polls every
4650
* host, so all of them converge on the same picture, and a restarted server simply reports nothing
4751
* usable until it has sampled - callers then fall back to allocation figures.
52+
*
53+
* What getCpuUtilization means depends on the hypervisor, and only KVM reports what this class
54+
* assumes:
55+
*
56+
* <ul>
57+
* <li>KVM reports busy time as a percentage of the host's cores, which is what is wanted.</li>
58+
* <li>VMware reports the share of CPU that is reserved rather than the share that is busy, so
59+
* the CPU term becomes a second allocation signal there rather than a load signal.</li>
60+
* <li>XenServer sums per-core averages without dividing by core count, so the value ranges up to
61+
* the number of cores and is under-reported here by roughly that factor.</li>
62+
* </ul>
63+
*
64+
* Memory is taken as used over total and is sound everywhere.
4865
*/
4966
public class HostLoadTracker extends ManagerBase implements Configurable {
5067

@@ -54,6 +71,13 @@ public class HostLoadTracker extends ManagerBase implements Configurable {
5471
"consider actual load. Should not be shorter than host.stats.interval.",
5572
false, ConfigKey.Scope.Global);
5673

74+
public static final ConfigKey<Integer> HostLoadStaleAfter = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
75+
Integer.class, "host.load.stale.after", "600",
76+
"Seconds after which a host's utilisation average is considered out of date and stops being used " +
77+
"for placement. A host whose agent stops reporting would otherwise keep vouching for itself " +
78+
"with figures that never change.",
79+
true, ConfigKey.Scope.Global);
80+
5781
public static final ConfigKey<Integer> HostLoadHalfLife = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
5882
Integer.class, "host.load.half.life", "300",
5983
"Half life in seconds of the moving average of host utilisation. Larger values react more " +
@@ -68,35 +92,41 @@ public class HostLoadTracker extends ManagerBase implements Configurable {
6892

6993
private final Map<Long, Sample> samples = new ConcurrentHashMap<>();
7094

71-
private Timer timer;
95+
private ScheduledExecutorService executor;
7296

7397
@Override
7498
public boolean start() {
75-
int interval = Math.max(1, HostLoadSampleInterval.value()) * 1000;
76-
TimerTask task = new ManagedContextTimerTask() {
99+
int interval = Math.max(1, HostLoadSampleInterval.value());
100+
executor = Executors.newSingleThreadScheduledExecutor(
101+
new NamedThreadFactory("HostLoadTracker"));
102+
// catch Throwable: an escaping error would cancel all future runs, and the failure would be
103+
// silent - placement would quietly go back to ranking on allocation alone
104+
executor.scheduleWithFixedDelay(new ManagedContextRunnable() {
77105
@Override
78106
protected void runInContext() {
79107
try {
80108
sampleAllHosts();
81-
} catch (Exception e) {
82-
logger.warn("Unable to sample host load", e);
109+
} catch (Throwable t) {
110+
logger.warn("Unable to sample host load", t);
83111
}
84112
}
85-
};
86-
timer = new Timer("HostLoadTracker");
87-
timer.schedule(task, interval, interval);
113+
}, interval, interval, TimeUnit.SECONDS);
88114
return true;
89115
}
90116

91117
@Override
92118
public boolean stop() {
93-
if (timer != null) {
94-
timer.cancel();
119+
if (executor != null) {
120+
executor.shutdownNow();
95121
}
96122
return true;
97123
}
98124

99125
protected void sampleAllHosts() {
126+
if (!isInUse()) {
127+
samples.clear();
128+
return;
129+
}
100130
for (HostVO host : hostDao.listByType(com.cloud.host.Host.Type.Routing)) {
101131
if (host.getStatus() != Status.Up) {
102132
samples.remove(host.getId());
@@ -106,6 +136,14 @@ protected void sampleAllHosts() {
106136
}
107137
}
108138

139+
/**
140+
* Only the placement algorithms that read these figures pay for collecting them.
141+
*/
142+
protected boolean isInUse() {
143+
return AllocationAlgorithm.balancedweighted.toString()
144+
.equals(DeploymentClusterPlanner.VmAllocationAlgorithm.value());
145+
}
146+
109147
protected void record(long hostId, HostStats stats) {
110148
record(hostId, stats, System.currentTimeMillis());
111149
}
@@ -118,18 +156,40 @@ protected void record(long hostId, HostStats stats, long now) {
118156
if (totalMemory <= 0) {
119157
return;
120158
}
121-
// getCpuUtilization is a percentage of the host's real cores
159+
160+
Sample previous = samples.get(hostId);
161+
if (previous != null && previous.isSameReadingAs(stats)) {
162+
// StatsCollector keeps the previous entry when a poll fails, so an unchanged object is
163+
// a reading we have already folded, not a fresh measurement
164+
return;
165+
}
166+
167+
// getCpuUtilization is a percentage of the host's real cores. That holds for KVM; see the
168+
// class javadoc for what it means on other hypervisors.
122169
double cpu = clamp(stats.getCpuUtilization() / 100.0);
123170
double memory = clamp((totalMemory - stats.getFreeMemoryKBs()) / totalMemory);
171+
int halfLife = HostLoadHalfLife.value();
124172

125-
samples.compute(hostId, (id, previous) -> previous == null
126-
? new Sample(cpu, memory, now)
127-
: previous.fold(cpu, memory, now, HostLoadHalfLife.value()));
173+
samples.compute(hostId, (id, current) -> current == null
174+
? new Sample(cpu, memory, now, stats)
175+
: current.fold(cpu, memory, now, halfLife, stats));
128176
}
129177

130178
public HostLoad getLoad(long hostId) {
179+
return getLoad(hostId, System.currentTimeMillis());
180+
}
181+
182+
protected HostLoad getLoad(long hostId, long now) {
131183
Sample sample = samples.get(hostId);
132-
return sample == null ? HostLoad.UNKNOWN : sample.toHostLoad();
184+
if (sample == null) {
185+
return HostLoad.UNKNOWN;
186+
}
187+
long staleAfter = Math.max(1, HostLoadStaleAfter.value()) * 1000L;
188+
if (now - sample.updatedAt > staleAfter) {
189+
// the host has stopped reporting; stop letting its last known figures speak for it
190+
return HostLoad.UNKNOWN;
191+
}
192+
return sample.toHostLoad();
133193
}
134194

135195
protected void clear() {
@@ -150,7 +210,7 @@ public String getConfigComponentName() {
150210

151211
@Override
152212
public ConfigKey<?>[] getConfigKeys() {
153-
return new ConfigKey<?>[] {HostLoadSampleInterval, HostLoadHalfLife};
213+
return new ConfigKey<?>[] {HostLoadSampleInterval, HostLoadHalfLife, HostLoadStaleAfter};
154214
}
155215

156216
/**
@@ -162,21 +222,28 @@ private static final class Sample {
162222
private final double memory;
163223
private final long updatedAt;
164224
private final long count;
225+
private final HostStats reading;
165226

166-
private Sample(double cpu, double memory, long updatedAt) {
167-
this(cpu, memory, updatedAt, 1);
227+
private Sample(double cpu, double memory, long updatedAt, HostStats reading) {
228+
this(cpu, memory, updatedAt, 1, reading);
168229
}
169230

170-
private Sample(double cpu, double memory, long updatedAt, long count) {
231+
private Sample(double cpu, double memory, long updatedAt, long count, HostStats reading) {
171232
this.cpu = cpu;
172233
this.memory = memory;
173234
this.updatedAt = updatedAt;
174235
this.count = count;
236+
this.reading = reading;
237+
}
238+
239+
private boolean isSameReadingAs(HostStats stats) {
240+
return reading == stats;
175241
}
176242

177-
private Sample fold(double newCpu, double newMemory, long now, int halfLifeSeconds) {
243+
private Sample fold(double newCpu, double newMemory, long now, int halfLifeSeconds, HostStats reading) {
178244
double alpha = alpha(now - updatedAt, halfLifeSeconds);
179-
return new Sample(cpu + alpha * (newCpu - cpu), memory + alpha * (newMemory - memory), now, count + 1);
245+
return new Sample(cpu + alpha * (newCpu - cpu), memory + alpha * (newMemory - memory), now,
246+
count + 1, reading);
180247
}
181248

182249
private static double alpha(long elapsedMillis, int halfLifeSeconds) {

0 commit comments

Comments
 (0)