1818
1919import java .util .ArrayList ;
2020import java .util .Collections ;
21+ import java .util .Comparator ;
2122import java .util .Date ;
2223import java .util .HashMap ;
2324import java .util .List ;
3435import com .cloud .capacity .CapacityManager ;
3536import com .cloud .capacity .CapacityVO ;
3637import com .cloud .capacity .dao .CapacityDao ;
38+ import com .cloud .dc .ClusterDetailsDao ;
39+ import com .cloud .dc .ClusterDetailsVO ;
3740import com .cloud .host .Host ;
3841import com .cloud .host .HostScoringWeights ;
3942import com .cloud .utils .component .AdapterBase ;
43+ import com .cloud .vm .VmDetailConstants ;
4044import com .cloud .vm .dao .VMInstanceDao ;
4145
4246/**
@@ -109,6 +113,9 @@ public class WeightedHostScorer extends AdapterBase implements Configurable {
109113 @ Inject
110114 private CapacityDao capacityDao ;
111115
116+ @ Inject
117+ private ClusterDetailsDao clusterDetailsDao ;
118+
112119 @ Inject
113120 private VMInstanceDao vmInstanceDao ;
114121
@@ -128,19 +135,53 @@ public List<Host> rank(long zoneId, Long podId, Long clusterId, List<? extends H
128135
129136 Map <Long , Double > scores = score (zoneId , podId , clusterId , hosts );
130137
131- List <Host > scored = hosts .stream ().filter (h -> scores .containsKey (h .getId ())).collect (Collectors .toList ());
132- List <Host > unscored = hosts .stream ().filter (h -> !scores .containsKey (h .getId ())).collect (Collectors .toList ());
133- scored .sort ((a , b ) -> Double .compare (scores .get (a .getId ()), scores .get (b .getId ())));
138+ List <Host > unscored = new ArrayList <>();
139+ List <Host > measured = new ArrayList <>();
140+ List <Host > unmeasured = new ArrayList <>();
141+ for (Host host : hosts ) {
142+ if (!scores .containsKey (host .getId ())) {
143+ unscored .add (host );
144+ } else if (hostLoadTracker .getLoad (host .getId ()).isUsable ()) {
145+ measured .add (host );
146+ } else {
147+ unmeasured .add (host );
148+ }
149+ }
150+
151+ Comparator <Host > byScore = Comparator .comparingDouble (h -> scores .get (h .getId ()));
152+ measured .sort (byScore );
153+ unmeasured .sort (byScore );
134154
135- List <Host > admitted = applyUtilisationThresholds (clusterId , scored );
136- applySelectionSpread (clusterId , admitted );
155+ List <Host > healthy = new ArrayList <>();
156+ List <Host > tooBusy = new ArrayList <>();
157+ partitionByUtilisation (clusterId , measured , healthy , tooBusy );
137158
138- logger .debug ("Weighted host ranking: {}" , () -> admitted .stream ()
159+ List <Host > result = new ArrayList <>();
160+ if (healthy .isEmpty () && unmeasured .isEmpty ()) {
161+ logger .warn ("Every candidate host is above its utilisation threshold, so the thresholds are being "
162+ + "ignored for this deployment. The cluster is short of capacity." );
163+ result .addAll (tooBusy );
164+ applySelectionSpread (clusterId , result );
165+ } else {
166+ result .addAll (healthy );
167+ // spread only over hosts known to be healthy, before anything else is appended,
168+ // otherwise a busy or unmeasured host can be shuffled into the lead
169+ applySelectionSpread (clusterId , result );
170+ // a host we cannot measure is not assumed to be idle: it ranks behind every host we can
171+ result .addAll (unmeasured );
172+ result .addAll (tooBusy );
173+ }
174+
175+ if (!tooBusy .isEmpty ()) {
176+ logger .debug ("Holding back {} host(s) above their utilisation threshold: {}" , tooBusy .size (), tooBusy );
177+ }
178+ logger .debug ("Weighted host ranking: {}" , () -> result .stream ()
179+ .filter (h -> scores .containsKey (h .getId ()))
139180 .map (h -> String .format ("%s=%.4f" , h .getName (), scores .get (h .getId ())))
140181 .collect (Collectors .joining (", " )));
141182
142- admitted .addAll (unscored );
143- return admitted ;
183+ result .addAll (unscored );
184+ return result ;
144185 }
145186
146187 protected Map <Long , Double > score (long zoneId , Long podId , Long clusterId , List <? extends Host > hosts ) {
@@ -165,27 +206,58 @@ protected Map<Long, Double> score(long zoneId, Long podId, Long clusterId, List<
165206 }
166207
167208 /**
168- * Allocated CPU and memory as a fraction of what the host advertises after overprovisioning,
169- * which is the same basis the existing allocators use.
209+ * Allocated CPU and memory as a fraction of what a host can hand out.
210+ *
211+ * op_host_capacity stores totals raw; overprovisioning is applied when they are read, so the
212+ * cluster's ratio has to be applied here too. Without it the fraction reaches 1 at the host's
213+ * physical size and every host on an overcommitted cluster clamps to 1, which is where this
214+ * algorithm is most needed.
215+ *
216+ * Only hosts with both a CPU and a memory row are returned. A host missing one would otherwise
217+ * score as if that resource were untouched, making it the most attractive host in the cluster.
170218 */
171219 protected Map <Long , Double []> allocatedFractions (List <CapacityVO > capacities ) {
172220 Map <Long , Double []> fractions = new HashMap <>();
221+ Map <Long , Integer > seen = new HashMap <>();
173222 for (CapacityVO capacity : capacities ) {
174223 long total = capacity .getTotalCapacity ();
175224 if (total <= 0 ) {
176225 continue ;
177226 }
178- double used = (double ) (capacity .getUsedCapacity () + capacity .getReservedCapacity ()) / total ;
227+ boolean isCpu = capacity .getCapacityType () == Capacity .CAPACITY_TYPE_CPU ;
228+ float overcommit = overcommitRatio (capacity .getClusterId (), isCpu );
229+ double allocatable = total * overcommit ;
230+ double used = (double ) (capacity .getUsedCapacity () + capacity .getReservedCapacity ()) / allocatable ;
231+
179232 Double [] entry = fractions .computeIfAbsent (capacity .getHostOrPoolId (), id -> new Double [] {0.0 , 0.0 });
180- if (capacity .getCapacityType () == Capacity .CAPACITY_TYPE_CPU ) {
181- entry [0 ] = clamp (used );
182- } else {
183- entry [1 ] = clamp (used );
184- }
233+ entry [isCpu ? 0 : 1 ] = clamp (used );
234+ seen .merge (capacity .getHostOrPoolId (), isCpu ? 1 : 2 , Integer ::sum );
185235 }
236+ fractions .keySet ().removeIf (hostId -> seen .getOrDefault (hostId , 0 ) != 3 );
186237 return fractions ;
187238 }
188239
240+ /**
241+ * The cluster's overprovisioning factor, defaulting to none if it cannot be read.
242+ */
243+ protected float overcommitRatio (Long clusterId , boolean forCpu ) {
244+ if (clusterId == null ) {
245+ return 1f ;
246+ }
247+ String key = forCpu ? VmDetailConstants .CPU_OVER_COMMIT_RATIO : VmDetailConstants .MEMORY_OVER_COMMIT_RATIO ;
248+ ClusterDetailsVO detail = clusterDetailsDao .findDetail (clusterId , key );
249+ if (detail == null || detail .getValue () == null ) {
250+ return 1f ;
251+ }
252+ try {
253+ float ratio = Float .parseFloat (detail .getValue ());
254+ return ratio > 0 ? ratio : 1f ;
255+ } catch (NumberFormatException e ) {
256+ logger .warn ("Cluster {} has an unreadable {} of [{}], treating it as 1." , clusterId , key , detail .getValue ());
257+ return 1f ;
258+ }
259+ }
260+
189261 /**
190262 * The blend. Every term is a fraction of the host's capacity for that resource so the weights
191263 * are directly comparable, and the dominant resource term is added on top of the weighted mean
@@ -238,35 +310,21 @@ protected double dominantResource(double cpuAllocated, double memoryAllocated, H
238310 }
239311
240312 /**
241- * Holds back hosts that are measurably too busy, unless that would leave nothing to deploy on,
242- * in which case ranking alone decides and the caller's capacity checks still apply .
313+ * Splits measurably busy hosts out from the rest. Only hosts with load samples can be held
314+ * back; a host that cannot be measured is dealt with by the caller .
243315 */
244- protected List <Host > applyUtilisationThresholds ( Long clusterId , List <Host > ranked ) {
316+ protected void partitionByUtilisation ( Long clusterId , List <Host > measured , List < Host > healthy , List <Host > tooBusy ) {
245317 double cpuThreshold = valueIn (CpuUtilisationThreshold , clusterId );
246318 double memoryThreshold = valueIn (MemoryUtilisationThreshold , clusterId );
247319
248- List <Host > admitted = new ArrayList <>();
249- List <Host > heldBack = new ArrayList <>();
250- for (Host host : ranked ) {
320+ for (Host host : measured ) {
251321 HostLoad load = hostLoadTracker .getLoad (host .getId ());
252- if (load .isUsable ()
253- && (load .getCpuUtilisation () > cpuThreshold || load .getMemoryUtilisation () > memoryThreshold )) {
254- heldBack .add (host );
322+ if (load .getCpuUtilisation () > cpuThreshold || load .getMemoryUtilisation () > memoryThreshold ) {
323+ tooBusy .add (host );
255324 } else {
256- admitted .add (host );
325+ healthy .add (host );
257326 }
258327 }
259-
260- if (admitted .isEmpty ()) {
261- logger .warn ("Every candidate host is above its utilisation threshold, so the thresholds are being "
262- + "ignored for this deployment. The cluster is short of capacity." );
263- return heldBack ;
264- }
265- if (!heldBack .isEmpty ()) {
266- logger .debug ("Holding back {} host(s) above their utilisation threshold: {}" , heldBack .size (), heldBack );
267- admitted .addAll (heldBack );
268- }
269- return admitted ;
270328 }
271329
272330 /**
0 commit comments