Skip to content

Commit 3cfcf72

Browse files
committed
feat(controller): add KUBERNETES_CPU_MEMORY_RATIO parameter
1 parent 7d60489 commit 3cfcf72

6 files changed

Lines changed: 96 additions & 66 deletions

File tree

rootfs/api/models/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,8 +640,8 @@ def deploy(self, release, force_deploy=False, rollback_on_failure=True): # noqa
640640
release.cleanup_old()
641641

642642
def _set_default_config(self):
643-
default_cpu = "{}m".format(settings.KUBERNETES_LIMITS_DEFAULT_CPU)
644-
default_memory = "{}M".format(settings.KUBERNETES_LIMITS_DEFAULT_MEMORY)
643+
default_cpu = "{}m".format(settings.KUBERNETES_LIMITS_MIN_CPU)
644+
default_memory = "{}M".format(settings.KUBERNETES_LIMITS_MIN_MEMORY)
645645
config = self.config_set.latest()
646646
new_cpu, new_memory = {}, {}
647647
for _type in self.types:

rootfs/api/models/config.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import json
2+
import math
23
import logging
4+
from decimal import Decimal
35
from django.conf import settings
46
from django.db import models
57
from django.contrib.auth import get_user_model
8+
from api.utils import unit_to_bytes, unit_to_millicpu
69
from api.models.release import Release
710
from api.models import UuidAuditedModel
811
from api.exceptions import DryccException, UnprocessableEntity
@@ -84,6 +87,55 @@ def get_healthcheck(self):
8487
return {'web/cmd': self.healthcheck}
8588
return self.healthcheck
8689

90+
def _set_cpu_memory(self):
91+
"""
92+
According to settings.KUBERNETES_CPU_MEMORY_RATIO corrects cpu and memory
93+
"""
94+
radio = settings.KUBERNETES_CPU_MEMORY_RATIO
95+
limit_min_cpu = settings.KUBERNETES_LIMITS_MIN_CPU
96+
limit_max_cpu = settings.KUBERNETES_LIMITS_MAX_CPU
97+
limit_min_memory = Decimal(settings.KUBERNETES_LIMITS_MIN_MEMORY * math.pow(1024, 2))
98+
limit_max_memory = Decimal(settings.KUBERNETES_LIMITS_MAX_MEMORY * math.pow(1024, 2))
99+
memory_cpu_min_radio = Decimal(unit_to_bytes(radio[0])) / Decimal(unit_to_millicpu('1'))
100+
memory_cpu_max_radio = Decimal(unit_to_bytes(radio[1])) / Decimal(unit_to_millicpu('1'))
101+
cpu_memory_min_radio = Decimal(unit_to_millicpu('1')) / Decimal(unit_to_bytes(radio[1]))
102+
cpu_memory_max_radio = Decimal(unit_to_millicpu('1')) / Decimal(unit_to_bytes(radio[0]))
103+
for container_type in set(
104+
self.app.structure.keys()).union(set(self.cpu)).union(set(self.memory)):
105+
if container_type in self.cpu:
106+
cpu = unit_to_millicpu(self.cpu[container_type])
107+
min_memory = cpu * memory_cpu_min_radio
108+
min_memory = limit_min_memory if min_memory < limit_min_memory else min_memory
109+
max_memory = cpu * memory_cpu_max_radio
110+
max_memory = limit_max_memory if max_memory > limit_max_memory else max_memory
111+
if self.memory.get(container_type):
112+
memory = unit_to_bytes(self.memory.get(container_type))
113+
if memory < min_memory:
114+
memory = min_memory
115+
elif memory > max_memory:
116+
memory = max_memory
117+
else:
118+
memory = min_memory
119+
if memory % Decimal(math.pow(1024, 3)) == 0:
120+
self.memory[container_type] = f'{round(memory / Decimal(math.pow(1024, 3)))}G'
121+
else:
122+
self.memory[container_type] = f'{round(memory / Decimal(math.pow(1024, 2)))}M'
123+
elif container_type in self.memory:
124+
memory = Decimal(unit_to_bytes(self.memory[container_type]))
125+
if container_type not in self.cpu:
126+
min_cpu = memory * cpu_memory_min_radio
127+
min_cpu = limit_min_cpu if min_cpu < limit_min_cpu else min_cpu
128+
max_cpu = memory * cpu_memory_max_radio
129+
max_cpu = limit_max_cpu if max_cpu > limit_max_cpu else max_cpu
130+
cpu = max_cpu if min_cpu < 1000 else min_cpu
131+
if cpu % 1000 == 0:
132+
self.cpu[container_type] = f'{round(cpu / 1000)}'
133+
else:
134+
self.cpu[container_type] = f'{round(cpu)}m'
135+
else:
136+
self.cpu[container_type] = f"{settings.KUBERNETES_LIMITS_MIN_CPU}m"
137+
self.memory[container_type] = f"{settings.KUBERNETES_LIMITS_MIN_MEMORY}M"
138+
87139
def set_registry(self):
88140
# lower case all registry options for consistency
89141
self.registry = {key.lower(): value for key, value in self.registry.copy().items()}
@@ -185,6 +237,7 @@ def save(self, **kwargs):
185237
else:
186238
data[key] = value
187239
setattr(self, attr, data)
240+
self._set_cpu_memory()
188241
self.set_healthcheck(previous_config)
189242
self._migrate_legacy_healthcheck()
190243
self.set_registry()

