Skip to content
This repository was archived by the owner on Dec 24, 2019. It is now read-only.

Commit 7625f37

Browse files
authored
Merge pull request #13 from hjacobs/allocatable-resources
Allocatable resources
2 parents e0596b0 + 5154d94 commit 7625f37

3 files changed

Lines changed: 36 additions & 24 deletions

File tree

README.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Goals:
2121
* respect Availability Zones, i.e. make sure that all AZs provide enough capacity
2222
* be deterministic and predictable, i.e. the ``DesiredCapacity`` is only calculated based on the current cluster state
2323
* scale down slowly to mitigate service disruptions, i.e. at most one node at a time
24+
* support "elastic" workloads like daily up/down scaling
25+
* support AWS Spot Fleet (not yet implemented)
2426
* require a minimum amount of configuration (preferably none)
2527
* keep it simple
2628

@@ -32,6 +34,13 @@ This hack was created as a proof of concept and born out of frustration with the
3234
* it requires unnecessary configuration
3335
* the code is quite complex
3436

37+
Disclaimer
38+
==========
39+
40+
** Use at your own risk! **
41+
This autoscaler was only tested with Kubernetes version 1.5.2.
42+
There is no guarantee that it works in previous Kubernetes versions.
43+
3544

3645
How it works
3746
============
@@ -48,7 +57,7 @@ The ``autoscale`` function performs the following task:
4857
* iterate through every ASG/AZ combination
4958
* use the calculated resource usage (sum of resource requests) and add the resource requests of any unassigned pods (pods not scheduled on any node yet)
5059
* apply the configured buffer values (10% extra for CPU and memory by default)
51-
* find the capacity of the weakest node
60+
* find the `allocatable capacity`_ of the weakest node
5261
* calculate the number of required nodes by adding up the capacity of the weakest node until the sum is greater than or equal to requested+buffer for both CPU and memory
5362
* sum up the number of required nodes from all AZ for the ASG
5463

@@ -99,3 +108,4 @@ The following command line options are supported:
99108

100109

101110
.. _"official" cluster-autoscaler: https://github.com/kubernetes/contrib/tree/master/cluster-autoscaler
111+
.. _allocatable capacity: https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node-allocatable.md

kube_aws_autoscaler/main.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def parse_resource(v: str):
4747
return int(match.group(1)) * factor
4848

4949

50-
def get_node_capacity_tuple(node: dict):
51-
capacity = node['capacity']
52-
return tuple(capacity[resource] for resource in RESOURCES)
50+
def get_node_allocatable_tuple(node: dict):
51+
allocatable = node['allocatable']
52+
return tuple(allocatable[resource] for resource in RESOURCES)
5353

5454

5555
def apply_buffer(requested: dict, buffer_percentage: dict, buffer_fixed: dict):
@@ -60,11 +60,11 @@ def apply_buffer(requested: dict, buffer_percentage: dict, buffer_fixed: dict):
6060

6161

6262
def find_weakest_node(nodes):
63-
return sorted(nodes, key=get_node_capacity_tuple)[0]
63+
return sorted(nodes, key=get_node_allocatable_tuple)[0]
6464

6565

