diff options
author | Jon Bratseth <bratseth@gmail.com> | 2021-03-18 12:44:48 +0100 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2021-03-18 12:44:48 +0100 |
commit | f67aea9c3d29a413cf81455fe372f4662dcab1b5 (patch) | |
tree | b5bca3b4dd96742f6f551378f5ba72fcab33e976 /node-repository | |
parent | 980a6180525633cf715d20cb965acc69474998d1 (diff) |
Move to ClusterModel
Diffstat (limited to 'node-repository')
-rw-r--r-- | node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java | 25 | ||||
-rw-r--r-- | node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java | 96 | ||||
-rw-r--r-- | node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/MetricsResponse.java | 2 | ||||
-rw-r--r-- | node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java | 74 | ||||
-rw-r--r-- | node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java | 29 | ||||
-rw-r--r-- | node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java (renamed from node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTargetTest.java) | 45 |
6 files changed, 131 insertions, 140 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 9834f898113..9791aabf7b4 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 @@ -59,9 +59,9 @@ public class Autoscaler { } private Advice autoscale(Application application, Cluster cluster, NodeList clusterNodes, Limits limits) { - ClusterModel clusterModel = new ClusterModel(application, cluster, clusterNodes, metricsDb, nodeRepository); + ClusterModel clusterModel = new ClusterModel(application, cluster, clusterNodes, metricsDb, nodeRepository.clock()); - if ( ! clusterModel.isStable()) + if ( ! clusterIsStable(clusterNodes, nodeRepository)) return Advice.none("Cluster change in progress"); if (scaledIn(clusterModel.scalingDuration(), cluster)) @@ -77,12 +77,7 @@ public class Autoscaler { " nodes, but require from " + clusterNodes.size()); var currentAllocation = new AllocatableClusterResources(clusterNodes.asList(), nodeRepository, cluster.exclusive()); - var target = ResourceTarget.idealLoad(clusterModel.scalingDuration(), - clusterModel.clusterTimeseries(), - clusterModel.nodeTimeseries(), - currentAllocation, - application, - nodeRepository.clock()); + var target = ResourceTarget.idealLoad(clusterModel, currentAllocation); Optional<AllocatableClusterResources> bestAllocation = allocationOptimizer.findBestAllocation(target, currentAllocation, limits); @@ -99,6 +94,20 @@ public class Autoscaler { return Advice.scaleTo(bestAllocation.get().advertisedResources()); } + public static boolean clusterIsStable(NodeList clusterNodes, NodeRepository nodeRepository) { + // The cluster is processing recent changes + if (clusterNodes.stream().anyMatch(node -> node.status().wantToRetire() || + node.allocation().get().membership().retired() || + node.allocation().get().isRemovable())) + return false; + + // A deployment is ongoing + if (nodeRepository.nodes().list(Node.State.reserved).owner(clusterNodes.first().get().allocation().get().owner()).size() > 0) + return false; + + return true; + } + /** Returns true if both total real resources and total cost are similar */ public static boolean similar(ClusterResources a, ClusterResources b) { return similar(a.cost(), b.cost(), costDifferenceWorthReallocation) && diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java index 32ddd52ca40..98955848d2d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModel.java @@ -1,14 +1,14 @@ // 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.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; -import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.applications.Cluster; import com.yahoo.vespa.hosted.provision.applications.ScalingEvent; +import java.time.Clock; import java.time.Duration; +import java.util.OptionalDouble; /** * A cluster with its associated metrics which allows prediction about its future behavior. @@ -22,7 +22,8 @@ public class ClusterModel { private final Cluster cluster; private final NodeList nodes; private final MetricsDb metricsDb; - private final NodeRepository nodeRepository; + private final Clock clock; + private final Duration scalingDuration; // Lazily initialized members private ClusterNodesTimeseries nodeTimeseries = null; @@ -32,14 +33,34 @@ public class ClusterModel { Cluster cluster, NodeList clusterNodes, MetricsDb metricsDb, - NodeRepository nodeRepository) { + Clock clock) { this.application = application; this.cluster = cluster; this.nodes = clusterNodes; this.metricsDb = metricsDb; - this.nodeRepository = nodeRepository; + this.clock = clock; + this.scalingDuration = computeScalingDuration(cluster, clusterNodes); } + /** For testing */ + ClusterModel(Application application, + Cluster cluster, + Clock clock, + Duration scalingDuration, + ClusterTimeseries clusterTimeseries) { + this.application = application; + this.cluster = cluster; + this.nodes = null; + this.metricsDb = null; + this.clock = clock; + + this.scalingDuration = scalingDuration; + this.clusterTimeseries = clusterTimeseries; + } + + /** Returns the predicted duration of a rescaling of this cluster */ + public Duration scalingDuration() { return scalingDuration; } + public ClusterNodesTimeseries nodeTimeseries() { if (nodeTimeseries != null) return nodeTimeseries; return nodeTimeseries = new ClusterNodesTimeseries(scalingDuration(), cluster, nodes, metricsDb); @@ -50,26 +71,63 @@ public class ClusterModel { return clusterTimeseries = metricsDb.getClusterTimeseries(application.id(), cluster.id()); } - public boolean isStable() { - return isStable(nodes, nodeRepository); + public double averageLoad(Resource resource) { return nodeTimeseries().averageLoad(resource); } + + public double idealLoad(Resource resource) { + switch (resource) { + case cpu : return idealCpuLoad(); + default : return resource.idealAverageLoad(); // TODO: Move here + } } - public static boolean isStable(NodeList clusterNodes, NodeRepository nodeRepository) { - // The cluster is processing recent changes - if (clusterNodes.stream().anyMatch(node -> node.status().wantToRetire() || - node.allocation().get().membership().retired() || - node.allocation().get().isRemovable())) - return false; + /** Ideal cpu load must take the application traffic fraction into account */ + private double idealCpuLoad() { + double queryCpuFraction = queryCpuFraction(); + + // What's needed to have headroom for growth during scale-up as a fraction of current resources? + double maxGrowthRate = clusterTimeseries().maxQueryGrowthRate(scalingDuration(), clock); // in fraction per minute of the current traffic + double growthRateHeadroom = 1 + maxGrowthRate * scalingDuration().toMinutes(); + // Cap headroom at 10% above the historical observed peak + double fractionOfMax = clusterTimeseries().queryFractionOfMax(scalingDuration(), clock); + if (fractionOfMax != 0) + growthRateHeadroom = Math.min(growthRateHeadroom, 1 / fractionOfMax + 0.1); + + // How much headroom is needed to handle sudden arrival of additional traffic due to another zone going down? + double maxTrafficShiftHeadroom = 10.0; // Cap to avoid extreme sizes from a current very small share + double trafficShiftHeadroom; + if (application.status().maxReadShare() == 0) // No traffic fraction data + trafficShiftHeadroom = 2.0; // assume we currently get half of the global share of traffic + else if (application.status().currentReadShare() == 0) + trafficShiftHeadroom = maxTrafficShiftHeadroom; + else + trafficShiftHeadroom = application.status().maxReadShare() / application.status().currentReadShare(); + trafficShiftHeadroom = Math.min(trafficShiftHeadroom, maxTrafficShiftHeadroom); + + // Assumptions: 1) Write load is not organic so we should not grow to handle more. + // (TODO: But allow applications to set their target write rate and size for that) + // 2) Write load does not change in BCP scenarios. + return queryCpuFraction * 1 / growthRateHeadroom * 1 / trafficShiftHeadroom * idealQueryCpuLoad() + + (1 - queryCpuFraction) * idealWriteCpuLoad(); + } - // A deployment is ongoing - if (nodeRepository.nodes().list(Node.State.reserved).owner(clusterNodes.first().get().allocation().get().owner()).size() > 0) - return false; + private double queryCpuFraction() { + OptionalDouble queryRate = clusterTimeseries().queryRate(scalingDuration(), clock); + OptionalDouble writeRate = clusterTimeseries().writeRate(scalingDuration(), clock); + if (queryRate.orElse(0) == 0 && writeRate.orElse(0) == 0) return queryCpuFraction(0.5); + return queryCpuFraction(queryRate.orElse(0) / (queryRate.orElse(0) + writeRate.orElse(0))); + } - return true; + private double queryCpuFraction(double queryFraction) { + double relativeQueryCost = 9; // How much more expensive are queries than writes? TODO: Measure + double writeFraction = 1 - queryFraction; + return queryFraction * relativeQueryCost / (queryFraction * relativeQueryCost + writeFraction); } - /** The predicted duration of a rescaling of this cluster */ - public Duration scalingDuration() { + public static double idealQueryCpuLoad() { return Resource.cpu.idealAverageLoad(); } + + public static double idealWriteCpuLoad() { return 0.95; } + + private static Duration computeScalingDuration(Cluster cluster, NodeList nodes) { int completedEventCount = 0; Duration totalDuration = Duration.ZERO; for (ScalingEvent event : cluster.scalingEvents()) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/MetricsResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/MetricsResponse.java index b6208cedc5e..b3cf6c1e962 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/MetricsResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/MetricsResponse.java @@ -103,7 +103,7 @@ public class MetricsResponse { private boolean clusterIsStable(Node node, NodeList applicationNodes, NodeRepository nodeRepository) { ClusterSpec cluster = node.allocation().get().membership().cluster(); - return ClusterModel.isStable(applicationNodes.cluster(cluster.id()), nodeRepository); + return Autoscaler.clusterIsStable(applicationNodes.cluster(cluster.id()), nodeRepository); } public static MetricsResponse empty() { return new MetricsResponse(List.of()); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java index 9f6a4fc77cd..b1a1e86b08d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java @@ -51,18 +51,14 @@ public class ResourceTarget { } /** Create a target of achieving ideal load given a current load */ - public static ResourceTarget idealLoad(Duration scalingDuration, - ClusterTimeseries clusterTimeseries, - ClusterNodesTimeseries clusterNodesTimeseries, - AllocatableClusterResources current, - Application application, - Clock clock) { - return new ResourceTarget(nodeUsage(Resource.cpu, clusterNodesTimeseries.averageLoad(Resource.cpu), current) - / idealCpuLoad(scalingDuration, clusterTimeseries, application, clock), - nodeUsage(Resource.memory, clusterNodesTimeseries.averageLoad(Resource.memory), current) - / Resource.memory.idealAverageLoad(), - nodeUsage(Resource.disk, clusterNodesTimeseries.averageLoad(Resource.disk), current) - / Resource.disk.idealAverageLoad(), + public static ResourceTarget idealLoad(ClusterModel clusterModel, + AllocatableClusterResources current) { + return new ResourceTarget(nodeUsage(Resource.cpu, clusterModel.averageLoad(Resource.cpu), current) + / clusterModel.idealLoad(Resource.cpu), + nodeUsage(Resource.memory, clusterModel.averageLoad(Resource.memory), current) + / clusterModel.idealLoad(Resource.memory), + nodeUsage(Resource.disk, clusterModel.averageLoad(Resource.disk), current) + / clusterModel.idealLoad(Resource.disk), true); } @@ -74,58 +70,4 @@ public class ResourceTarget { false); } - /** Ideal cpu load must take the application traffic fraction into account */ - public static double idealCpuLoad(Duration scalingDuration, - ClusterTimeseries clusterTimeseries, - Application application, - Clock clock) { - double queryCpuFraction = queryCpuFraction(clusterTimeseries, scalingDuration, clock); - - // What's needed to have headroom for growth during scale-up as a fraction of current resources? - double maxGrowthRate = clusterTimeseries.maxQueryGrowthRate(scalingDuration, clock); // in fraction per minute of the current traffic - double growthRateHeadroom = 1 + maxGrowthRate * scalingDuration.toMinutes(); - // Cap headroom at 10% above the historical observed peak - double fractionOfMax = clusterTimeseries.queryFractionOfMax(scalingDuration, clock); - if (fractionOfMax != 0) - growthRateHeadroom = Math.min(growthRateHeadroom, 1 / fractionOfMax + 0.1); - - // How much headroom is needed to handle sudden arrival of additional traffic due to another zone going down? - double maxTrafficShiftHeadroom = 10.0; // Cap to avoid extreme sizes from a current very small share - double trafficShiftHeadroom; - if (application.status().maxReadShare() == 0) // No traffic fraction data - trafficShiftHeadroom = 2.0; // assume we currently get half of the global share of traffic - else if (application.status().currentReadShare() == 0) - trafficShiftHeadroom = maxTrafficShiftHeadroom; - else - trafficShiftHeadroom = application.status().maxReadShare() / application.status().currentReadShare(); - trafficShiftHeadroom = Math.min(trafficShiftHeadroom, maxTrafficShiftHeadroom); - - // Assumptions: 1) Write load is not organic so we should not grow to handle more. - // (TODO: But allow applications to set their target write rate and size for that) - // 2) Write load does not change in BCP scenarios. - return queryCpuFraction * 1 / growthRateHeadroom * 1 / trafficShiftHeadroom * idealQueryCpuLoad() + - (1 - queryCpuFraction) * idealWriteCpuLoad(); - } - - private static double queryCpuFraction(ClusterTimeseries clusterTimeseries, Duration scalingDuration, Clock clock) { - OptionalDouble queryRate = clusterTimeseries.queryRate(scalingDuration, clock); - OptionalDouble writeRate = clusterTimeseries.writeRate(scalingDuration, clock); - if (queryRate.orElse(0) == 0 && writeRate.orElse(0) == 0) return queryCpuFraction(0.5); - return queryCpuFraction(queryRate.orElse(0) / (queryRate.orElse(0) + writeRate.orElse(0))); - } - - private static double queryCpuFraction(double queryFraction) { - double relativeQueryCost = 9; // How much more expensive are queries than writes? TODO: Measure - double writeFraction = 1 - queryFraction; - return queryFraction * relativeQueryCost / (queryFraction * relativeQueryCost + writeFraction); - } - - public static double idealQueryCpuLoad() { return Resource.cpu.idealAverageLoad(); } - - public static double idealWriteCpuLoad() { return 0.95; } - - public static double idealMemoryLoad() { return Resource.memory.idealAverageLoad(); } - - public static double idealDiskLoad() { return Resource.disk.idealAverageLoad(); } - } 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 af2f8d0c239..93d760fe00e 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 @@ -69,9 +69,7 @@ public class ApplicationSerializer { NodeList nodes = applicationNodes.not().retired().cluster(cluster.id()); if (nodes.isEmpty()) return; ClusterResources currentResources = nodes.toResources(); - ClusterModel clusterModel = new ClusterModel(application, cluster, nodes, metricsDb, nodeRepository); - var clusterNodesTimeseries = new ClusterNodesTimeseries(Duration.ofHours(1), cluster, nodes, metricsDb); - var clusterTimeseries = metricsDb.getClusterTimeseries(application.id(), cluster.id()); + ClusterModel clusterModel = new ClusterModel(application, cluster, nodes, metricsDb, nodeRepository.clock()); Cursor clusterObject = clustersObject.setObject(cluster.id().value()); clusterObject.setString("type", nodes.clusterSpec().type().name()); @@ -81,12 +79,12 @@ public class ApplicationSerializer { if (cluster.shouldSuggestResources(currentResources)) cluster.suggestedResources().ifPresent(suggested -> toSlime(suggested.resources(), clusterObject.setObject("suggested"))); cluster.targetResources().ifPresent(target -> toSlime(target, clusterObject.setObject("target"))); - clusterUtilizationToSlime(application, clusterModel.scalingDuration(), clusterTimeseries, clusterNodesTimeseries, metricsDb.clock(), clusterObject.setObject("utilization")); + clusterUtilizationToSlime(clusterModel, clusterObject.setObject("utilization")); scalingEventsToSlime(cluster.scalingEvents(), clusterObject.setArray("scalingEvents")); clusterObject.setString("autoscalingStatus", cluster.autoscalingStatus()); clusterObject.setLong("scalingDuration", clusterModel.scalingDuration().toMillis()); - clusterObject.setDouble("maxQueryGrowthRate", clusterTimeseries.maxQueryGrowthRate(clusterModel.scalingDuration(), metricsDb.clock())); - clusterObject.setDouble("currentQueryFractionOfMax", clusterTimeseries.queryFractionOfMax(clusterModel.scalingDuration(), metricsDb.clock())); + clusterObject.setDouble("maxQueryGrowthRate", clusterModel.clusterTimeseries().maxQueryGrowthRate(clusterModel.scalingDuration(), metricsDb.clock())); + clusterObject.setDouble("currentQueryFractionOfMax", clusterModel.clusterTimeseries().queryFractionOfMax(clusterModel.scalingDuration(), metricsDb.clock())); } private static void toSlime(ClusterResources resources, Cursor clusterResourcesObject) { @@ -95,18 +93,13 @@ public class ApplicationSerializer { NodeResourcesSerializer.toSlime(resources.nodeResources(), clusterResourcesObject.setObject("resources")); } - private static void clusterUtilizationToSlime(Application application, - Duration scalingDuration, - ClusterTimeseries clusterTimeseries, - ClusterNodesTimeseries clusterNodesTimeseries, - Clock clock, - Cursor utilizationObject) { - utilizationObject.setDouble("cpu", clusterNodesTimeseries.averageLoad(Resource.cpu)); - utilizationObject.setDouble("idealCpu", ResourceTarget.idealCpuLoad(scalingDuration, clusterTimeseries, application, clock)); - utilizationObject.setDouble("memory", clusterNodesTimeseries.averageLoad(Resource.memory)); - utilizationObject.setDouble("idealMemory", ResourceTarget.idealMemoryLoad()); - utilizationObject.setDouble("disk", clusterNodesTimeseries.averageLoad(Resource.disk)); - utilizationObject.setDouble("idealDisk", ResourceTarget.idealDiskLoad()); + private static void clusterUtilizationToSlime(ClusterModel clusterModel, Cursor utilizationObject) { + utilizationObject.setDouble("cpu", clusterModel.averageLoad(Resource.cpu)); + utilizationObject.setDouble("idealCpu", clusterModel.idealLoad(Resource.cpu)); + utilizationObject.setDouble("memory", clusterModel.averageLoad(Resource.memory)); + utilizationObject.setDouble("idealMemory", clusterModel.idealLoad(Resource.memory)); + utilizationObject.setDouble("disk", clusterModel.averageLoad(Resource.disk)); + utilizationObject.setDouble("idealDisk", clusterModel.idealLoad(Resource.disk)); } private static void scalingEventsToSlime(List<ScalingEvent> scalingEvents, Cursor scalingEventsArray) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTargetTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java index 6f60de62f1f..70550b0a7c3 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTargetTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java @@ -22,7 +22,7 @@ import static org.junit.Assert.assertEquals; /** * @author bratseth */ -public class ResourceTargetTest { +public class ClusterModelTest { private static final double delta = 0.001; @@ -34,22 +34,16 @@ public class ResourceTargetTest { application = application.with(cluster); // No current traffic share: Ideal load is low but capped - application = application.with(new Status(0.0, 1.0)); - assertEquals(0.131, - ResourceTarget.idealCpuLoad(Duration.ofMinutes(10), - timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0, t -> 0.0, clock), - application, - clock), - delta); + var model1 = new ClusterModel(application.with(new Status(0.0, 1.0)), + cluster, clock, Duration.ofMinutes(10), + timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0, t -> 0.0, clock)); + assertEquals(0.131, model1.idealLoad(Resource.cpu), delta); // Almost no current traffic share: Ideal load is low but capped - application = application.with(new Status(0.0001, 1.0)); - assertEquals(0.131, - ResourceTarget.idealCpuLoad(Duration.ofMinutes(10), - timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0, t -> 0.0, clock), - application, - clock), - delta); + var model2 = new ClusterModel(application.with(new Status(0.0001, 1.0)), + cluster, clock, Duration.ofMinutes(10), + timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0, t -> 0.0, clock)); + assertEquals(0.131, model2.idealLoad(Resource.cpu), delta); } @Test @@ -61,21 +55,16 @@ public class ResourceTargetTest { application = application.with(cluster); // No current traffic: Ideal load is low but capped - assertEquals(0.275, - ResourceTarget.idealCpuLoad(Duration.ofMinutes(10), - timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0, t -> 0.0, clock), - application, - clock), - delta); + var model1 = new ClusterModel(application, + cluster, clock, Duration.ofMinutes(10), + timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0, t -> 0.0, clock)); + assertEquals(0.275, model1.idealLoad(Resource.cpu), delta); // Almost current traffic: Ideal load is low but capped - application = application.with(new Status(0.0001, 1.0)); - assertEquals(0.04, - ResourceTarget.idealCpuLoad(Duration.ofMinutes(10), - timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0001, t -> 0.0, clock), - application, - clock), - delta); + var model2 = new ClusterModel(application.with(new Status(0.0001, 1.0)), + cluster, clock, Duration.ofMinutes(10), + timeseries(cluster,100, t -> t == 0 ? 10000.0 : 0.0001, t -> 0.0, clock)); + assertEquals(0.275, model1.idealLoad(Resource.cpu), delta); } private Cluster cluster(NodeResources resources) { |