rootfs/api/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ def validate_values(data):
310310
def validate_memory(data):
311311
max_memory = settings.KUBERNETES_LIMITS_MAX_MEMORY
312312
# The minimum limit memory is equal to the memory allocated by default
313-
min_memory = settings.KUBERNETES_LIMITS_DEFAULT_MEMORY
313+
min_memory = settings.KUBERNETES_LIMITS_MIN_MEMORY
314314
for key, value in data.items():
315315
if value is None: # use NoneType to unset an item
316316
continue
@@ -336,7 +336,7 @@ def validate_memory(data):
336336
def validate_cpu(data):
337337
max_cpu = settings.KUBERNETES_LIMITS_MAX_CPU
338338
# The minimum CPU limit is equal to the CPU allocated by default
339-
min_cpu = settings.KUBERNETES_LIMITS_DEFAULT_CPU
339+
min_cpu = settings.KUBERNETES_LIMITS_MIN_CPU
340340
for key, value in data.items():
341341
if value is None: # use NoneType to unset an item
342342
continue

rootfs/api/settings/production.py

Lines changed: 28 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.path.insert(0, os.path.join(BASE_DIR, 'apps_extra'))
1414
# A boolean that turns on/off debug mode.
1515
# https://docs.djangoproject.com/en/1.11/ref/settings/#debug
16-
DEBUG = bool(os.environ.get('DRYCC_DEBUG', False))
16+
DEBUG = bool(strtobool(os.environ.get('DRYCC_DEBUG', 'false')))
1717

1818
# If set to True, Django's normal exception handling of view functions
1919
# will be suppressed, and exceptions will propagate upwards
@@ -277,7 +277,7 @@
277277

278278
PLATFORM_DOMAIN = os.environ.get('DRYCC_PLATFORM_DOMAIN', 'local.drycc.cc')
279279

280-
IMAGE_PULL_POLICY = os.environ.get('IMAGE_PULL_POLICY', "IfNotPresent") # noqa
280+
IMAGE_PULL_POLICY = os.environ.get('IMAGE_PULL_POLICY', "IfNotPresent")
281281

