diff options
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.java | 103 |
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() || |