diff options
author | Jon Bratseth <bratseth@gmail.com> | 2023-01-21 17:50:10 +0100 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2023-01-21 17:50:10 +0100 |
commit | 7a6af9caa065b3ab63b094d78b7347d7df6bea0f (patch) | |
tree | 48e80650a9cd88016ff8e618d85d727a28f3542c /node-repository/src/main/java/com | |
parent | 00d86602a88c66486c8f4c68a1c8bdff096c7273 (diff) |
Support a group size constraint in content clusters
Diffstat (limited to 'node-repository/src/main/java/com')
8 files changed, 85 insertions, 32 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java index 0a731f66418..60a5682101f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.applications; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -28,6 +29,7 @@ public class Cluster { private final ClusterSpec.Id id; private final boolean exclusive; private final ClusterResources min, max; + private final IntRange groupSize; private final boolean required; private final Autoscaling suggested; private final Autoscaling target; @@ -39,6 +41,7 @@ public class Cluster { boolean exclusive, ClusterResources minResources, ClusterResources maxResources, + IntRange groupSize, boolean required, Autoscaling suggested, Autoscaling target, @@ -47,6 +50,7 @@ public class Cluster { this.exclusive = exclusive; this.min = Objects.requireNonNull(minResources); this.max = Objects.requireNonNull(maxResources); + this.groupSize = Objects.requireNonNull(groupSize); this.required = required; this.suggested = Objects.requireNonNull(suggested); Objects.requireNonNull(target); @@ -68,6 +72,9 @@ public class Cluster { /** Returns the configured maximal resources in this cluster */ public ClusterResources maxResources() { return max; } + /** Returns the configured group size range in this cluster */ + public IntRange groupSize() { return groupSize; } + /** * Returns whether the resources of this cluster are required to be within the specified min and max. * Otherwise they may be adjusted by capacity policies. @@ -105,16 +112,16 @@ public class Cluster { public Cluster withConfiguration(boolean exclusive, Capacity capacity) { return new Cluster(id, exclusive, - capacity.minResources(), capacity.maxResources(), capacity.isRequired(), + capacity.minResources(), capacity.maxResources(), capacity.groupSize(), capacity.isRequired(), suggested, target, scalingEvents); } public Cluster withSuggested(Autoscaling suggested) { - return new Cluster(id, exclusive, min, max, required, suggested, target, scalingEvents); + return new Cluster(id, exclusive, min, max, groupSize, required, suggested, target, scalingEvents); } public Cluster withTarget(Autoscaling target) { - return new Cluster(id, exclusive, min, max, required, suggested, target, scalingEvents); + return new Cluster(id, exclusive, min, max, groupSize, required, suggested, target, scalingEvents); } /** Add or update (based on "at" time) a scaling event */ @@ -128,7 +135,7 @@ public class Cluster { scalingEvents.add(scalingEvent); prune(scalingEvents); - return new Cluster(id, exclusive, min, max, required, suggested, target, scalingEvents); + return new Cluster(id, exclusive, min, max, groupSize, required, suggested, target, scalingEvents); } @Override @@ -158,7 +165,8 @@ public class Cluster { } public static Cluster create(ClusterSpec.Id id, boolean exclusive, Capacity requested) { - return new Cluster(id, exclusive, requested.minResources(), requested.maxResources(), requested.isRequired(), + return new Cluster(id, exclusive, + requested.minResources(), requested.maxResources(), requested.groupSize(), requested.isRequired(), Autoscaling.empty(), Autoscaling.empty(), List.of()); } 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 56a6d61ebee..c3dc0ff250f 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 @@ -1,6 +1,7 @@ // Copyright Yahoo. 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.collections.IntRange; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -38,7 +39,8 @@ public class AllocationOptimizer { int minimumNodes = AllocationOptimizer.minimumNodes; if (limits.isEmpty()) limits = Limits.of(new ClusterResources(minimumNodes, 1, NodeResources.unspecified()), - new ClusterResources(maximumNodes, maximumNodes, NodeResources.unspecified())); + new ClusterResources(maximumNodes, maximumNodes, NodeResources.unspecified()), + IntRange.empty()); else limits = atLeast(minimumNodes, limits).fullySpecified(current.clusterSpec(), nodeRepository, clusterModel.application().id()); Optional<AllocatableClusterResources> bestAllocation = Optional.empty(); @@ -50,6 +52,7 @@ public class AllocationOptimizer { 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; + if ( ! limits.groupSize().includes(nodes / groups)) continue; var resources = new ClusterResources(nodes, groups, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java index cb5d8dd5042..9de7134a6d7 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java @@ -1,6 +1,7 @@ // Copyright Yahoo. 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.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; @@ -19,13 +20,15 @@ import java.util.Objects; */ public class Limits { - private static final Limits empty = new Limits(null, null); + private static final Limits empty = new Limits(null, null, IntRange.empty()); private final ClusterResources min, max; + private final IntRange groupSize; - private Limits(ClusterResources min, ClusterResources max) { + private Limits(ClusterResources min, ClusterResources max, IntRange groupSize) { this.min = min; this.max = max; + this.groupSize = groupSize; } public static Limits empty() { return empty; } @@ -42,12 +45,14 @@ public class Limits { return max; } + public IntRange groupSize() { return groupSize; } + public Limits withMin(ClusterResources min) { - return new Limits(min, max); + return new Limits(min, max, groupSize); } public Limits withMax(ClusterResources max) { - return new Limits(min, max); + return new Limits(min, max, groupSize); } /** Caps the given resources at the limits of this. If it is empty the node resources are returned as-is */ @@ -66,7 +71,7 @@ public class Limits { var defaultResources = new CapacityPolicies(nodeRepository).defaultNodeResources(clusterSpec, applicationId); var specifiedMin = min.nodeResources().isUnspecified() ? min.with(defaultResources) : min; var specifiedMax = max.nodeResources().isUnspecified() ? max.with(defaultResources) : max; - return new Limits(specifiedMin, specifiedMax); + return new Limits(specifiedMin, specifiedMax, groupSize); } private double between(double min, double max, double value) { @@ -76,21 +81,23 @@ public class Limits { } public static Limits of(Cluster cluster) { - return new Limits(cluster.minResources(), cluster.maxResources()); + return new Limits(cluster.minResources(), cluster.maxResources(), cluster.groupSize()); } public static Limits of(Capacity capacity) { - return new Limits(capacity.minResources(), capacity.maxResources()); + return new Limits(capacity.minResources(), capacity.maxResources(), capacity.groupSize()); } - public static Limits of(ClusterResources min, ClusterResources max) { - return new Limits(Objects.requireNonNull(min, "min"), Objects.requireNonNull(max, "max")); + public static Limits of(ClusterResources min, ClusterResources max, IntRange groupSize) { + return new Limits(Objects.requireNonNull(min, "min"), + Objects.requireNonNull(max, "max"), + Objects.requireNonNull(groupSize, "groupSize")); } @Override public String toString() { if (isEmpty()) return "no limits"; - return "limits: from " + min + " to " + max; + return "limits: from " + min + " to " + max + ( groupSize.isEmpty() ? "" : " with group size " + groupSize); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java index 2f5b057e927..4d2fae8a4d7 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.persistence; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -23,6 +24,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; /** * Application JSON serializer @@ -48,6 +50,7 @@ public class ApplicationSerializer { private static final String exclusiveKey = "exclusive"; private static final String minResourcesKey = "min"; private static final String maxResourcesKey = "max"; + private static final String groupSizeKey = "groupSize"; private static final String requiredKey = "required"; private static final String suggestedKey = "suggested"; private static final String resourcesKey = "resources"; @@ -122,6 +125,7 @@ public class ApplicationSerializer { clusterObject.setBool(exclusiveKey, cluster.exclusive()); toSlime(cluster.minResources(), clusterObject.setObject(minResourcesKey)); toSlime(cluster.maxResources(), clusterObject.setObject(maxResourcesKey)); + toSlime(cluster.groupSize(), clusterObject.setObject(groupSizeKey)); clusterObject.setBool(requiredKey, cluster.required()); toSlime(cluster.suggested(), clusterObject.setObject(suggestedKey)); toSlime(cluster.target(), clusterObject.setObject(targetKey)); @@ -133,6 +137,7 @@ public class ApplicationSerializer { clusterObject.field(exclusiveKey).asBool(), clusterResourcesFromSlime(clusterObject.field(minResourcesKey)), clusterResourcesFromSlime(clusterObject.field(maxResourcesKey)), + intRangeFromSlime(clusterObject.field(groupSizeKey)), clusterObject.field(requiredKey).asBool(), autoscalingFromSlime(clusterObject.field(suggestedKey), clusterObject.field("nonExisting")), autoscalingFromSlime(clusterObject.field(targetKey), clusterObject.field(autoscalingStatusObjectKey)), @@ -165,6 +170,16 @@ public class ApplicationSerializer { NodeResourcesSerializer.resourcesFromSlime(clusterResourcesObject.field(nodeResourcesKey))); } + private static void toSlime(IntRange range, Cursor rangeObject) { + range.from().ifPresent(from -> rangeObject.setLong(fromKey, from)); + range.to().ifPresent(from -> rangeObject.setLong(toKey, from)); + } + + private static IntRange intRangeFromSlime(Inspector rangeObject) { + if ( ! rangeObject.valid()) return IntRange.empty(); + return new IntRange(optionalInt(rangeObject.field(fromKey)), optionalInt(rangeObject.field(toKey))); + } + private static void toSlime(Load load, Cursor loadObject) { loadObject.setDouble(cpuKey, load.cpu()); loadObject.setDouble(memoryKey, load.memory()); @@ -257,4 +272,8 @@ public class ApplicationSerializer { return inspector.valid() ? Optional.of(Instant.ofEpochMilli(inspector.asLong())) : Optional.empty(); } + private static OptionalInt optionalInt(Inspector inspector) { + return inspector.valid() ? OptionalInt.of((int)inspector.asLong()) : OptionalInt.empty(); + } + } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java index a1400626658..081de1fd8dc 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java @@ -39,8 +39,11 @@ public class CapacityPolicies { } public Capacity applyOn(Capacity capacity, ApplicationId application, boolean exclusive) { - return capacity.withLimits(applyOn(capacity.minResources(), capacity, application, exclusive), - applyOn(capacity.maxResources(), capacity, application, exclusive)); + var min = applyOn(capacity.minResources(), capacity, application, exclusive); + var max = applyOn(capacity.maxResources(), capacity, application, exclusive); + var groupSize = capacity.groupSize().fromAtMost(max.nodes() / max.groups()) + .toAtLeast(min.nodes() / min.groups()); + return capacity.withLimits(min, max, groupSize); } private ClusterResources applyOn(ClusterResources resources, Capacity capacity, ApplicationId application, boolean exclusive) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java index 59e67f65f8f..bc623b495c8 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.provisioning; +import com.yahoo.collections.IntRange; import com.yahoo.component.annotation.Inject; import com.yahoo.config.provision.ActivationContext; import com.yahoo.config.provision.ApplicationId; @@ -98,7 +99,7 @@ public class NodeRepositoryProvisioner implements Provisioner { cluster = capacityPolicies.decideExclusivity(requested, cluster); Capacity actual = capacityPolicies.applyOn(requested, application, cluster.isExclusive()); ClusterResources target = decideTargetResources(application, cluster, actual); - ensureRedundancy(target.nodes(), cluster, actual.canFail(), application); + validate(actual, target, cluster, application); logIfDownscaled(requested.minResources().nodes(), actual.minResources().nodes(), cluster, logger); groups = target.groups(); @@ -204,18 +205,17 @@ public class NodeRepositoryProvisioner implements Provisioner { .advertisedResources(); } - /** - * Throw if the node count is 1 for container and content clusters and we're in a production zone - * - * @throws IllegalArgumentException if only one node is requested and we can fail - */ - private void ensureRedundancy(int nodeCount, ClusterSpec cluster, boolean canFail, ApplicationId application) { - if (! application.instance().isTester() && - canFail && - nodeCount == 1 && - requiresRedundancy(cluster.type()) && - zone.environment().isProduction()) - throw new IllegalArgumentException("Deployments to prod require at least 2 nodes per cluster for redundancy. Not fulfilled for " + cluster); + private void validate(Capacity actual, ClusterResources target, ClusterSpec cluster, ApplicationId application) { + if ( ! actual.canFail()) return; + + if (! application.instance().isTester() && zone.environment().isProduction() && + requiresRedundancy(cluster.type()) && target.nodes() == 1) + throw new IllegalArgumentException("In " + cluster + + ": Deployments to prod require at least 2 nodes per cluster for redundancy."); + + if ( ! actual.groupSize().includes(target.nodes() / target.groups())) + throw new IllegalArgumentException("In " + cluster + ": Group size with " + target + + " is not within allowed group size " + actual.groupSize()); } private static boolean requiresRedundancy(ClusterSpec.Type clusterType) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java index cb927c72eb5..6936a767936 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.restapi; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.ClusterResources; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; @@ -62,6 +63,8 @@ public class ApplicationSerializer { Limits limits = Limits.of(cluster).fullySpecified(nodes.clusterSpec(), nodeRepository, application.id()); toSlime(limits.min(), clusterObject.setObject("min")); toSlime(limits.max(), clusterObject.setObject("max")); + if ( ! cluster.groupSize().isEmpty()) + toSlime(cluster.groupSize(), clusterObject.setObject("groupSize")); toSlime(currentResources, clusterObject.setObject("current")); if (cluster.shouldSuggestResources(currentResources)) toSlime(cluster.suggested(), clusterObject.setObject("suggested")); @@ -85,6 +88,11 @@ public class ApplicationSerializer { NodeResourcesSerializer.toSlime(resources.nodeResources(), clusterResourcesObject.setObject("resources")); } + private static void toSlime(IntRange range, Cursor rangeObject) { + range.from().ifPresent(from -> rangeObject.setLong("from", range.from().getAsInt())); + range.to().ifPresent(to -> rangeObject.setLong("to", range.to().getAsInt())); + } + private static void toSlime(Load load, Cursor utilizationObject) { utilizationObject.setDouble("cpu", load.cpu()); utilizationObject.setDouble("memory", load.memory()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java index 92ffe9828c3..e50db5c0d55 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.testutils; +import com.yahoo.collections.IntRange; import com.yahoo.component.Version; import com.yahoo.config.provision.ActivationContext; import com.yahoo.config.provision.ApplicationId; @@ -193,7 +194,11 @@ public class MockNodeRepository extends NodeRepository { activate(provisioner.prepare(app1Id, cluster1Id, Capacity.from(new ClusterResources(2, 1, new NodeResources(2, 8, 50, 1)), - new ClusterResources(8, 2, new NodeResources(4, 16, 1000, 1)), false, true), + new ClusterResources(8, 2, new NodeResources(4, 16, 1000, 1)), + IntRange.empty(), + false, + true, + Optional.empty()), null), app1Id, provisioner); Application app1 = applications().get(app1Id).get(); Cluster cluster1 = app1.cluster(cluster1Id.id()).get(); |