282282
# True, true, yes, y and more evaluate to True
283283
# False, false, no, n and more evaluate to False
@@ -290,7 +290,8 @@
290290
# If the user has a Procfile in both deploys then processes are scaled up / down as per usual
291291
#
292292
# By default the process types are scaled down unless this setting is turned on
293-
DRYCC_DEPLOY_PROCFILE_MISSING_REMOVE = bool(strtobool(os.environ.get('DRYCC_DEPLOY_PROCFILE_MISSING_REMOVE', 'true'))) # noqa
293+
DRYCC_DEPLOY_PROCFILE_MISSING_REMOVE = bool(strtobool(os.environ.get(
294+
'DRYCC_DEPLOY_PROCFILE_MISSING_REMOVE', 'true')))
294295

295296
# True, true, yes, y and more evaluate to True
296297
# False, false, no, n and more evaluate to False
@@ -300,7 +301,8 @@
300301
# If a previous deploy had a Procfile but then the following deploy has no Procfile then it will
301302
# result in a 406 - Not Acceptable
302303
# Has priority over DRYCC_DEPLOY_PROCFILE_MISSING_REMOVE
303-
DRYCC_DEPLOY_REJECT_IF_PROCFILE_MISSING = bool(strtobool(os.environ.get('DRYCC_DEPLOY_REJECT_IF_PROCFILE_MISSING', 'false'))) # noqa
304+
DRYCC_DEPLOY_REJECT_IF_PROCFILE_MISSING = bool(strtobool(os.environ.get(
305+
'DRYCC_DEPLOY_REJECT_IF_PROCFILE_MISSING', 'false')))
304306

305307
# Define a global default on how many pods to bring up and then
306308
# take down sequentially during a deploy
@@ -324,29 +326,31 @@
324326

325327
DRYCC_APP_STORAGE_CLASS = os.environ.get('DRYCC_APP_STORAGE_CLASS', "")
326328

327-
KUBERNETES_DEPLOYMENTS_REVISION_HISTORY_LIMIT = os.environ.get('KUBERNETES_DEPLOYMENTS_REVISION_HISTORY_LIMIT', None) # noqa
328-
329329
DRYCC_DEFAULT_CONFIG_TAGS = os.environ.get('DRYCC_DEFAULT_CONFIG_TAGS', '')
330330

331+
KUBERNETES_DEPLOYMENTS_REVISION_HISTORY_LIMIT = os.environ.get(
332+
'KUBERNETES_DEPLOYMENTS_REVISION_HISTORY_LIMIT', None)
333+
331334
# How long k8s waits for a pod to finish work after a SIGTERM before sending SIGKILL
332-
KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS = int(os.environ.get('KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS', 30)) # noqa
335+
KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS = int(os.environ.get(
336+
'KUBERNETES_POD_TERMINATION_GRACE_PERIOD_SECONDS', 30))
333337

