Commit f89f883e authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Implement backend allocator and management command

New module implementing instance allocation to one of the available
backends. Allocation is performed based on memory and disk utilization.
Statistics for each backend are stored in DB and updated periodically.

Allocation can be customized by creating a new allocation strategy
and declaring it in BACKEND_ALLOCATOR_MODULE setting.
parent cb381dec
......@@ -31,8 +31,6 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import random
from base64 import b64decode
from logging import getLogger
......@@ -49,6 +47,10 @@ from synnefo.db.models import Backend, VirtualMachine, VirtualMachineMetadata
from synnefo.logic.backend import create_instance, delete_instance
from synnefo.logic.utils import get_rsapi_state
from synnefo.util.rapi import GanetiApiError
from synnefo.logic.backend_allocator import BackendAllocator
from django.utils import importlib
log = getLogger('synnefo.api')
......@@ -196,7 +198,6 @@ def create_server(request):
# badRequest (400),
# serverCapacityUnavailable (503),
# overLimit (413)
req = util.get_request_dict(request)
log.debug('create_server %s', req)
......@@ -253,9 +254,13 @@ def create_server(request):
if count >= vms_limit_for_user:
raise faults.OverLimit("Server count limit exceeded for your account.")
backend = random.choice(Backend.objects.all())
# We must save the VM instance now, so that it gets a
# valid vm.backend_vm_id.
backend_allocator = BackendAllocator()
backend = backend_allocator.allocate(flavor)
if backend is None:
raise Exception
# We must save the VM instance now, so that it gets a valid
# vm.backend_vm_id.
vm = VirtualMachine.objects.create(
name=name,
backend=backend,
......
......@@ -33,3 +33,8 @@ GANETI_CREATEINSTANCE_KWARGS = {
'os': 'snf-image+default',
'hvparams': {'serial_console': False},
'wait_for_sync': False}
# This module implements the strategy for allocating a vm to a backend
BACKEND_ALLOCATOR_MODULE = "synnefo.logic.allocators.default_allocator"
# Refresh backend statistics timeout, in minutes, used in backend allocation
BACKEND_REFRESH_MIN = 15
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from __future__ import division
def allocate(backends, vm):
if len(backends) == 1:
return backends.keys()[0]
# Filter those that can not host the vm
capable_backends = dict((k, v) for k, v in backends.iteritems()\
if vm_fits_in_backend(v, vm))
# Since we are conservatively updating backend resources on each
# allocation, a backend may actually be able to host a vm (despite
# the state of the backend in db)
if not capable_backends:
capable_backends = backends
# Compute the scores for each backend
backend_scores = [(back_id, backend_score(back_resources, vm))\
for back_id, back_resources in \
capable_backends.iteritems()]
# Pick out the best
result = min(backend_scores, key=lambda (b_id, b_score): b_score)
backend_id = result[0]
return backend_id
def vm_fits_in_backend(backend, vm):
return backend['dfree'] > vm['disk'] and backend['mfree'] > vm['ram']
def backend_score(backend, flavor):
mratio = 1 - (backend['mfree'] / backend['mtotal'])
dratio = 1 - (backend['dfree'] / backend['dtotal'])
# cratio = (backend.pinst_cnt+1)/backend.ctotal
return 0.5 * (mratio + dratio)
......@@ -443,7 +443,7 @@ def get_ganeti_jobs(backend=None, bulk=False):
def get_backends(backend=None):
if backend:
return [backend]
return Backend.objects.all()
return Backend.objects.filter(offline=False)
def get_physical_resources(backend):
......
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of GRNET S.A.
import datetime
from django.utils import importlib
from synnefo import settings
from synnefo.db.models import Backend
from synnefo.logic.backend import update_resources
class BackendAllocator():
"""Wrapper class for instance allocation.
"""
def __init__(self):
self.strategy_mod =\
importlib.import_module(settings.BACKEND_ALLOCATOR_MODULE)
def allocate(self, flavor):
# Get the size of the vm
disk = flavor_disk(flavor)
ram = flavor.ram
cpu = flavor.cpu
vm = {'ram': ram, 'disk': disk, 'cpu': cpu}
# Refresh backends, if needed
refresh_backends_stats()
# Get available backends
available_backends = get_available_backends()
if not available_backends:
return None
# Find the best backend to host the vm, based on the allocation
# strategy
backend_id = self.strategy_mod.allocate(available_backends, vm)
backend = Backend.objects.get(id=backend_id)
# Reduce the free resources of the selected backend by the size of
# the vm
reduce_backend_resources(backend, vm)
return backend
def get_available_backends():
"""Get available backends from db.
"""
attrs = ['mfree', 'mtotal', 'dfree', 'dtotal', 'pinst_cnt', 'ctotal']
backends = {}
for b in Backend.objects.filter(drained=False, offline=False):
backend = {}
for a in attrs:
backend[a] = getattr(b, a)
backends[b.id] = backend
return backends
def flavor_disk(flavor):
""" Get flavor's 'real' disk size
"""
if flavor.disk_template == 'drbd':
return flavor.disk * 1024 * 2
else:
return flavor.disk * 1024
def reduce_backend_resources(backend, vm):
""" Conservatively update the resources of a backend.
Reduce the free resources of the backend by the size of the of the vm that
will host. This is an underestimation of the backend capabilities.
"""
new_mfree = backend.mfree - vm['ram']
new_dfree = backend.dfree - vm['disk']
backend.mfree = 0 if new_mfree < 0 else new_mfree
backend.dfree = 0 if new_dfree < 0 else new_dfree
backend.pinst_cnt += 1
backend.save()
def refresh_backends_stats():
""" Refresh the statistics of the backends.
Set db backend state to the actual state of the backend, if
BACKEND_REFRESH_MIN time has passed.
"""
now = datetime.datetime.now()
delta = datetime.timedelta(minutes=settings.BACKEND_REFRESH_MIN)
for b in Backend.objects.filter(drained=False, offline=False):
if now > b.updated + delta:
update_resources(b)
# Copyright 2011-2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of GRNET S.A.
#
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from synnefo import settings
import datetime
from synnefo.db.models import Backend
from synnefo.logic.backend import update_resources
class Command(BaseCommand):
can_import_settings = True
help = "Update backend statistics, which are used for instance allocation."
output_transaction = True # The management command runs inside
# an SQL transaction
option_list = BaseCommand.option_list + (
make_option('--backend_id', dest='backend_id',
help="Update statistics of only this backend"),
make_option('--older_than', dest='older_than', metavar="MINUTES",
help="Update only backends that have not been updated for\
MINUTES. Set to 0 to force update.")
)
def handle(self, **options):
if options['backend_id']:
try:
backend_id = int(options['backend_id'])
backends = [Backend.objects.get(id=backend_id)]
except ValueError:
raise CommandError("Wrong backend ID")
except Backend.DoesNotExist:
raise CommandError("Backend not found in DB")
else:
# XXX:filter drained ?
backends = Backend.objects.all()
now = datetime.datetime.now()
if options['older_than'] is not None:
minutes = int(options['older_than'])
else:
minutes = settings.BACKEND_REFRESH_MIN
delta = datetime.timedelta(minutes=minutes)
for b in backends:
if now > b.updated + delta:
update_resources(b)
print 'Successfully updated backend with id: %d' % b.id
else:
print 'Backend %d does not need update' % b.id
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment