summaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java
diff options
context:
space:
mode:
Diffstat (limited to 'node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java103
1 files changed, 92 insertions, 11 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java
index 8930bf34f4a..6dca9c9a796 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java
@@ -1,11 +1,17 @@
// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.autoscale;
+import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.Flavor;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.host.FlavorOverrides;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.applications.Cluster;
+import com.yahoo.vespa.hosted.provision.provisioning.HostResourcesCalculator;
+import com.yahoo.vespa.hosted.provision.provisioning.NodeResourceLimits;
import java.time.Duration;
import java.util.List;
@@ -35,14 +41,18 @@ public class Autoscaler {
/** What difference factor for a resource is worth a reallocation? */
private static final double resourceDifferenceWorthReallocation = 0.1;
+ private final HostResourcesCalculator resourcesCalculator;
private final NodeMetricsDb metricsDb;
private final NodeRepository nodeRepository;
- private final AllocationOptimizer allocationOptimizer;
+ private final NodeResourceLimits nodeResourceLimits;
- public Autoscaler(NodeMetricsDb metricsDb, NodeRepository nodeRepository) {
+ public Autoscaler(HostResourcesCalculator resourcesCalculator,
+ NodeMetricsDb metricsDb,
+ NodeRepository nodeRepository) {
+ this.resourcesCalculator = resourcesCalculator;
this.metricsDb = metricsDb;
this.nodeRepository = nodeRepository;
- this.allocationOptimizer = new AllocationOptimizer(nodeRepository);
+ this.nodeResourceLimits = new NodeResourceLimits(nodeRepository.zone());
}
/**
@@ -53,41 +63,61 @@ public class Autoscaler {
* @return a new suggested allocation for this cluster, or empty if it should not be rescaled at this time
*/
public Optional<ClusterResources> suggest(Cluster cluster, List<Node> clusterNodes) {
- return autoscale(clusterNodes, Limits.empty())
+ return autoscale(cluster, clusterNodes, false)
.map(AllocatableClusterResources::toAdvertisedClusterResources);
}
/**
- * Autoscale a cluster by load. This returns a better allocation (if found) inside the min and max limits.
+ * Autoscale a cluster. This returns a better allocation (if found) inside the min and max limits.
*
* @param clusterNodes the list of all the active nodes in a cluster
* @return a new suggested allocation for this cluster, or empty if it should not be rescaled at this time
*/
public Optional<ClusterResources> autoscale(Cluster cluster, List<Node> clusterNodes) {
if (cluster.minResources().equals(cluster.maxResources())) return Optional.empty(); // Shortcut
- return autoscale(clusterNodes, Limits.of(cluster))
+ return autoscale(cluster, clusterNodes, true)
.map(AllocatableClusterResources::toAdvertisedClusterResources);
}
- private Optional<AllocatableClusterResources> autoscale(List<Node> clusterNodes, Limits limits) {
+ private Optional<AllocatableClusterResources> autoscale(Cluster cluster, List<Node> clusterNodes, boolean respectLimits) {
if (unstable(clusterNodes)) return Optional.empty();
ClusterSpec.Type clusterType = clusterNodes.get(0).allocation().get().membership().cluster().type();
- AllocatableClusterResources currentAllocation = new AllocatableClusterResources(clusterNodes, nodeRepository);
+ AllocatableClusterResources currentAllocation = new AllocatableClusterResources(clusterNodes, resourcesCalculator);
Optional<Double> cpuLoad = averageLoad(Resource.cpu, clusterNodes, clusterType);
Optional<Double> memoryLoad = averageLoad(Resource.memory, clusterNodes, clusterType);
Optional<Double> diskLoad = averageLoad(Resource.disk, clusterNodes, clusterType);
if (cpuLoad.isEmpty() || memoryLoad.isEmpty() || diskLoad.isEmpty()) return Optional.empty();
- var target = ResourceTarget.idealLoad(cpuLoad.get(), memoryLoad.get(), diskLoad.get(), currentAllocation);
- Optional<AllocatableClusterResources> bestAllocation =
- allocationOptimizer.findBestAllocation(target, currentAllocation, limits);
+ Optional<AllocatableClusterResources> bestAllocation = findBestAllocation(cpuLoad.get(),
+ memoryLoad.get(),
+ diskLoad.get(),
+ currentAllocation,
+ cluster,
+ respectLimits);
if (bestAllocation.isEmpty()) return Optional.empty();
if (similar(bestAllocation.get(), currentAllocation)) return Optional.empty();
return bestAllocation;
}
+ private Optional<AllocatableClusterResources> findBestAllocation(double cpuLoad, double memoryLoad, double diskLoad,
+ AllocatableClusterResources currentAllocation,
+ Cluster cluster, boolean respectLimits) {
+ Optional<AllocatableClusterResources> bestAllocation = Optional.empty();
+ for (ResourceIterator i = new ResourceIterator(cpuLoad, memoryLoad, diskLoad, currentAllocation, cluster, respectLimits);
+ i.hasNext(); ) {
+ Optional<AllocatableClusterResources> allocatableResources = toAllocatableResources(i.next(),
+ currentAllocation.clusterType(),
+ cluster,
+ respectLimits);
+ if (allocatableResources.isEmpty()) continue;
+ if (bestAllocation.isEmpty() || allocatableResources.get().preferableTo(bestAllocation.get()))
+ bestAllocation = allocatableResources;
+ }
+ return bestAllocation;
+ }
+
/** Returns true if both total real resources and total cost are similar */
private boolean similar(AllocatableClusterResources a, AllocatableClusterResources b) {
return similar(a.cost(), b.cost(), costDifferenceWorthReallocation) &&
@@ -105,6 +135,51 @@ public class Autoscaler {
}
/**
+ * Returns the smallest allocatable node resources larger than the given node resources,
+ * or empty if none available.
+ */
+ private Optional<AllocatableClusterResources> toAllocatableResources(ClusterResources resources,
+ ClusterSpec.Type clusterType,
+ Cluster cluster,
+ boolean respectLimits) {
+ NodeResources nodeResources = resources.nodeResources();
+ if (respectLimits)
+ nodeResources = cluster.capAtLimits(nodeResources);
+ nodeResources = nodeResourceLimits.enlargeToLegal(nodeResources, clusterType); // enforce system limits
+
+ if (allowsHostSharing(nodeRepository.zone().cloud())) {
+ // return the requested resources, or empty if they cannot fit on existing hosts
+ for (Flavor flavor : nodeRepository.getAvailableFlavors().getFlavors()) {
+ if (flavor.resources().satisfies(nodeResources))
+ return Optional.of(new AllocatableClusterResources(resources.with(nodeResources),
+ nodeResources,
+ resources.nodeResources(),
+ clusterType));
+ }
+ return Optional.empty();
+ }
+ else {
+ // return the cheapest flavor satisfying the target resources, if any
+ Optional<AllocatableClusterResources> best = Optional.empty();
+ for (Flavor flavor : nodeRepository.getAvailableFlavors().getFlavors()) {
+ if ( ! flavor.resources().satisfies(nodeResources)) continue;
+
+ if (flavor.resources().storageType() == NodeResources.StorageType.remote)
+ flavor = flavor.with(FlavorOverrides.ofDisk(nodeResources.diskGb()));
+ var candidate = new AllocatableClusterResources(resources.with(flavor.resources()),
+ flavor,
+ resources.nodeResources(),
+ clusterType,
+ resourcesCalculator);
+
+ if (best.isEmpty() || candidate.cost() <= best.get().cost())
+ best = Optional.of(candidate);
+ }
+ return best;
+ }
+ }
+
+ /**
* Returns the average load of this resource in the measurement window,
* or empty if we are not in a position to make decisions from these measurements at this time.
*/
@@ -125,6 +200,12 @@ public class Autoscaler {
return Duration.ofHours(12); // TODO: Measure much more often to get this down to minutes. And, ideally we should take node startup time into account
}
+ // TODO: Put this in zone config instead?
+ private boolean allowsHostSharing(CloudName cloudName) {
+ if (cloudName.value().equals("aws")) return false;
+ return true;
+ }
+
public static boolean unstable(List<Node> nodes) {
return nodes.stream().anyMatch(node -> node.status().wantToRetire() ||
node.allocation().get().membership().retired() ||