338+
# min and max memory for a CPU
339+
KUBERNETES_CPU_MEMORY_RATIO = tuple(map(lambda x: x.strip(), os.environ.get(
340+
'DRYCC_CPU_MEMORY_DISTRIBUTION_RATIO', '1G,4G').split(",")))
334341
# CPU request ratio
335342
KUBERNETES_REQUEST_CPU_RATIO = int(os.environ.get('KUBERNETES_REQUEST_CPU_RATIO', '10'))
336343
# Memory request ratio
337344
KUBERNETES_REQUEST_MEMORY_RATIO = int(os.environ.get('KUBERNETES_REQUEST_MEMORY_RATIO', '2'))
338-
# Minimum limits cpu, units are represented in the millicpu of CPUs
339-
KUBERNETES_LIMITS_MIN_CPU = int(os.environ.get('KUBERNETES_LIMITS_MIN_CPU', '9'))
340-
# Minimum limits memory, units are represented in Megabytes(M)
341-
KUBERNETES_LIMITS_MIN_MEMORY = int(os.environ.get('KUBERNETES_LIMITS_MIN_MEMORY', '63'))
342-
# Maximum limits cpu, units are represented in the millicpu of CPUs
343-
KUBERNETES_LIMITS_MAX_CPU = int(os.environ.get('KUBERNETES_LIMITS_MAX_CPU', '32000'))
344-
# Maximum limits memory, units are represented in Megabytes(M)
345-
KUBERNETES_LIMITS_MAX_MEMORY = int(os.environ.get('KUBERNETES_LIMITS_MAX_MEMORY', '131072'))
346-
# Default CPU limit, units are represented in the millicpu of CPUs
347-
KUBERNETES_LIMITS_DEFAULT_CPU = (KUBERNETES_LIMITS_MIN_CPU + 1) * KUBERNETES_REQUEST_CPU_RATIO
348-
# Default Memory limit, units are represented in Megabytes(M)
349-
KUBERNETES_LIMITS_DEFAULT_MEMORY = (KUBERNETES_LIMITS_MIN_MEMORY + 1) * KUBERNETES_REQUEST_MEMORY_RATIO # noqa
345+
346+
# Minimum CPU limit, units are represented in the millicpu of CPUs
347+
KUBERNETES_LIMITS_MIN_CPU = 125
348+
# Minimum CPU limit, units are represented in the millicpu of CPUs
349+
KUBERNETES_LIMITS_MAX_CPU = 32 * 1000
350+
# Minimum Memory limit, units are represented in Megabytes(M)
351+
KUBERNETES_LIMITS_MIN_MEMORY = 128
352+
# Minimum Memory limit
353+
KUBERNETES_LIMITS_MAX_MEMORY = 128 * 1024
350354

351355
# Default pod spec for application.
352356
KUBERNETES_POD_DEFAULT_RESOURCES = os.environ.get(
@@ -366,39 +370,7 @@
366370
)
367371
# Default limit range spec for application namespace
368372
KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC = os.environ.get(
369-
'KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC',
370-
json.dumps({
371-
"limits": [
372-
{
373-
"default": {
374-
"cpu": "%sm" % KUBERNETES_LIMITS_DEFAULT_CPU,
375-
"memory": "%sMi" % KUBERNETES_LIMITS_DEFAULT_MEMORY
376-
},
377-
"defaultRequest": {
378-
"cpu": "%sm" % (KUBERNETES_LIMITS_MIN_CPU + 1),
379-
"memory": "%sMi" % (KUBERNETES_LIMITS_MIN_MEMORY + 1)
380-
},
381-
"max": {
382-
"cpu": "%sm" % KUBERNETES_LIMITS_MAX_CPU,
383-
"memory": "%sMi" % KUBERNETES_LIMITS_MAX_MEMORY
384-
},
385-
"min": {
386-
"cpu": "%sm" % KUBERNETES_LIMITS_MIN_CPU,
387-
"memory": "%sMi" % KUBERNETES_LIMITS_MIN_MEMORY
388-
},
389-
"type": "Container"
390-
},
391-
{
392-
"max": {
393-
"storage": "100Gi"
394-
},
395-
"min": {
396-
"storage": "100Mi"
397-
},
398-
"type": "PersistentVolumeClaim"
399-
}
400-
]
401-
})
373+
'KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC', ''
402374
)
403375

