diff options
author | Jon Bratseth <bratseth@gmail.com> | 2022-11-16 15:19:21 +0100 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2022-11-16 15:19:21 +0100 |
commit | e595c0cf98f1caecd70dbc5ed14ff03967a3ced0 (patch) | |
tree | 213362b289fe7bd260c69c5ad41d925b92d85db9 /node-repository/src/main/java/com/yahoo | |
parent | b88c7d56c33166d7f77c68ca2d5d0d9c684c4017 (diff) |
Support autoscaling in dynamic shared zones
Diffstat (limited to 'node-repository/src/main/java/com/yahoo')
5 files changed, 42 insertions, 33 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocatableClusterResources.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocatableClusterResources.java index fc6ff3d0c56..0e9683a1a78 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocatableClusterResources.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocatableClusterResources.java @@ -156,7 +156,7 @@ public class AllocatableClusterResources { public static Optional<AllocatableClusterResources> from(ClusterResources wantedResources, ClusterSpec clusterSpec, Limits applicationLimits, - NodeList hosts, + List<NodeResources> availableRealHostResources, NodeRepository nodeRepository) { var systemLimits = new NodeResourceLimits(nodeRepository); boolean exclusive = nodeRepository.exclusiveAllocation(clusterSpec); @@ -168,8 +168,7 @@ public class AllocatableClusterResources { var realResources = nodeRepository.resourcesCalculator().requestToReal(advertisedResources, exclusive); // What we'll really get if ( ! systemLimits.isWithinRealLimits(realResources, clusterSpec.type())) return Optional.empty(); - - if (matchesAny(hosts, advertisedResources)) + if (anySatisfies(realResources, availableRealHostResources)) return Optional.of(new AllocatableClusterResources(wantedResources.with(realResources), advertisedResources, wantedResources, @@ -212,11 +211,8 @@ public class AllocatableClusterResources { } /** Returns true if the given resources could be allocated on any of the given host flavors */ - private static boolean matchesAny(NodeList hosts, NodeResources advertisedResources) { - // Tenant nodes should not consume more than half the resources of the biggest hosts - // to make it easier to shift them between hosts. - return hosts.stream().anyMatch(host -> host.resources().withVcpu(host.resources().vcpu() / 2) - .satisfies(advertisedResources)); + private static boolean anySatisfies(NodeResources realResources, List<NodeResources> availableRealHostResources) { + return availableRealHostResources.stream().anyMatch(realHostResources -> realHostResources.satisfies(realResources)); } private static boolean between(NodeResources min, NodeResources max, NodeResources r) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java index 9a8b01f33af..1c99ea7dc08 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java @@ -6,6 +6,7 @@ import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; +import java.util.List; import java.util.Optional; /** @@ -43,7 +44,11 @@ public class AllocationOptimizer { else limits = atLeast(minimumNodes, limits).fullySpecified(current.clusterSpec(), nodeRepository, clusterModel.application().id()); Optional<AllocatableClusterResources> bestAllocation = Optional.empty(); - NodeList hosts = nodeRepository.nodes().list().hosts(); + var availableRealHostResources = nodeRepository.zone().cloud().dynamicProvisioning() + ? nodeRepository.flavors().getFlavors().stream().map(flavor -> flavor.resources()).toList() + : nodeRepository.nodes().list().hosts().stream().map(host -> host.flavor().resources()) + .map(hostResources -> maxResourcesOf(hostResources, clusterModel)) + .toList(); for (int groups = limits.min().groups(); groups <= limits.max().groups(); groups++) { for (int nodes = limits.min().nodes(); nodes <= limits.max().nodes(); nodes++) { if (nodes % groups != 0) continue; @@ -53,7 +58,7 @@ public class AllocationOptimizer { nodeResourcesWith(nodes, groups, limits, targetLoad, current, clusterModel)); var allocatableResources = AllocatableClusterResources.from(resources, current.clusterSpec(), limits, - hosts, nodeRepository); + availableRealHostResources, nodeRepository); if (allocatableResources.isEmpty()) continue; if (bestAllocation.isEmpty() || allocatableResources.get().preferableTo(bestAllocation.get())) bestAllocation = allocatableResources; @@ -62,6 +67,13 @@ public class AllocationOptimizer { return bestAllocation; } + /** Returns the max resources of a host one node may allocate. */ + private NodeResources maxResourcesOf(NodeResources hostResources, ClusterModel clusterModel) { + if (nodeRepository.exclusiveAllocation(clusterModel.clusterSpec())) return hostResources; + // static, shared hosts: Allocate at most half of the host cpu to simplify management + return hostResources.withVcpu(hostResources.vcpu() / 2); + } + /** * For the observed load this instance is initialized with, returns the resources needed per node to be at * the target relative load, given a target node and group count. diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java index 46dd88b78d0..5ee4bdddd02 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java @@ -127,14 +127,14 @@ class NodeAllocation { boolean resizeable = requestedNodes.considerRetiring() && candidate.isResizable; boolean acceptToRetire = acceptToRetire(candidate); - if ((! saturated() && hasCompatibleFlavor(candidate) && requestedNodes.acceptable(candidate)) || acceptToRetire) { + if ((! saturated() && hasCompatibleResources(candidate) && requestedNodes.acceptable(candidate)) || acceptToRetire) { candidate = candidate.withNode(); if (candidate.isValid()) { acceptNode(candidate, shouldRetire(candidate, candidates), resizeable); } } } - else if (! saturated() && hasCompatibleFlavor(candidate)) { + else if (! saturated() && hasCompatibleResources(candidate)) { if (! nodeResourceLimits.isWithinRealLimits(candidate, cluster)) { ++rejectedDueToInsufficientRealResources; continue; @@ -169,7 +169,7 @@ class NodeAllocation { } if ( ! nodeResourceLimits.isWithinRealLimits(candidate, cluster)) return Retirement.outsideRealLimits; if (violatesParentHostPolicy(candidate)) return Retirement.violatesParentHostPolicy; - if ( ! hasCompatibleFlavor(candidate)) return Retirement.incompatibleFlavor; + if ( ! hasCompatibleResources(candidate)) return Retirement.incompatibleResources; if (candidate.wantToRetire()) return Retirement.hardRequest; if (candidate.preferToRetire() && candidate.replaceableBy(candidates)) return Retirement.softRequest; if (violatesExclusivity(candidate)) return Retirement.violatesExclusivity; @@ -241,11 +241,11 @@ class NodeAllocation { if (! requestedNodes.considerRetiring()) return false; return cluster.isStateful() || - (cluster.type() == ClusterSpec.Type.container && !hasCompatibleFlavor(candidate)); + (cluster.type() == ClusterSpec.Type.container && !hasCompatibleResources(candidate)); } - private boolean hasCompatibleFlavor(NodeCandidate candidate) { - return requestedNodes.isCompatible(candidate.flavor(), nodeRepository.flavors()) || candidate.isResizable; + private boolean hasCompatibleResources(NodeCandidate candidate) { + return requestedNodes.isCompatible(candidate.resources()) || candidate.isResizable; } private Node acceptNode(NodeCandidate candidate, Retirement retirement, boolean resizeable) { @@ -391,7 +391,7 @@ class NodeAllocation { } else if (deltaRetiredCount < 0) { // unretire until deltaRetiredCount is 0 for (NodeCandidate candidate : byUnretiringPriority(nodes.values())) { - if ( candidate.allocation().get().membership().retired() && hasCompatibleFlavor(candidate) ) { + if (candidate.allocation().get().membership().retired() && hasCompatibleResources(candidate) ) { candidate = candidate.withNode(); if (candidate.isResizable) candidate = candidate.withNode(resize(candidate.toNode())); @@ -482,7 +482,7 @@ class NodeAllocation { alreadyRetired("node is already retired"), outsideRealLimits("node real resources is outside limits"), violatesParentHostPolicy("node violates parent host policy"), - incompatibleFlavor("node flavor is incompatible"), + incompatibleResources("node resources are incompatible"), hardRequest("node is requested to retire"), softRequest("node is requested to retire (soft)"), violatesExclusivity("node violates host exclusivity"), diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java index a2f5eabf447..54c24977fef 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java @@ -28,8 +28,8 @@ public interface NodeSpec { /** Returns whether the hosts running the nodes of this application can also run nodes of other applications. */ boolean isExclusive(); - /** Returns whether the given flavor is compatible with this spec */ - boolean isCompatible(Flavor flavor, NodeFlavors flavors); + /** Returns whether the given node resources is compatible with this spec */ + boolean isCompatible(NodeResources resources); /** Returns whether the given node count is sufficient to consider this spec fulfilled to the maximum amount */ boolean saturatedBy(int count); @@ -115,12 +115,8 @@ public interface NodeSpec { public NodeType type() { return NodeType.tenant; } @Override - public boolean isCompatible(Flavor flavor, NodeFlavors flavors) { - if (flavor.isDocker()) { // Docker nodes can satisfy a request for parts of their resources - return flavor.resources().compatibleWith(requestedNodeResources); - } else { // Other nodes must be matched exactly - return requestedNodeResources.equals(flavor.resources()); - } + public boolean isCompatible(NodeResources resources) { + return requestedNodeResources.compatibleWith(resources); } @Override @@ -208,7 +204,7 @@ public interface NodeSpec { public boolean isExclusive() { return false; } @Override - public boolean isCompatible(Flavor flavor, NodeFlavors flavors) { return true; } + public boolean isCompatible(NodeResources resources) { return true; } @Override public boolean saturatedBy(int count) { return false; } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java index aa4c763fcdc..5a8c5221c47 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.testutils; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Cloud; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; @@ -45,19 +46,21 @@ public class MockHostProvisioner implements HostProvisioner { private int deprovisionedHosts = 0; private EnumSet<Behaviour> behaviours = EnumSet.noneOf(Behaviour.class); private Optional<Flavor> hostFlavor = Optional.empty(); + private Cloud cloud; - public MockHostProvisioner(List<Flavor> flavors, MockNameResolver nameResolver, int memoryTaxGb) { + public MockHostProvisioner(List<Flavor> flavors, MockNameResolver nameResolver, int memoryTaxGb, Cloud cloud) { this.flavors = List.copyOf(flavors); this.nameResolver = nameResolver; this.memoryTaxGb = memoryTaxGb; + this.cloud = cloud; } - public MockHostProvisioner(List<Flavor> flavors) { - this(flavors, 0); + public MockHostProvisioner(List<Flavor> flavors, Cloud cloud) { + this(flavors, 0, cloud); } - public MockHostProvisioner(List<Flavor> flavors, int memoryTaxGb) { - this(flavors, new MockNameResolver().mockAnyLookup(), memoryTaxGb); + public MockHostProvisioner(List<Flavor> flavors, int memoryTaxGb, Cloud cloud) { + this(flavors, new MockNameResolver().mockAnyLookup(), memoryTaxGb, cloud); } @Override @@ -65,8 +68,10 @@ public class MockHostProvisioner implements HostProvisioner { ApplicationId applicationId, Version osVersion, HostSharing sharing, Optional<ClusterSpec.Type> clusterType, CloudAccount cloudAccount, Consumer<List<ProvisionedHost>> provisionedHostsConsumer) { + boolean exclusive = sharing == HostSharing.exclusive || ! cloud.allowHostSharing(); Flavor hostFlavor = this.hostFlavor.orElseGet(() -> flavors.stream() - .filter(f -> compatible(f, resources)) + .filter(f -> exclusive ? compatible(f, resources) + : f.resources().satisfies(resources)) .findFirst() .orElseThrow(() -> new NodeAllocationException("No host flavor matches " + resources, true))); List<ProvisionedHost> hosts = new ArrayList<>(); |