66-
def is_sufficient(requested: dict, capacity: dict):
67-
for resource, cap in capacity.items():
66+
def is_sufficient(requested: dict, allocatable: dict):
67+
for resource, cap in allocatable.items():
6868
if requested.get(resource, 0) > cap:
6969
return False
7070
return True
@@ -86,13 +86,15 @@ def get_nodes(api) -> dict:
8686
region = node.labels['failure-domain.beta.kubernetes.io/region']
8787
zone = node.labels['failure-domain.beta.kubernetes.io/zone']
8888
instance_type = node.labels['beta.kubernetes.io/instance-type']
89-
capacity = {}
90-
for key, val in node.obj['status']['capacity'].items():
91-
capacity[key] = parse_resource(val)
89+
allocatable = {}
90+
# Use the Node Allocatable Resources to account for any kube/system reservations:
91+
# https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node-allocatable.md
92+
for key, val in node.obj['status']['allocatable'].items():
93+
allocatable[key] = parse_resource(val)
9294
instance_id = node.obj['spec']['externalID']
9395
obj = {'name': node.name,
9496
'region': region, 'zone': zone, 'instance_id': instance_id, 'instance_type': instance_type,
95-
'capacity': capacity,
97+
'allocatable': allocatable,
9698
'ready': is_node_ready(node),
9799
'unschedulable': node.obj['spec'].get('unschedulable', False),
98100
'master': node.labels.get('master', 'false') == 'true'}
@@ -201,10 +203,10 @@ def calculate_required_auto_scaling_group_sizes(nodes_by_asg_zone: dict, usage_b
201203
requested_with_buffer = apply_buffer(requested, buffer_percentage, buffer_fixed)
202204
weakest_node = find_weakest_node(nodes)
203205
required_nodes = 0
204-
capacity = {resource: 0 for resource in RESOURCES}
205-
while not is_sufficient(requested_with_buffer, capacity):
206-
for resource in capacity:
207-
capacity[resource] += weakest_node['capacity'][resource]
206+
allocatable = {resource: 0 for resource in RESOURCES}
207+
while not is_sufficient(requested_with_buffer, allocatable):
208+
for resource in allocatable:
209+
allocatable[resource] += weakest_node['allocatable'][resource]
208210
required_nodes += 1
209211

210212
for node in nodes:
@@ -215,7 +217,7 @@ def calculate_required_auto_scaling_group_sizes(nodes_by_asg_zone: dict, usage_b
215217
required_nodes += 1
216218

217219
overprovisioned = {resource: 0 for resource in RESOURCES}
218-
for resource, value in capacity.items():
220+
for resource, value in allocatable.items():
219221
overprovisioned[resource] = value - requested[resource]
220222

221223
if dump_info:
@@ -226,7 +228,7 @@ def calculate_required_auto_scaling_group_sizes(nodes_by_asg_zone: dict, usage_b
226228
logger.info('{}/{}: with buffer: {}'.format(asg_name, zone,
227229
' '.join([format_resource(requested_with_buffer[r], r).rjust(10) for r in RESOURCES])))
228230
logger.info('{}/{}: weakest node: {}'.format(asg_name, zone,
229-
' '.join([format_resource(weakest_node['capacity'][r], r).rjust(10) for r in RESOURCES])))
231+
' '.join([format_resource(weakest_node['allocatable'][r], r).rjust(10) for r in RESOURCES])))
230232
logger.info('{}/{}: overprovision: {}'.format(asg_name, zone,
231233
' '.join([format_resource(overprovisioned[r], r).rjust(10) for r in RESOURCES])))
232234
logger.info('{}/{}: => {} nodes required (current: {})'.format(asg_name, zone, required_nodes, len(nodes)))

tests/test_autoscaler.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,20 @@ def test_calculate_usage_by_asg_zone():
6666

6767
def test_calculate_required_auto_scaling_group_sizes():
6868
assert calculate_required_auto_scaling_group_sizes({}, {}, {}, {}) == {}
69-
node = {'capacity': {'cpu': 1, 'memory': 1, 'pods': 1}, 'unschedulable': False, 'master': False}
69+
node = {'allocatable': {'cpu': 1, 'memory': 1, 'pods': 1}, 'unschedulable': False, 'master': False}
7070
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {}, {}, {}) == {'a1': 0}
7171
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {('a1', 'z1'): {'cpu': 1, 'memory': 1, 'pods': 1}}, {}, {}) == {'a1': 1}
7272
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {('unknown', 'unknown'): {'cpu': 1, 'memory': 1, 'pods': 1}}, {}, {}) == {'a1': 1}
7373

7474