404376
# registry settings
@@ -463,8 +435,10 @@
463435
CACHES = {
464436
"default": {
465437
"BACKEND": "django_redis.cache.RedisCache",
466-
"LOCATION": ['redis://:{}@{}'.format(DRYCC_REDIS_PASSWORD, DRYCC_REDIS_ADDR) \
467-
for DRYCC_REDIS_ADDR in DRYCC_REDIS_ADDRS], # noqa
438+
"LOCATION": [
439+
'redis://:{}@{}'.format(
440+
DRYCC_REDIS_PASSWORD, DRYCC_REDIS_ADDR) for DRYCC_REDIS_ADDR in DRYCC_REDIS_ADDRS
441+
],
468442
"OPTIONS": {
469443
"CLIENT_CLASS": "django_redis.client.ShardClient",
470444
}

rootfs/api/tests/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def test_response_data_types_converted(self, mock_requests):
195195
'owner': self.user.username,
196196
'app': app_id,
197197
'values': {'PORT': '5000'},
198-
'memory': {},
198+
'memory': {'web': '1G'},
199199
'cpu': {'web': "1000m"},
200200
'tags': {},
201201
'registry': {}

rootfs/api/tests/test_limits.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def test_request_limit(self, mock_requests):
104104
self.assertNotEqual(limit1['uuid'], limit2['uuid'])
105105
memory = response.data['memory']
106106
self.assertIn('worker', memory)
107-
self.assertEqual(memory['worker'], '512M')
107+
self.assertEqual(memory['worker'], '2G')
108108
self.assertIn('web', memory)
109109
self.assertEqual(memory['web'], '1G')
110110
cpu = response.data['cpu']
@@ -120,7 +120,7 @@ def test_request_limit(self, mock_requests):
120120
self.assertEqual(limit2, limit3)
121121
memory = response.data['memory']
122122
self.assertIn('worker', memory)
123-
self.assertEqual(memory['worker'], '512M')
123+
self.assertEqual(memory['worker'], '2G')
124124
self.assertIn('web', memory)
125125
self.assertEqual(memory['web'], '1G')
126126
cpu = response.data['cpu']
@@ -141,7 +141,7 @@ def test_request_limit(self, mock_requests):
141141
self.assertEqual(response.status_code, 200, response.data)
142142
memory = response.data['memory']
143143
self.assertIn('worker', memory)
144-
self.assertEqual(memory['worker'], '512M')
144+
self.assertEqual(memory['worker'], '2G')
145145
self.assertIn('web', memory)
146146
self.assertEqual(memory['web'], '1G')
147147
cpu = response.data['cpu']
@@ -160,7 +160,7 @@ def test_request_limit(self, mock_requests):
160160
self.assertEqual(response.status_code, 200, response.data)
161161
memory = response.data['memory']
162162
self.assertIn('worker', memory)
163-
self.assertEqual(memory['worker'], '512M')
163+
self.assertEqual(memory['worker'], '2G')
164164
self.assertIn('web', memory)
165165
self.assertEqual(memory['web'], '1G')
166166
self.assertIn('db', memory)
@@ -183,7 +183,7 @@ def test_request_limit(self, mock_requests):
183183
self.assertEqual(response.status_code, 200, response.data)
184184
memory = response.data['memory']
185185
self.assertIn('worker', memory)
186-
self.assertEqual(memory['worker'], '512M')
186+
self.assertEqual(memory['worker'], '2G')
187187
self.assertIn('web', memory)
188188
self.assertEqual(memory['web'], '3G')
189189
self.assertIn('db', memory)
@@ -206,7 +206,10 @@ def test_request_limit(self, mock_requests):
206206
self.assertEqual(cpu['db'], '1000m')
207207

208208
# unset a value
209-
body = {'memory': json.dumps({'worker': None})}
209+
body = {
210+
'cpu': json.dumps({'worker': None}),
211+
'memory': json.dumps({'worker': None})
212+
}
210213
response = self.client.post(url, body)
211214
self.assertEqual(response.status_code, 201, response.data)
212215
limit4 = response.data
@@ -240,7 +243,7 @@ def test_request_limit(self, mock_requests):
240243
body = {'cpu': json.dumps({'web': '1000'})}
241244
response = self.client.post(url, body)
242245
self.assertEqual(response.status_code, 400, response.data)
243-
body = {'cpu': json.dumps({'web': '100m'})}
246+
body = {'cpu': json.dumps({'web': '125m'})}
244247
response = self.client.post(url, body)
245248
self.assertEqual(response.status_code, 201, response.data)
246249

0 commit comments

Comments
 (0)