diff options
author | Jon Bratseth <bratseth@verizonmedia.com> | 2019-05-06 10:02:49 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@verizonmedia.com> | 2019-05-06 10:02:49 +0200 |
commit | 1add32ea899b62a38008cc460a42437e15f31b15 (patch) | |
tree | a8d5ecaa20880676be7af49617319eb88cfefa36 /config-model | |
parent | 21a212f66f4491ad1ae42349139ec9ec16973fa2 (diff) |
Allow node allocation by resource spec
Diffstat (limited to 'config-model')
10 files changed, 118 insertions, 46 deletions
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java index c744c509b9a..2439475e95c 100644 --- a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java +++ b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.ProvisionLogger; import java.util.ArrayList; @@ -42,42 +43,47 @@ public class InMemoryProvisioner implements HostProvisioner { /** Hosts which should be returned as retired */ private final Set<String> retiredHostNames; - /** Free hosts of each flavor */ - private final ListMap<String, Host> freeNodes = new ListMap<>(); + /** Free hosts of each resource size */ + private final ListMap<NodeResources, Host> freeNodes = new ListMap<>(); private final Map<String, HostSpec> legacyMapping = new LinkedHashMap<>(); private final Map<ClusterSpec, List<HostSpec>> allocations = new LinkedHashMap<>(); /** Indexes must be unique across all groups in a cluster */ - private final Map<Pair<ClusterSpec.Type,ClusterSpec.Id>, Integer> nextIndexInCluster = new HashMap<>(); + private final Map<Pair<ClusterSpec.Type, ClusterSpec.Id>, Integer> nextIndexInCluster = new HashMap<>(); /** Use this index as start index for all clusters */ private final int startIndexForClusters; /** Creates this with a number of nodes of the flavor 'default' */ public InMemoryProvisioner(int nodeCount) { - this(Collections.singletonMap("default", createHostInstances(nodeCount)), true, 0); + this(Collections.singletonMap(NodeResources.fromLegacyName("default"), + createHostInstances(nodeCount)), true, 0); } /** Creates this with a set of host names of the flavor 'default' */ public InMemoryProvisioner(boolean failOnOutOfCapacity, String... hosts) { - this(Collections.singletonMap("default", toHostInstances(hosts)), failOnOutOfCapacity, 0); + this(Collections.singletonMap(NodeResources.fromLegacyName("default"), + toHostInstances(hosts)), failOnOutOfCapacity, 0); } /** Creates this with a set of hosts of the flavor 'default' */ public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, String ... retiredHostNames) { - this(Collections.singletonMap("default", hosts.asCollection()), failOnOutOfCapacity, 0, retiredHostNames); + this(Collections.singletonMap(NodeResources.fromLegacyName("default"), + hosts.asCollection()), failOnOutOfCapacity, 0, retiredHostNames); } /** Creates this with a set of hosts of the flavor 'default' */ public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) { - this(Collections.singletonMap("default", hosts.asCollection()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames); + this(Collections.singletonMap(NodeResources.fromLegacyName("default"), + hosts.asCollection()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames); } - public InMemoryProvisioner(Map<String, Collection<Host>> hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) { + public InMemoryProvisioner(Map<NodeResources, Collection<Host>> hosts, boolean failOnOutOfCapacity, + int startIndexForClusters, String ... retiredHostNames) { this.failOnOutOfCapacity = failOnOutOfCapacity; - for (Map.Entry<String, Collection<Host>> hostsOfFlavor : hosts.entrySet()) - for (Host host : hostsOfFlavor.getValue()) - freeNodes.put(hostsOfFlavor.getKey(), host); + for (Map.Entry<NodeResources, Collection<Host>> hostsWithResources : hosts.entrySet()) + for (Host host : hostsWithResources.getValue()) + freeNodes.put(hostsWithResources.getKey(), host); this.retiredHostNames = new HashSet<>(Arrays.asList(retiredHostNames)); this.startIndexForClusters = startIndexForClusters; } @@ -104,9 +110,9 @@ public class InMemoryProvisioner implements HostProvisioner { @Override public HostSpec allocateHost(String alias) { if (legacyMapping.containsKey(alias)) return legacyMapping.get(alias); - List<Host> defaultHosts = freeNodes.get("default"); + List<Host> defaultHosts = freeNodes.get(NodeResources.fromLegacyName("default")); if (defaultHosts.isEmpty()) throw new IllegalArgumentException("No more hosts of default flavor available"); - Host newHost = freeNodes.removeValue("default", 0); + Host newHost = freeNodes.removeValue(NodeResources.fromLegacyName("default"), 0); HostSpec hostSpec = new HostSpec(newHost.hostname(), newHost.aliases(), newHost.flavor(), Optional.empty(), newHost.version()); legacyMapping.put(alias, hostSpec); return hostSpec; @@ -122,16 +128,16 @@ public class InMemoryProvisioner implements HostProvisioner { int capacity = failOnOutOfCapacity || requestedCapacity.isRequired() ? requestedCapacity.nodeCount() - : Math.min(requestedCapacity.nodeCount(), freeNodes.get("default").size() + totalAllocatedTo(cluster)); + : Math.min(requestedCapacity.nodeCount(), freeNodes.get(NodeResources.fromLegacyName("default")).size() + totalAllocatedTo(cluster)); if (groups > capacity) groups = capacity; - String flavor = requestedCapacity.flavor().orElse("default"); + NodeResources nodeResources = requestedCapacity.nodeResources().orElse(NodeResources.fromLegacyName("default")); List<HostSpec> allocation = new ArrayList<>(); if (groups == 1) { allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(0))), - flavor, + nodeResources, capacity, startIndexForClusters, requestedCapacity.canFail())); @@ -139,7 +145,7 @@ public class InMemoryProvisioner implements HostProvisioner { else { for (int i = 0; i < groups; i++) { allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(i))), - flavor, + nodeResources, capacity / groups, allocation.size(), requestedCapacity.canFail())); @@ -161,19 +167,19 @@ public class InMemoryProvisioner implements HostProvisioner { host.version()); } - private List<HostSpec> allocateHostGroup(ClusterSpec clusterGroup, String flavor, int nodesInGroup, int startIndex, boolean canFail) { + private List<HostSpec> allocateHostGroup(ClusterSpec clusterGroup, NodeResources nodeResources, int nodesInGroup, int startIndex, boolean canFail) { List<HostSpec> allocation = allocations.getOrDefault(clusterGroup, new ArrayList<>()); allocations.put(clusterGroup, allocation); int nextIndex = nextIndexInCluster.getOrDefault(new Pair<>(clusterGroup.type(), clusterGroup.id()), startIndex); while (allocation.size() < nodesInGroup) { - if (freeNodes.get(flavor).isEmpty()) { + if (freeNodes.get(nodeResources).isEmpty()) { if (canFail) - throw new IllegalArgumentException("Insufficient capacity of flavor '" + flavor + "'"); + throw new IllegalArgumentException("Insufficient capacity of for " + nodeResources); else break; } - Host newHost = freeNodes.removeValue(flavor, 0); + Host newHost = freeNodes.removeValue(nodeResources, 0); ClusterMembership membership = ClusterMembership.from(clusterGroup, nextIndex++); allocation.add(new HostSpec(newHost.hostname(), newHost.aliases(), newHost.flavor(), Optional.of(membership), newHost.version())); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java index 1f649b122fa..d34a11abdf4 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java @@ -176,6 +176,14 @@ public class ModelElement { return xml.getAttribute(name); } + /** Returns the content of the attribute with the given name or throws IllegalArgumentException if not present */ + public String requiredStringAttribute(String name) { + if (stringAttribute(name) == null) + throw new IllegalArgumentException("Required attribute '" + name + "' is missing"); + return stringAttribute(name); + } + + public List<ModelElement> subElements(String name) { List<Element> elements = XML.getChildren(xml, name); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java index 1031ae7b787..7864623f251 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java @@ -7,7 +7,7 @@ import com.yahoo.config.model.ConfigModelContext; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.config.provision.FlavorSpec; +import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.RotationName; import com.yahoo.vespa.model.HostResource; import com.yahoo.vespa.model.HostSystem; @@ -44,15 +44,15 @@ public class NodesSpecification { private final boolean exclusive; - /** The flavor the nodes should have, or empty to use the default */ - private final Optional<FlavorSpec> flavor; + /** The resources each node should have, or empty to use the default */ + private final Optional<NodeResources> resources; /** The identifier of the custom docker image layer to use (not supported yet) */ private final Optional<String> dockerImage; private NodesSpecification(boolean dedicated, int count, int groups, Version version, boolean required, boolean canFail, boolean exclusive, - Optional<FlavorSpec> flavor, Optional<String> dockerImage) { + Optional<NodeResources> resources, Optional<String> dockerImage) { this.dedicated = dedicated; this.count = count; this.groups = groups; @@ -60,7 +60,7 @@ public class NodesSpecification { this.required = required; this.canFail = canFail; this.exclusive = exclusive; - this.flavor = flavor; + this.resources = resources; this.dockerImage = dockerImage; } @@ -170,28 +170,65 @@ public class NodesSpecification { DeployLogger logger, Set<RotationName> rotations) { ClusterSpec cluster = ClusterSpec.request(clusterType, clusterId, version, exclusive, rotations); - return hostSystem.allocateHosts(cluster, Capacity.fromCount(count, flavor, required, canFail), groups, logger); + return hostSystem.allocateHosts(cluster, Capacity.fromCount(count, resources, required, canFail), groups, logger); } - private static Optional<FlavorSpec> getFlavor(ModelElement nodesElement) { - ModelElement flavor = nodesElement.child("flavor"); + private static Optional<NodeResources> getFlavor(ModelElement nodesElement) { + ModelElement flavor = nodesElement.child("resources"); if (flavor != null) { - return Optional.of(new FlavorSpec(flavor.requiredDoubleAttribute("cpus"), - flavor.requiredDoubleAttribute("memory"), - flavor.requiredDoubleAttribute("disk"))); + return Optional.of(new NodeResources(flavor.requiredDoubleAttribute("vcpu"), + parseGbAmount(flavor.requiredStringAttribute("memory")), + parseGbAmount(flavor.requiredStringAttribute("disk")))); } else if (nodesElement.stringAttribute("flavor") != null) { // legacy fallback - return Optional.of(FlavorSpec.fromLegacyFlavorName(nodesElement.stringAttribute("flavor"))); + return Optional.of(NodeResources.fromLegacyName(nodesElement.stringAttribute("flavor"))); } else { // Get the default return Optional.empty(); } } + private static double parseGbAmount(String byteAmount) { + byteAmount = byteAmount.strip(); + byteAmount = byteAmount.toUpperCase(); + if (byteAmount.endsWith("B")) + byteAmount = byteAmount.substring(0, byteAmount.length() -1); + + double multiplier = 1/1000^3; + if (byteAmount.endsWith("K")) + multiplier = 1/1000^2; + else if (byteAmount.endsWith("M")) + multiplier = 1/1000; + else if (byteAmount.endsWith("G")) + multiplier = 1; + else if (byteAmount.endsWith("T")) + multiplier = 1000; + else if (byteAmount.endsWith("P")) + multiplier = 1000^2; + else if (byteAmount.endsWith("E")) + multiplier = 1000^3; + else if (byteAmount.endsWith("Z")) + multiplier = 1000^4; + else if (byteAmount.endsWith("Y")) + multiplier = 1000^5; + else + throw new IllegalArgumentException("Invalid byte amount '" + byteAmount + + "': Must end with k, M, G, T, P, E, Z or Y"); + + byteAmount = byteAmount.substring(0, byteAmount.length() -1 ).strip(); + try { + return Double.parseDouble(byteAmount) * multiplier; + } + catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid byte amount '" + byteAmount + + "': Must be a floating point number followed by k, M, G, T, P, E, Z or Y"); + } + } + @Override public String toString() { return "specification of " + count + (dedicated ? " dedicated " : " ") + "nodes" + - (flavor.isPresent() ? " of flavor " + flavor.get() : "") + + (resources.isPresent() ? " with resources " + resources.get() : "") + (groups > 1 ? " in " + groups + " groups" : ""); } diff --git a/config-model/src/main/resources/schema/common.rnc b/config-model/src/main/resources/schema/common.rnc index 73882da2b01..59b40f433b5 100644 --- a/config-model/src/main/resources/schema/common.rnc +++ b/config-model/src/main/resources/schema/common.rnc @@ -19,7 +19,14 @@ JavaId = xsd:string { pattern = "([a-zA-Z_$][a-zA-Z\d_$]*\.)*[a-zA-Z_$][a-zA-Z\d Nodes = element nodes { attribute count { xsd:positiveInteger } & attribute flavor { xsd:string }? & - attribute docker-image { xsd:string }? + attribute docker-image { xsd:string }? & + Resources? +} + +Resources = element resources { + attribute vcpu { xsd:double { minExclusive = "0.0" } } & + attribute memory { xsd:string } & + attribute disk { xsd:string } } OptionalDedicatedNodes = element nodes { diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc index 6bc54c433f3..d3a3b26c635 100644 --- a/config-model/src/main/resources/schema/containercluster.rnc +++ b/config-model/src/main/resources/schema/containercluster.rnc @@ -215,6 +215,7 @@ NodesOfContainerCluster = element nodes { attribute preload { text }? & attribute allocated-memory { text }? & attribute cpu-socket-affinity { xsd:boolean }? & + Resources? & element environment-variables { anyElement + } ? & diff --git a/config-model/src/main/resources/schema/content.rnc b/config-model/src/main/resources/schema/content.rnc index c23f99518cb..0686708a8a1 100644 --- a/config-model/src/main/resources/schema/content.rnc +++ b/config-model/src/main/resources/schema/content.rnc @@ -208,6 +208,7 @@ ContentNode = element node { } ContentNodes = element nodes { + Resources? & attribute cpu-socket-affinity { xsd:string }? & attribute mmap-core-limit { xsd:nonNegativeInteger }? & attribute core-on-oom { xsd:boolean }? & diff --git a/config-model/src/main/resources/schema/docproc.rnc b/config-model/src/main/resources/schema/docproc.rnc index b5bd85ba095..3ee249c89d4 100644 --- a/config-model/src/main/resources/schema/docproc.rnc +++ b/config-model/src/main/resources/schema/docproc.rnc @@ -37,6 +37,7 @@ ClusterV3 = element cluster { DocprocClusterAttributes? & element nodes { + Resources? & attribute jvmargs { text }? & attribute preload { text }? & element node { diff --git a/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java b/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java index 6c9b9fdc084..06c9f78456c 100644 --- a/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java +++ b/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java @@ -704,7 +704,7 @@ public class ModelProvisioningTest { } @Test - public void testSlobroksClustersAreExpandedToIncludeRetiredNodesWhenRetiredComesLast() throws ParseException { + public void testSlobroksClustersAreExpandedToIncludeRetiredNodesWhenRetiredComesLast() { String services = "<?xml version='1.0' encoding='utf-8' ?>\n" + "<services>" + @@ -718,7 +718,7 @@ public class ModelProvisioningTest { VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, "default09", "default08"); - assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts)); + assertEquals(numberOfHosts, model.getRoot().getHostSystem().getHosts().size()); // Check slobroks clusters assertEquals("Includes retired node", 3+2, model.getAdmin().getSlobroks().size()); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java b/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java index 801e138f3c7..866c4027711 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java @@ -14,6 +14,7 @@ import com.yahoo.config.model.provision.InMemoryProvisioner; import com.yahoo.config.model.provision.SingleNodeProvisioner; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Flavor; +import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils; @@ -44,7 +45,7 @@ public class VespaModelTester { private final ConfigModelRegistry configModelRegistry; private boolean hosted = true; - private Map<String, Collection<Host>> hostsByFlavor = new HashMap<>(); + private Map<NodeResources, Collection<Host>> hostsByResources = new HashMap<>(); private ApplicationId applicationId = ApplicationId.defaultId(); private boolean useDedicatedNodeForLogserver = false; private boolean enableMetricsProxyContainer = false; @@ -62,24 +63,30 @@ public class VespaModelTester { /** Adds some hosts to this system */ public Hosts addHosts(String flavor, int count) { - return addHosts(Optional.empty(), flavor, count); + return addHosts(Optional.empty(), NodeResources.fromLegacyName(flavor), count); } public void addHosts(Flavor flavor, int count) { - addHosts(Optional.of(flavor), flavor.name(), count); + addHosts(Optional.of(flavor), NodeResources.fromLegacyName(flavor.name()), count); } - private Hosts addHosts(Optional<Flavor> flavor, String flavorName, int count) { + public void addHosts(NodeResources resources, int count) { + addHosts(Optional.of(new Flavor(resources)), resources, count); + } + + private Hosts addHosts(Optional<Flavor> flavor, NodeResources resources, int count) { List<Host> hosts = new ArrayList<>(); for (int i = 0; i < count; ++i) { // Let host names sort in the opposite order of the order the hosts are added // This allows us to test index vs. name order selection when subsets of hosts are selected from a cluster // (for e.g cluster controllers and slobrok nodes) - String hostname = String.format("%s%02d", flavorName, count - i); + String hostname = String.format("%s%02d", + resources.allocateByLegacyName() ? resources.legacyName().get() : resources.toString(), + count - i); hosts.add(new Host(hostname, ImmutableList.of(), flavor)); } - this.hostsByFlavor.put(flavorName, hosts); + this.hostsByResources.put(resources, hosts); if (hosts.size() > 100) throw new IllegalStateException("The host naming scheme is nameNN. To test more than 100 hosts, change to nameNNN"); @@ -135,7 +142,7 @@ public class VespaModelTester { ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg; HostProvisioner provisioner = hosted ? - new InMemoryProvisioner(hostsByFlavor, failOnOutOfCapacity, startIndexForClusters, retiredHostNames) : + new InMemoryProvisioner(hostsByResources, failOnOutOfCapacity, startIndexForClusters, retiredHostNames) : new SingleNodeProvisioner(); TestProperties properties = new TestProperties() diff --git a/config-model/src/test/schema-test-files/services-hosted.xml b/config-model/src/test/schema-test-files/services-hosted.xml index e9b1672ce7d..f7f20d003ee 100644 --- a/config-model/src/test/schema-test-files/services-hosted.xml +++ b/config-model/src/test/schema-test-files/services-hosted.xml @@ -7,7 +7,9 @@ </admin> <jdisc id="container1" version="1.0"> - <nodes count="5" flavor="medium" required="true"/> + <nodes count="5" required="true"> + <resources vcpu="1.2" memory="10Gb" disk="0.3 TB"/> + </nodes> </jdisc> <jdisc id="container1" version="1.0"> @@ -28,7 +30,9 @@ <content id="search" version="1.0"> <redundancy>2</redundancy> - <nodes count="7" flavor="large" groups="12"/> + <nodes count="7" flavor="large" groups="12"> + <resources vcpu="3.0" memory="32000.0Mb" disk="300 Gb"/> + </nodes> </content> </services> |