7575
def test_calculate_required_auto_scaling_group_sizes_cordon():
76-
node = {'name': 'mynode', 'capacity': {'cpu': 1, 'memory': 1, 'pods': 1}, 'unschedulable': True, 'master': False, 'asg_lifecycle_state': 'InService'}
76+
node = {'name': 'mynode', 'allocatable': {'cpu': 1, 'memory': 1, 'pods': 1}, 'unschedulable': True, 'master': False, 'asg_lifecycle_state': 'InService'}
7777
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {}, {}, {}) == {'a1': 1}
7878
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {('a1', 'z1'): {'cpu': 1, 'memory': 1, 'pods': 1}}, {}, {}) == {'a1': 2}
7979

8080

8181
def test_calculate_required_auto_scaling_group_sizes_unschedulable_terminating():
82-
node = {'name': 'mynode', 'capacity': {'cpu': 1, 'memory': 1, 'pods': 1}, 'unschedulable': True, 'master': False, 'asg_lifecycle_state': 'Terminating'}
82+
node = {'name': 'mynode', 'allocatable': {'cpu': 1, 'memory': 1, 'pods': 1}, 'unschedulable': True, 'master': False, 'asg_lifecycle_state': 'Terminating'}
8383
# do not compensate if the instance is terminating.. (it will probably be replaced by ASG)
8484
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {}, {}, {}) == {'a1': 0}
8585
assert calculate_required_auto_scaling_group_sizes({('a1', 'z1'): [node]}, {('a1', 'z1'): {'cpu': 1, 'memory': 1, 'pods': 1}}, {}, {}) == {'a1': 1}
@@ -246,7 +246,7 @@ def test_get_nodes(monkeypatch):
246246
'beta.kubernetes.io/instance-type': 'x1.mega'
247247
}
248248
node.obj = {
249-
'status': {'capacity': {'cpu': '2', 'memory': '16Gi', 'pods': '10'}},
249+
'status': {'allocatable': {'cpu': '2', 'memory': '16Gi', 'pods': '10'}},
250250
'spec': {'externalID': 'i-123'}
251251
}
252252

@@ -257,7 +257,7 @@ def test_get_nodes(monkeypatch):
257257
assert get_nodes(api) == {'n1': {
258258
'name': 'n1',
259259
'region': 'eu-north-1', 'zone': 'eu-north-1a', 'instance_id': 'i-123', 'instance_type': 'x1.mega',
260-
'capacity': {'cpu': 2, 'memory': 16*1024*1024*1024, 'pods': 10},
260+
'allocatable': {'cpu': 2, 'memory': 16*1024*1024*1024, 'pods': 10},
261261
'ready': False,
262262
'unschedulable': False,
263263
'master': False}}
@@ -278,7 +278,7 @@ def test_autoscale(monkeypatch):
278278
get_nodes.return_value = {'n1': {
279279
'name': 'n1',
280280
'region': 'eu-north-1', 'zone': 'eu-north-1a', 'instance_id': 'i-123', 'instance_type': 'x1.mega',
281-
'capacity': {'cpu': 2, 'memory': 16*1024*1024*1024, 'pods': 10},
281+
'allocatable': {'cpu': 2, 'memory': 16*1024*1024*1024, 'pods': 10},
282282
'ready': True,
283283
'unschedulable': False,
284284
'master': False}}
@@ -309,7 +309,7 @@ def test_autoscale_node_without_asg(monkeypatch):
309309
get_nodes.return_value = {'n1': {
310310
'name': 'n1',
311311
'region': 'eu-north-1', 'zone': 'eu-north-1a', 'instance_id': 'i-123', 'instance_type': 'x1.mega',
312-
'capacity': {'cpu': 2, 'memory': 16*1024*1024*1024, 'pods': 10},
312+
'allocatable': {'cpu': 2, 'memory': 16*1024*1024*1024, 'pods': 10},
313313
'ready': True,
314314
'unschedulable': False,
315315
'master': False}}

0 commit comments

Comments
 (0)