service-capacity-modeling 0.3.74__py3-none-any.whl → 0.3.75__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of service-capacity-modeling might be problematic. Click here for more details.
- service_capacity_modeling/interface.py +29 -5
- service_capacity_modeling/models/common.py +257 -180
- service_capacity_modeling/models/org/netflix/cassandra.py +14 -9
- service_capacity_modeling/models/org/netflix/evcache.py +10 -18
- {service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/METADATA +1 -1
- {service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/RECORD +10 -10
- {service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/WHEEL +0 -0
- {service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/entry_points.txt +0 -0
- {service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/licenses/LICENSE +0 -0
- {service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/top_level.txt +0 -0
|
@@ -264,14 +264,14 @@ class Drive(ExcludeUnsetModel):
|
|
|
264
264
|
return max(self.block_size_kib, self.group_size_kib)
|
|
265
265
|
|
|
266
266
|
@property
|
|
267
|
-
def max_size_gib(self):
|
|
267
|
+
def max_size_gib(self) -> int:
|
|
268
268
|
if self.max_scale_size_gib != 0:
|
|
269
269
|
return self.max_scale_size_gib
|
|
270
270
|
else:
|
|
271
271
|
return self.size_gib
|
|
272
272
|
|
|
273
273
|
@property
|
|
274
|
-
def max_io_per_s(self):
|
|
274
|
+
def max_io_per_s(self) -> int:
|
|
275
275
|
if self.max_scale_io_per_s != 0:
|
|
276
276
|
return self.max_scale_io_per_s
|
|
277
277
|
else:
|
|
@@ -779,23 +779,48 @@ class BufferComponent(str, Enum):
|
|
|
779
779
|
compute = "compute"
|
|
780
780
|
# [Data Shape] a.k.a. "Dataset" related buffers, e.g. Disk and Memory
|
|
781
781
|
storage = "storage"
|
|
782
|
-
|
|
783
782
|
# Resource specific component
|
|
784
783
|
cpu = "cpu"
|
|
785
784
|
network = "network"
|
|
786
785
|
disk = "disk"
|
|
787
786
|
memory = "memory"
|
|
788
787
|
|
|
788
|
+
@staticmethod
|
|
789
|
+
def is_generic(component: str) -> bool:
|
|
790
|
+
return component in {BufferComponent.compute, BufferComponent.storage}
|
|
791
|
+
|
|
792
|
+
@staticmethod
|
|
793
|
+
def is_specific(component: str) -> bool:
|
|
794
|
+
return not BufferComponent.is_generic(component)
|
|
795
|
+
|
|
789
796
|
|
|
790
797
|
class BufferIntent(str, Enum):
|
|
791
798
|
# Most buffers show "desired" buffer, this is the default
|
|
792
799
|
desired = "desired"
|
|
793
800
|
# ratio on top of existing buffers to ensure exists. Generally combined
|
|
794
801
|
# with a different desired buffer to ensure we don't just scale needlessly
|
|
802
|
+
# This means we can scale up or down as as long as we meet the desired buffer.
|
|
795
803
|
scale = "scale"
|
|
796
|
-
|
|
804
|
+
|
|
805
|
+
# DEPRECATED: Use scale_up/scale_down instead
|
|
806
|
+
# Ignores model preferences, just preserve existing buffers
|
|
807
|
+
# We rarely actually want to do this since it can cause severe over provisioning
|
|
797
808
|
preserve = "preserve"
|
|
798
809
|
|
|
810
|
+
# Scale up if necessary to meet the desired buffer.
|
|
811
|
+
# If the existing resource is over-provisioned, do not reduce the requirement.
|
|
812
|
+
# If under-provisioned, the requirement can be increased to meet the desired buffer.
|
|
813
|
+
# Example: need 20 cores but have 10 → scale up to 20 cores.
|
|
814
|
+
# Example 2: need 20 cores but have 40 → do not scale down and require at
|
|
815
|
+
# least 40 cores
|
|
816
|
+
scale_up = "scale_up"
|
|
817
|
+
# Scale down if necessary to meet the desired buffer.
|
|
818
|
+
# If the existing resource is under-provisioned, do not increase the requirement.
|
|
819
|
+
# If over-provisioned, the requirement can be decreased to meet the desired buffer.
|
|
820
|
+
# Example: need 20 cores but have 10 → maintain buffer and do not scale up.
|
|
821
|
+
# Example 2: need 20 cores but have 40 → scale down to 20 cores.
|
|
822
|
+
scale_down = "scale_down"
|
|
823
|
+
|
|
799
824
|
|
|
800
825
|
class Buffer(ExcludeUnsetModel):
|
|
801
826
|
# The value of the buffer expressed as a ratio over "normal" load e.g. 1.5x
|
|
@@ -819,7 +844,6 @@ class Buffers(ExcludeUnsetModel):
|
|
|
819
844
|
"compute": Buffer(ratio: 1.5),
|
|
820
845
|
}
|
|
821
846
|
)
|
|
822
|
-
|
|
823
847
|
And then models layer in their buffers, for example if a workload
|
|
824
848
|
requires 10 CPU cores, but the operator of that workload likes to build in
|
|
825
849
|
2x buffer for background work (20 cores provisioned), they would express that
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=too-many-lines
|
|
1
2
|
import logging
|
|
2
3
|
import math
|
|
3
4
|
import random
|
|
@@ -6,8 +7,12 @@ from typing import Callable
|
|
|
6
7
|
from typing import Dict
|
|
7
8
|
from typing import List
|
|
8
9
|
from typing import Optional
|
|
10
|
+
from typing import Set
|
|
9
11
|
from typing import Tuple
|
|
10
12
|
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
|
|
11
16
|
from service_capacity_modeling.hardware import shapes
|
|
12
17
|
from service_capacity_modeling.interface import AVG_ITEM_SIZE_BYTES
|
|
13
18
|
from service_capacity_modeling.interface import Buffer
|
|
@@ -63,6 +68,23 @@ def _QOS(tier: int) -> float:
|
|
|
63
68
|
return 1
|
|
64
69
|
|
|
65
70
|
|
|
71
|
+
def combine_buffer_ratios(left: Optional[float], right: Optional[float]) -> float:
|
|
72
|
+
"""
|
|
73
|
+
Strategy for how two buffers for the same component are combined.
|
|
74
|
+
- Multiply two buffers by multiplying if both are not None
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
if left is None and right is None:
|
|
78
|
+
raise ValueError("Cannot combine buffer ratios when both values are None")
|
|
79
|
+
if left is None:
|
|
80
|
+
assert right is not None # MyPy
|
|
81
|
+
return right
|
|
82
|
+
if right is None:
|
|
83
|
+
assert left is not None # MyPy
|
|
84
|
+
return left
|
|
85
|
+
return left * right
|
|
86
|
+
|
|
87
|
+
|
|
66
88
|
def _sqrt_staffed_cores(rps: float, latency_s: float, qos: float) -> int:
|
|
67
89
|
# Square root staffing
|
|
68
90
|
# s = a + Q*sqrt(a)
|
|
@@ -153,18 +175,31 @@ def normalize_cores(
|
|
|
153
175
|
target_shape: Instance,
|
|
154
176
|
reference_shape: Optional[Instance] = None,
|
|
155
177
|
) -> int:
|
|
156
|
-
"""Calculates equivalent
|
|
178
|
+
"""Calculates equivalent CPU on a target shape relative to a reference
|
|
157
179
|
|
|
158
180
|
Takes into account relative core frequency and IPC factor from the hardware
|
|
159
181
|
description to give a rough estimate of how many equivalent cores you need
|
|
160
182
|
in a target_shape to have the core_count number of cores on the reference_shape
|
|
161
183
|
"""
|
|
184
|
+
# Normalize the core count the same as CPUs
|
|
185
|
+
return _normalize_cpu(
|
|
186
|
+
cpu_count=core_count,
|
|
187
|
+
target_shape=target_shape,
|
|
188
|
+
reference_shape=reference_shape,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _normalize_cpu(
|
|
193
|
+
cpu_count: float,
|
|
194
|
+
target_shape: Instance,
|
|
195
|
+
reference_shape: Optional[Instance] = None,
|
|
196
|
+
) -> int:
|
|
162
197
|
if reference_shape is None:
|
|
163
198
|
reference_shape = default_reference_shape
|
|
164
199
|
|
|
165
200
|
target_speed = target_shape.cpu_ghz * target_shape.cpu_ipc_scale
|
|
166
201
|
reference_speed = reference_shape.cpu_ghz * reference_shape.cpu_ipc_scale
|
|
167
|
-
return max(1, math.ceil(
|
|
202
|
+
return max(1, math.ceil(cpu_count / (target_speed / reference_speed)))
|
|
168
203
|
|
|
169
204
|
|
|
170
205
|
def _reserved_headroom(
|
|
@@ -218,8 +253,6 @@ def cpu_headroom_target(instance: Instance, buffers: Optional[Buffers] = None) -
|
|
|
218
253
|
# When someone asks for the key, return any buffers that
|
|
219
254
|
# influence the component in the value
|
|
220
255
|
_default_buffer_fallbacks: Dict[str, List[str]] = {
|
|
221
|
-
BufferComponent.compute: [BufferComponent.cpu],
|
|
222
|
-
BufferComponent.storage: [BufferComponent.disk],
|
|
223
256
|
BufferComponent.cpu: [BufferComponent.compute],
|
|
224
257
|
BufferComponent.network: [BufferComponent.compute],
|
|
225
258
|
BufferComponent.memory: [BufferComponent.storage],
|
|
@@ -227,6 +260,44 @@ _default_buffer_fallbacks: Dict[str, List[str]] = {
|
|
|
227
260
|
}
|
|
228
261
|
|
|
229
262
|
|
|
263
|
+
def _expand_components(
|
|
264
|
+
components: List[str],
|
|
265
|
+
component_fallbacks: Optional[Dict[str, List[str]]] = None,
|
|
266
|
+
) -> Set[str]:
|
|
267
|
+
"""Expand and dedupe components to include their fallbacks
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
components: List of component names to expand
|
|
271
|
+
component_fallbacks: Optional fallback mapping (uses default if None)
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Set of expanded component names including fallbacks
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
# Semantically it does not make sense to fetch buffers for the generic category
|
|
278
|
+
generic_components = [c for c in components if BufferComponent.is_generic(c)]
|
|
279
|
+
if generic_components:
|
|
280
|
+
all_specific_components = [
|
|
281
|
+
c for c in BufferComponent if BufferComponent.is_specific(c)
|
|
282
|
+
]
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"Only specific components allowed. Generic components found: "
|
|
285
|
+
f"{', '.join(str(c) for c in generic_components)}. "
|
|
286
|
+
f"Use specific components instead: "
|
|
287
|
+
f"{', '.join(str(c) for c in all_specific_components)}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if component_fallbacks is None:
|
|
291
|
+
component_fallbacks = _default_buffer_fallbacks
|
|
292
|
+
|
|
293
|
+
expanded_components = set(components)
|
|
294
|
+
for component in components:
|
|
295
|
+
expanded_components = expanded_components | set(
|
|
296
|
+
component_fallbacks.get(component, [])
|
|
297
|
+
)
|
|
298
|
+
return expanded_components
|
|
299
|
+
|
|
300
|
+
|
|
230
301
|
def buffer_for_components(
|
|
231
302
|
buffers: Buffers,
|
|
232
303
|
components: List[str],
|
|
@@ -245,14 +316,7 @@ def buffer_for_components(
|
|
|
245
316
|
components: the components that ultimately matched after applying
|
|
246
317
|
source: All the component buffers that made up the composite ratio
|
|
247
318
|
"""
|
|
248
|
-
|
|
249
|
-
component_fallbacks = _default_buffer_fallbacks
|
|
250
|
-
|
|
251
|
-
unique_components = set(components)
|
|
252
|
-
for component in components:
|
|
253
|
-
unique_components = unique_components | set(
|
|
254
|
-
component_fallbacks.get(component, [])
|
|
255
|
-
)
|
|
319
|
+
expanded_components = _expand_components(components, component_fallbacks)
|
|
256
320
|
|
|
257
321
|
desired = {k: v.model_copy() for k, v in buffers.desired.items()}
|
|
258
322
|
if current_capacity:
|
|
@@ -266,14 +330,14 @@ def buffer_for_components(
|
|
|
266
330
|
ratio = 1.0
|
|
267
331
|
sources = {}
|
|
268
332
|
for name, buffer in desired.items():
|
|
269
|
-
if
|
|
333
|
+
if expanded_components.intersection(buffer.components):
|
|
270
334
|
sources[name] = buffer
|
|
271
|
-
ratio
|
|
335
|
+
ratio = combine_buffer_ratios(ratio, buffer.ratio)
|
|
272
336
|
if not sources:
|
|
273
337
|
ratio = buffers.default.ratio
|
|
274
338
|
|
|
275
339
|
return Buffer(
|
|
276
|
-
ratio=ratio, components=sorted(list(
|
|
340
|
+
ratio=ratio, components=sorted(list(expanded_components)), sources=sources
|
|
277
341
|
)
|
|
278
342
|
|
|
279
343
|
|
|
@@ -483,7 +547,7 @@ def compute_stateful_zone( # pylint: disable=too-many-positional-arguments
|
|
|
483
547
|
# When initially provisioniong we don't want to attach more than
|
|
484
548
|
# 1/3 the maximum volume size in one node (preferring more nodes
|
|
485
549
|
# with smaller volumes)
|
|
486
|
-
max_size = drive.max_size_gib / 3
|
|
550
|
+
max_size = math.ceil(drive.max_size_gib / 3)
|
|
487
551
|
if ebs_gib > max_size > 0:
|
|
488
552
|
ratio = ebs_gib / max_size
|
|
489
553
|
count = max(cluster_size(math.ceil(count * ratio)), min_count)
|
|
@@ -717,180 +781,188 @@ def merge_plan(
|
|
|
717
781
|
)
|
|
718
782
|
|
|
719
783
|
|
|
720
|
-
|
|
721
|
-
scale = 0
|
|
722
|
-
preserve = False
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
784
|
+
class DerivedBuffers(BaseModel):
|
|
785
|
+
scale: float = Field(default=1, gt=0)
|
|
786
|
+
preserve: bool = False
|
|
787
|
+
# When present, this is the maximum ratio of the current usage
|
|
788
|
+
ceiling: Optional[float] = Field(
|
|
789
|
+
default=None,
|
|
790
|
+
gt=0,
|
|
791
|
+
)
|
|
792
|
+
# When present, this is the minimum ratio of the current usage
|
|
793
|
+
floor: Optional[float] = Field(default=None, gt=0)
|
|
794
|
+
|
|
795
|
+
@staticmethod
|
|
796
|
+
def for_components(
|
|
797
|
+
buffer: Dict[str, Buffer],
|
|
798
|
+
components: List[str],
|
|
799
|
+
component_fallbacks: Optional[Dict[str, List[str]]] = None,
|
|
800
|
+
):
|
|
801
|
+
expanded_components = _expand_components(components, component_fallbacks)
|
|
802
|
+
|
|
803
|
+
scale = 1.0
|
|
804
|
+
preserve = False
|
|
805
|
+
ceiling = None
|
|
806
|
+
floor = None
|
|
807
|
+
|
|
808
|
+
for bfr in buffer.values():
|
|
809
|
+
if not expanded_components.intersection(bfr.components):
|
|
810
|
+
continue
|
|
811
|
+
|
|
812
|
+
if bfr.intent in [
|
|
813
|
+
BufferIntent.scale,
|
|
814
|
+
BufferIntent.scale_up,
|
|
815
|
+
BufferIntent.scale_down,
|
|
816
|
+
]:
|
|
817
|
+
scale = combine_buffer_ratios(scale, bfr.ratio)
|
|
818
|
+
if bfr.intent == BufferIntent.scale_up:
|
|
819
|
+
floor = 1 # Create a floor of 1.0x the current usage
|
|
820
|
+
if bfr.intent == BufferIntent.scale_down:
|
|
821
|
+
ceiling = 1 # Create a ceiling of 1.0x the current usage
|
|
731
822
|
if bfr.intent == BufferIntent.preserve:
|
|
732
823
|
preserve = True
|
|
733
824
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def get_cores_from_current_capacity(
|
|
738
|
-
current_capacity: CurrentClusterCapacity, buffers: Buffers, instance: Instance
|
|
739
|
-
):
|
|
740
|
-
# compute cores required per zone
|
|
741
|
-
cpu_success_buffer = (1 - cpu_headroom_target(instance, buffers)) * 100
|
|
742
|
-
current_cpu_utilization = current_capacity.cpu_utilization.mid
|
|
743
|
-
|
|
744
|
-
if current_capacity.cluster_instance is None:
|
|
745
|
-
cluster_instance = shapes.instance(current_capacity.cluster_instance_name)
|
|
746
|
-
else:
|
|
747
|
-
cluster_instance = current_capacity.cluster_instance
|
|
748
|
-
|
|
749
|
-
current_cores = cluster_instance.cpu * current_capacity.cluster_instance_count.mid
|
|
750
|
-
|
|
751
|
-
scale, preserve = derived_buffer_for_component(buffers.derived, ["compute", "cpu"])
|
|
752
|
-
# Scale and preserve for the same component should not be passed together.
|
|
753
|
-
# If user passes it, then scale will be preferred over preserve.
|
|
754
|
-
if scale > 0:
|
|
755
|
-
# if the new cpu core is less than the current,
|
|
756
|
-
# then take no action and return the current cpu cores
|
|
757
|
-
new_cpu_utilization = current_cpu_utilization * scale
|
|
758
|
-
core_scale_up_factor = max(1.0, new_cpu_utilization / cpu_success_buffer)
|
|
759
|
-
return math.ceil(current_cores * core_scale_up_factor)
|
|
760
|
-
|
|
761
|
-
if preserve:
|
|
762
|
-
return current_cores
|
|
763
|
-
|
|
764
|
-
return int(current_cores * (current_cpu_utilization / cpu_success_buffer))
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
def get_memory_from_current_capacity(
|
|
768
|
-
current_capacity: CurrentClusterCapacity, buffers: Buffers
|
|
769
|
-
):
|
|
770
|
-
# compute memory required per zone
|
|
771
|
-
current_memory_utilization = (
|
|
772
|
-
current_capacity.memory_utilization_gib.mid
|
|
773
|
-
* current_capacity.cluster_instance_count.mid
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
if current_capacity.cluster_instance is None:
|
|
777
|
-
cluster_instance = shapes.instance(current_capacity.cluster_instance_name)
|
|
778
|
-
else:
|
|
779
|
-
cluster_instance = current_capacity.cluster_instance
|
|
780
|
-
|
|
781
|
-
zonal_ram_allocated = (
|
|
782
|
-
cluster_instance.ram_gib * current_capacity.cluster_instance_count.mid
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
# These are the desired buffers
|
|
786
|
-
memory_buffer = buffer_for_components(
|
|
787
|
-
buffers=buffers, components=[BufferComponent.memory]
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
scale, preserve = derived_buffer_for_component(
|
|
791
|
-
buffers.derived, ["memory", "storage"]
|
|
792
|
-
)
|
|
793
|
-
# Scale and preserve for the same component should not be passed together.
|
|
794
|
-
# If user passes it, then scale will be preferred over preserve.
|
|
795
|
-
if scale > 0:
|
|
796
|
-
# if the new required memory is less than the current,
|
|
797
|
-
# then take no action and return the current ram
|
|
798
|
-
return max(
|
|
799
|
-
current_memory_utilization * scale * memory_buffer.ratio,
|
|
800
|
-
zonal_ram_allocated,
|
|
825
|
+
return DerivedBuffers(
|
|
826
|
+
scale=scale, preserve=preserve, ceiling=ceiling, floor=floor
|
|
801
827
|
)
|
|
802
828
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
829
|
+
def calculate_requirement(
|
|
830
|
+
self,
|
|
831
|
+
current_usage: float,
|
|
832
|
+
existing_capacity: float,
|
|
833
|
+
desired_buffer_ratio: float = 1.0,
|
|
834
|
+
) -> float:
|
|
835
|
+
if self.preserve:
|
|
836
|
+
return existing_capacity
|
|
837
|
+
|
|
838
|
+
requirement = self.scale * current_usage * desired_buffer_ratio
|
|
839
|
+
if self.ceiling is not None:
|
|
840
|
+
requirement = min(requirement, self.ceiling * existing_capacity)
|
|
841
|
+
if self.floor is not None:
|
|
842
|
+
requirement = max(requirement, self.floor * existing_capacity)
|
|
843
|
+
|
|
844
|
+
return requirement
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class RequirementFromCurrentCapacity(BaseModel):
|
|
848
|
+
current_capacity: CurrentClusterCapacity
|
|
849
|
+
buffers: Buffers
|
|
850
|
+
|
|
851
|
+
@property
|
|
852
|
+
def current_instance(self) -> Instance:
|
|
853
|
+
if self.current_capacity.cluster_instance is not None:
|
|
854
|
+
return self.current_capacity.cluster_instance
|
|
855
|
+
return shapes.instance(self.current_capacity.cluster_instance_name)
|
|
856
|
+
|
|
857
|
+
def cpu(self, instance_candidate: Instance) -> int:
|
|
858
|
+
current_cpu_util = self.current_capacity.cpu_utilization.mid / 100
|
|
859
|
+
current_total_cpu = (
|
|
860
|
+
self.current_instance.cpu * self.current_capacity.cluster_instance_count.mid
|
|
861
|
+
)
|
|
831
862
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
)
|
|
835
|
-
# Scale and preserve for the same component should not be passed together.
|
|
836
|
-
# If user passes it, then scale will be preferred over preserve.
|
|
837
|
-
if scale > 0:
|
|
838
|
-
# if the new required network is less than the current,
|
|
839
|
-
# then take no action and return the current bandwidth
|
|
840
|
-
return max(
|
|
841
|
-
current_network_utilization * scale * network_buffer.ratio,
|
|
842
|
-
zonal_network_allocated,
|
|
863
|
+
derived_buffers = DerivedBuffers.for_components(
|
|
864
|
+
self.buffers.derived, [BufferComponent.cpu]
|
|
843
865
|
)
|
|
844
866
|
|
|
845
|
-
|
|
846
|
-
|
|
867
|
+
# The ideal CPU% that accomodates the headroom + desired buffer, sometimes
|
|
868
|
+
# referred to as the "success buffer"
|
|
869
|
+
target_cpu_util = 1 - cpu_headroom_target(instance_candidate, self.buffers)
|
|
870
|
+
# current_util / target_util ratio indicates CPU scaling direction:
|
|
871
|
+
# > 1: scale up, < 1: scale down, = 1: no change needed
|
|
872
|
+
used_cpu = (current_cpu_util / target_cpu_util) * current_total_cpu
|
|
873
|
+
return math.ceil(
|
|
874
|
+
# Desired buffer is omitted because the cpu_headroom already
|
|
875
|
+
# includes it
|
|
876
|
+
derived_buffers.calculate_requirement(
|
|
877
|
+
current_usage=used_cpu,
|
|
878
|
+
existing_capacity=current_total_cpu,
|
|
879
|
+
)
|
|
880
|
+
)
|
|
847
881
|
|
|
848
|
-
|
|
882
|
+
@property
|
|
883
|
+
def mem_gib(self) -> float:
|
|
884
|
+
current_memory_utilization = (
|
|
885
|
+
self.current_capacity.memory_utilization_gib.mid
|
|
886
|
+
* self.current_capacity.cluster_instance_count.mid
|
|
887
|
+
)
|
|
888
|
+
zonal_ram_allocated = (
|
|
889
|
+
self.current_instance.ram_gib
|
|
890
|
+
* self.current_capacity.cluster_instance_count.mid
|
|
891
|
+
)
|
|
849
892
|
|
|
893
|
+
desired_buffer = buffer_for_components(
|
|
894
|
+
buffers=self.buffers, components=[BufferComponent.memory]
|
|
895
|
+
)
|
|
896
|
+
derived_buffer = DerivedBuffers.for_components(
|
|
897
|
+
self.buffers.derived, [BufferComponent.memory]
|
|
898
|
+
)
|
|
850
899
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
current_capacity.disk_utilization_gib.mid
|
|
857
|
-
* current_capacity.cluster_instance_count.mid
|
|
858
|
-
)
|
|
900
|
+
return derived_buffer.calculate_requirement(
|
|
901
|
+
current_usage=current_memory_utilization,
|
|
902
|
+
existing_capacity=zonal_ram_allocated,
|
|
903
|
+
desired_buffer_ratio=desired_buffer.ratio,
|
|
904
|
+
)
|
|
859
905
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
906
|
+
@property
|
|
907
|
+
def disk_gib(self) -> int:
|
|
908
|
+
current_cluster_disk_util_gib = (
|
|
909
|
+
self.current_capacity.disk_utilization_gib.mid
|
|
910
|
+
* self.current_capacity.cluster_instance_count.mid
|
|
911
|
+
)
|
|
912
|
+
current_node_disk_gib = (
|
|
913
|
+
self.current_instance.drive.max_size_gib
|
|
914
|
+
if self.current_instance.drive is not None
|
|
915
|
+
else (
|
|
916
|
+
self.current_capacity.cluster_drive.size_gib
|
|
917
|
+
if self.current_capacity.cluster_drive is not None
|
|
918
|
+
else 0
|
|
919
|
+
)
|
|
920
|
+
)
|
|
864
921
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
922
|
+
zonal_disk_allocated = (
|
|
923
|
+
current_node_disk_gib * self.current_capacity.cluster_instance_count.mid
|
|
924
|
+
)
|
|
925
|
+
# These are the desired buffers
|
|
926
|
+
disk_buffer = buffer_for_components(
|
|
927
|
+
buffers=self.buffers, components=[BufferComponent.disk]
|
|
928
|
+
)
|
|
870
929
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
930
|
+
derived_buffer = DerivedBuffers.for_components(
|
|
931
|
+
self.buffers.derived, [BufferComponent.disk]
|
|
932
|
+
)
|
|
933
|
+
required_disk = derived_buffer.calculate_requirement(
|
|
934
|
+
current_usage=current_cluster_disk_util_gib,
|
|
935
|
+
existing_capacity=zonal_disk_allocated,
|
|
936
|
+
desired_buffer_ratio=disk_buffer.ratio,
|
|
937
|
+
)
|
|
938
|
+
return math.ceil(required_disk)
|
|
874
939
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
940
|
+
@property
|
|
941
|
+
def network_mbps(self) -> int:
|
|
942
|
+
current_network_utilization = (
|
|
943
|
+
self.current_capacity.network_utilization_mbps.mid
|
|
944
|
+
* self.current_capacity.cluster_instance_count.mid
|
|
945
|
+
)
|
|
946
|
+
zonal_network_allocated = (
|
|
947
|
+
self.current_instance.net_mbps
|
|
948
|
+
* self.current_capacity.cluster_instance_count.mid
|
|
949
|
+
)
|
|
879
950
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
return max(
|
|
887
|
-
current_disk_utilization * scale * disk_buffer.ratio, zonal_disk_allocated
|
|
951
|
+
# These are the desired buffers
|
|
952
|
+
network_buffer = buffer_for_components(
|
|
953
|
+
buffers=self.buffers, components=[BufferComponent.network]
|
|
954
|
+
)
|
|
955
|
+
derived_buffer = DerivedBuffers.for_components(
|
|
956
|
+
self.buffers.derived, [BufferComponent.network]
|
|
888
957
|
)
|
|
889
|
-
if preserve:
|
|
890
|
-
# preserve the current disk size for the zone
|
|
891
|
-
return zonal_disk_allocated
|
|
892
958
|
|
|
893
|
-
|
|
959
|
+
return math.ceil(
|
|
960
|
+
derived_buffer.calculate_requirement(
|
|
961
|
+
current_usage=current_network_utilization,
|
|
962
|
+
existing_capacity=zonal_network_allocated,
|
|
963
|
+
desired_buffer_ratio=network_buffer.ratio,
|
|
964
|
+
)
|
|
965
|
+
)
|
|
894
966
|
|
|
895
967
|
|
|
896
968
|
def zonal_requirements_from_current(
|
|
@@ -901,20 +973,25 @@ def zonal_requirements_from_current(
|
|
|
901
973
|
) -> CapacityRequirement:
|
|
902
974
|
if current_cluster is not None and current_cluster.zonal[0] is not None:
|
|
903
975
|
current_capacity: CurrentClusterCapacity = current_cluster.zonal[0]
|
|
904
|
-
|
|
905
|
-
|
|
976
|
+
|
|
977
|
+
# Adjust the CPUs (vCPU + cores) based on generation / instance type
|
|
978
|
+
requirement = RequirementFromCurrentCapacity(
|
|
979
|
+
current_capacity=current_capacity,
|
|
980
|
+
buffers=buffers,
|
|
981
|
+
)
|
|
982
|
+
normalized_cpu = _normalize_cpu(
|
|
983
|
+
requirement.cpu(instance),
|
|
906
984
|
instance,
|
|
907
985
|
reference_shape,
|
|
908
986
|
)
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
needed_memory_gib =
|
|
913
|
-
needed_disk_gib = get_disk_from_current_capacity(current_capacity, buffers)
|
|
987
|
+
|
|
988
|
+
needed_network_mbps = requirement.network_mbps
|
|
989
|
+
needed_disk_gib = requirement.disk_gib
|
|
990
|
+
needed_memory_gib = requirement.mem_gib
|
|
914
991
|
|
|
915
992
|
return CapacityRequirement(
|
|
916
993
|
requirement_type="zonal-capacity",
|
|
917
|
-
cpu_cores=certain_int(
|
|
994
|
+
cpu_cores=certain_int(normalized_cpu),
|
|
918
995
|
mem_gib=certain_float(needed_memory_gib),
|
|
919
996
|
disk_gib=certain_float(needed_disk_gib),
|
|
920
997
|
network_mbps=certain_float(needed_network_mbps),
|
|
@@ -35,7 +35,7 @@ from service_capacity_modeling.interface import ServiceCapacity
|
|
|
35
35
|
from service_capacity_modeling.models import CapacityModel
|
|
36
36
|
from service_capacity_modeling.models.common import buffer_for_components
|
|
37
37
|
from service_capacity_modeling.models.common import compute_stateful_zone
|
|
38
|
-
from service_capacity_modeling.models.common import
|
|
38
|
+
from service_capacity_modeling.models.common import DerivedBuffers
|
|
39
39
|
from service_capacity_modeling.models.common import get_effective_disk_per_node_gib
|
|
40
40
|
from service_capacity_modeling.models.common import network_services
|
|
41
41
|
from service_capacity_modeling.models.common import normalize_cores
|
|
@@ -181,7 +181,9 @@ def _zonal_requirement_for_new_cluster(
|
|
|
181
181
|
)
|
|
182
182
|
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
# pylint: disable=too-many-locals
|
|
185
|
+
# pylint: disable=too-many-positional-arguments
|
|
186
|
+
def _estimate_cassandra_requirement(
|
|
185
187
|
instance: Instance,
|
|
186
188
|
desires: CapacityDesires,
|
|
187
189
|
working_set: float,
|
|
@@ -205,19 +207,22 @@ def _estimate_cassandra_requirement( # pylint: disable=too-many-positional-argu
|
|
|
205
207
|
# If the cluster is already provisioned
|
|
206
208
|
if current_capacity and desires.current_clusters is not None:
|
|
207
209
|
capacity_requirement = zonal_requirements_from_current(
|
|
208
|
-
desires.current_clusters,
|
|
210
|
+
desires.current_clusters,
|
|
211
|
+
desires.buffers,
|
|
212
|
+
instance,
|
|
213
|
+
reference_shape,
|
|
209
214
|
)
|
|
210
|
-
|
|
211
|
-
desires.buffers.derived, [
|
|
215
|
+
disk_derived_buffer = DerivedBuffers.for_components(
|
|
216
|
+
desires.buffers.derived, [BufferComponent.disk]
|
|
212
217
|
)
|
|
213
218
|
disk_used_gib = (
|
|
214
219
|
current_capacity.disk_utilization_gib.mid
|
|
215
220
|
* current_capacity.cluster_instance_count.mid
|
|
216
|
-
*
|
|
217
|
-
)
|
|
218
|
-
_, memory_preserve = derived_buffer_for_component(
|
|
219
|
-
desires.buffers.derived, ["storage", "memory"]
|
|
221
|
+
* disk_derived_buffer.scale
|
|
220
222
|
)
|
|
223
|
+
memory_preserve = DerivedBuffers.for_components(
|
|
224
|
+
desires.buffers.derived, [BufferComponent.memory]
|
|
225
|
+
).preserve
|
|
221
226
|
else:
|
|
222
227
|
# If the cluster is not yet provisioned
|
|
223
228
|
capacity_requirement = _zonal_requirement_for_new_cluster(
|
|
@@ -33,13 +33,10 @@ from service_capacity_modeling.interface import Requirements
|
|
|
33
33
|
from service_capacity_modeling.models import CapacityModel
|
|
34
34
|
from service_capacity_modeling.models.common import buffer_for_components
|
|
35
35
|
from service_capacity_modeling.models.common import compute_stateful_zone
|
|
36
|
-
from service_capacity_modeling.models.common import get_cores_from_current_capacity
|
|
37
|
-
from service_capacity_modeling.models.common import get_disk_from_current_capacity
|
|
38
36
|
from service_capacity_modeling.models.common import get_effective_disk_per_node_gib
|
|
39
|
-
from service_capacity_modeling.models.common import get_memory_from_current_capacity
|
|
40
|
-
from service_capacity_modeling.models.common import get_network_from_current_capacity
|
|
41
37
|
from service_capacity_modeling.models.common import network_services
|
|
42
38
|
from service_capacity_modeling.models.common import normalize_cores
|
|
39
|
+
from service_capacity_modeling.models.common import RequirementFromCurrentCapacity
|
|
43
40
|
from service_capacity_modeling.models.common import simple_network_mbps
|
|
44
41
|
from service_capacity_modeling.models.common import sqrt_staffed_cores
|
|
45
42
|
from service_capacity_modeling.models.common import working_set_from_drive_and_slo
|
|
@@ -104,25 +101,20 @@ def calculate_vitals_for_capacity_planner(
|
|
|
104
101
|
)
|
|
105
102
|
if not current_capacity:
|
|
106
103
|
return needed_cores, needed_network_mbps, needed_memory_gib, needed_disk_gib
|
|
104
|
+
requirements = RequirementFromCurrentCapacity(
|
|
105
|
+
current_capacity=current_capacity,
|
|
106
|
+
buffers=desires.buffers,
|
|
107
|
+
)
|
|
107
108
|
needed_cores = normalize_cores(
|
|
108
|
-
core_count=
|
|
109
|
-
current_capacity, desires.buffers, instance
|
|
110
|
-
),
|
|
109
|
+
core_count=requirements.cpu(instance_candidate=instance),
|
|
111
110
|
target_shape=instance,
|
|
112
111
|
reference_shape=current_capacity.cluster_instance,
|
|
113
112
|
)
|
|
114
|
-
needed_network_mbps =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
needed_memory_gib = get_memory_from_current_capacity(
|
|
118
|
-
current_capacity, desires.buffers
|
|
113
|
+
needed_network_mbps = requirements.network_mbps
|
|
114
|
+
needed_disk_gib = (
|
|
115
|
+
requirements.disk_gib if current_capacity.cluster_drive is not None else 0.0
|
|
119
116
|
)
|
|
120
|
-
|
|
121
|
-
needed_disk_gib = 0.0
|
|
122
|
-
else:
|
|
123
|
-
needed_disk_gib = get_disk_from_current_capacity(
|
|
124
|
-
current_capacity, desires.buffers
|
|
125
|
-
)
|
|
117
|
+
needed_memory_gib = requirements.mem_gib
|
|
126
118
|
return needed_cores, needed_network_mbps, needed_memory_gib, needed_disk_gib
|
|
127
119
|
|
|
128
120
|
|
{service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/RECORD
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
service_capacity_modeling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
service_capacity_modeling/capacity_planner.py,sha256=B6e0esOAvV6qMkEeLIO9rEveTftRG_Ut_d9gYgSIM0w,31914
|
|
3
|
-
service_capacity_modeling/interface.py,sha256
|
|
3
|
+
service_capacity_modeling/interface.py,sha256=WLCVqrRgH4Vz3YhzZAct0QMXrBOow7v5ipZLoJ7AfSc,38826
|
|
4
4
|
service_capacity_modeling/stats.py,sha256=8HIPwVnmvbauBwXhn6vbNYO7-CzWPuymnq0eX7ZA1_w,5849
|
|
5
5
|
service_capacity_modeling/hardware/__init__.py,sha256=kzIHnIymwnf4qQYDpfIChIAxTF8b87XtnBg1TwF_J9E,8974
|
|
6
6
|
service_capacity_modeling/hardware/profiles/__init__.py,sha256=7-y3JbCBkgzaAjFla2RIymREcImdZ51HTl3yn3vzoGw,1602
|
|
@@ -46,19 +46,19 @@ service_capacity_modeling/hardware/profiles/shapes/aws/manual_drives.json,sha256
|
|
|
46
46
|
service_capacity_modeling/hardware/profiles/shapes/aws/manual_instances.json,sha256=-_jxyQgmwKe5JnbfhMD9xDCq0sy7z2fdZn7Fu76IUkk,12457
|
|
47
47
|
service_capacity_modeling/hardware/profiles/shapes/aws/manual_services.json,sha256=h63675KKmu5IrI3BORDN8fiAqLjAyYHArErKbC7-T30,776
|
|
48
48
|
service_capacity_modeling/models/__init__.py,sha256=XK7rTBW8ZXQY5L9Uy2FwjuFN_KBW3hKw7IrhG1piajs,13567
|
|
49
|
-
service_capacity_modeling/models/common.py,sha256=
|
|
49
|
+
service_capacity_modeling/models/common.py,sha256=ntBx5Rku4UfJiKwZ0tkv75gXBzUlGT6hOUTLvENlElA,36534
|
|
50
50
|
service_capacity_modeling/models/headroom_strategy.py,sha256=QIkP_K_tK2EGAjloaGfXeAPH5M0UDCN8FlAtwV9xxTA,651
|
|
51
51
|
service_capacity_modeling/models/utils.py,sha256=WosEEg4o1_WSbTb5mL-M1v8JuWJgvS2oWvnDS3qNz3k,2662
|
|
52
52
|
service_capacity_modeling/models/org/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
service_capacity_modeling/models/org/netflix/__init__.py,sha256=m7IaQbo85NEbDvfoPJREIznpzg0YHTCrKP5C1GvnOYM,2378
|
|
54
54
|
service_capacity_modeling/models/org/netflix/aurora.py,sha256=Mi9zd48k64GkKIjAs3J1S2qThguNvyWIy2dUmhwrVhc,12883
|
|
55
|
-
service_capacity_modeling/models/org/netflix/cassandra.py,sha256=
|
|
55
|
+
service_capacity_modeling/models/org/netflix/cassandra.py,sha256=MnTD7X3-mm05LtM_CjWatIVyeMCfPqVsxszTDb8s7ao,38632
|
|
56
56
|
service_capacity_modeling/models/org/netflix/counter.py,sha256=hOVRRCgCPU-A5TdLKQXc_mWTQpkKOWRNjOeECdDP7kA,9205
|
|
57
57
|
service_capacity_modeling/models/org/netflix/crdb.py,sha256=ELIbxwfNsJcEkNGW7qtz0SEzt3Vj6wj8QL5QQeebIlo,20635
|
|
58
58
|
service_capacity_modeling/models/org/netflix/ddb.py,sha256=GDoXVIpDDY6xDB0dsiaz7RAPPj-qffTrM9N6w5-5ndg,26311
|
|
59
59
|
service_capacity_modeling/models/org/netflix/elasticsearch.py,sha256=AfyqfC4Y_QDyvYLBbeq8_ReM9q54RUNrZkOsSBjBgIc,25085
|
|
60
60
|
service_capacity_modeling/models/org/netflix/entity.py,sha256=M0vzwhf8UAbVxnXspAkN4GEbq3rix6yoky6W2oDG6a0,8648
|
|
61
|
-
service_capacity_modeling/models/org/netflix/evcache.py,sha256=
|
|
61
|
+
service_capacity_modeling/models/org/netflix/evcache.py,sha256=mLSoecrXwwfrt9ZRu1LZ2po8lD50orQAFnpvW0YTmI8,25396
|
|
62
62
|
service_capacity_modeling/models/org/netflix/graphkv.py,sha256=iS5QDDv9_hNY6nIgdL-umB439qP7-jN-n6_Tl6d-ZSo,8557
|
|
63
63
|
service_capacity_modeling/models/org/netflix/iso_date_math.py,sha256=CPGHLmbGeNqkcYcmCkLKhPZcAU-yTJ2HjvuXdnNyCYc,996
|
|
64
64
|
service_capacity_modeling/models/org/netflix/kafka.py,sha256=MDHaht5cWsOJ113uMl6nQ7nllSATrlBCQ-TXLkqMWEk,25466
|
|
@@ -75,9 +75,9 @@ service_capacity_modeling/tools/auto_shape.py,sha256=Pe9a7vbFxqIy8eL8ssENTu9FNnF
|
|
|
75
75
|
service_capacity_modeling/tools/fetch_pricing.py,sha256=JkgJPTE0SVj8sdGQvo0HN-Hdv3nfA2tu7C_Arad5aX8,3762
|
|
76
76
|
service_capacity_modeling/tools/generate_missing.py,sha256=XqUs54CPfli4XtK0rEiFKqDvpwCiMAD8wrl7fAxpYHs,3062
|
|
77
77
|
service_capacity_modeling/tools/instance_families.py,sha256=5Y4_aJ5ML-JPzwaWcS_caUeZ28CmUVoqjYYLaRl01vg,9148
|
|
78
|
-
service_capacity_modeling-0.3.
|
|
79
|
-
service_capacity_modeling-0.3.
|
|
80
|
-
service_capacity_modeling-0.3.
|
|
81
|
-
service_capacity_modeling-0.3.
|
|
82
|
-
service_capacity_modeling-0.3.
|
|
83
|
-
service_capacity_modeling-0.3.
|
|
78
|
+
service_capacity_modeling-0.3.75.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
|
|
79
|
+
service_capacity_modeling-0.3.75.dist-info/METADATA,sha256=r_DDZjFo54y1QWSRY7HfJ4wWwypgxT31-oKXeCBLoMA,10214
|
|
80
|
+
service_capacity_modeling-0.3.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
81
|
+
service_capacity_modeling-0.3.75.dist-info/entry_points.txt,sha256=ZsjzpG5SomWpT1zCE19n1uSXKH2gTI_yc33sdl0vmJg,146
|
|
82
|
+
service_capacity_modeling-0.3.75.dist-info/top_level.txt,sha256=H8XjTCLgR3enHq5t3bIbxt9SeUkUT8HT_SDv2dgIT_A,26
|
|
83
|
+
service_capacity_modeling-0.3.75.dist-info/RECORD,,
|
{service_capacity_modeling-0.3.74.dist-info → service_capacity_modeling-0.3.75.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|