176 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			176 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright (c) 2011 Openstack, LLC.
 | 
						|
# All Rights Reserved.
 | 
						|
#
 | 
						|
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
						|
#    not use this file except in compliance with the License. You may obtain
 | 
						|
#    a copy of the License at
 | 
						|
#
 | 
						|
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
#    Unless required by applicable law or agreed to in writing, software
 | 
						|
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
						|
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
						|
#    License for the specific language governing permissions and limitations
 | 
						|
#    under the License.
 | 
						|
"""
 | 
						|
Least Cost Scheduler is a mechanism for choosing which host machines to
 | 
						|
provision a set of resources to. The input of the least-cost-scheduler is a
 | 
						|
set of objective-functions, called the 'cost-functions', a weight for each
 | 
						|
cost-function, and a list of candidate hosts (gathered via FilterHosts).
 | 
						|
 | 
						|
The cost-function and weights are tabulated, and the host with the least cost
 | 
						|
is then selected for provisioning.
 | 
						|
"""
 | 
						|
 | 
						|
import collections
 | 
						|
 | 
						|
from nova import flags
 | 
						|
from nova import log as logging
 | 
						|
from nova.scheduler import zone_aware_scheduler
 | 
						|
from nova import utils
 | 
						|
from nova import exception
 | 
						|
 | 
						|
LOG = logging.getLogger('nova.scheduler.least_cost')
 | 
						|
 | 
						|
FLAGS = flags.FLAGS
 | 
						|
flags.DEFINE_list('least_cost_scheduler_cost_functions',
 | 
						|
                  ['nova.scheduler.least_cost.noop_cost_fn'],
 | 
						|
                  'Which cost functions the LeastCostScheduler should use.')
 | 
						|
 | 
						|
 | 
						|
# TODO(sirp): Once we have enough of these rules, we can break them out into a
 | 
						|
# cost_functions.py file (perhaps in a least_cost_scheduler directory)
 | 
						|
flags.DEFINE_integer('noop_cost_fn_weight', 1,
 | 
						|
                     'How much weight to give the noop cost function')
 | 
						|
 | 
						|
 | 
						|
def noop_cost_fn(host):
 | 
						|
    """Return a pre-weight cost of 1 for each host"""
 | 
						|
    return 1
 | 
						|
 | 
						|
 | 
						|
flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1,
 | 
						|
                     'How much weight to give the fill-first cost function')
 | 
						|
 | 
						|
 | 
						|
def compute_fill_first_cost_fn(host):
 | 
						|
    """Prefer hosts that have less ram available, filter_hosts will exclude
 | 
						|
    hosts that don't have enough ram"""
 | 
						|
    hostname, caps = host
 | 
						|
    free_mem = caps['host_memory_free']
 | 
						|
    return free_mem
 | 
						|
 | 
						|
 | 
						|
class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler):
 | 
						|
    def __init__(self, *args, **kwargs):
 | 
						|
        self.cost_fns_cache = {}
 | 
						|
        super(LeastCostScheduler, self).__init__(*args, **kwargs)
 | 
						|
 | 
						|
    def get_cost_fns(self, topic):
 | 
						|
        """Returns a list of tuples containing weights and cost functions to
 | 
						|
        use for weighing hosts
 | 
						|
        """
 | 
						|
 | 
						|
        if topic in self.cost_fns_cache:
 | 
						|
            return self.cost_fns_cache[topic]
 | 
						|
 | 
						|
        cost_fns = []
 | 
						|
        for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions:
 | 
						|
            if '.' in cost_fn_str:
 | 
						|
                short_name = cost_fn_str.split('.')[-1]
 | 
						|
            else:
 | 
						|
                short_name = cost_fn_str
 | 
						|
                cost_fn_str = "%s.%s.%s" % (
 | 
						|
                        __name__, self.__class__.__name__, short_name)
 | 
						|
 | 
						|
            if not (short_name.startswith('%s_' % topic) or
 | 
						|
                    short_name.startswith('noop')):
 | 
						|
                continue
 | 
						|
 | 
						|
            try:
 | 
						|
                # NOTE(sirp): import_class is somewhat misnamed since it can
 | 
						|
                # any callable from a module
 | 
						|
                cost_fn = utils.import_class(cost_fn_str)
 | 
						|
            except exception.ClassNotFound:
 | 
						|
                raise exception.SchedulerCostFunctionNotFound(
 | 
						|
                    cost_fn_str=cost_fn_str)
 | 
						|
 | 
						|
            try:
 | 
						|
                weight = getattr(FLAGS, "%s_weight" % cost_fn.__name__)
 | 
						|
            except AttributeError:
 | 
						|
                raise exception.SchedulerWeightFlagNotFound(
 | 
						|
                    flag_name=flag_name)
 | 
						|
 | 
						|
            cost_fns.append((weight, cost_fn))
 | 
						|
 | 
						|
        self.cost_fns_cache[topic] = cost_fns
 | 
						|
        return cost_fns
 | 
						|
 | 
						|
    def weigh_hosts(self, topic, request_spec, hosts):
 | 
						|
        """Returns a list of dictionaries of form:
 | 
						|
           [ {weight: weight, hostname: hostname, capabilities: capabs} ]
 | 
						|
        """
 | 
						|
 | 
						|
        cost_fns = self.get_cost_fns(topic)
 | 
						|
        costs = weighted_sum(domain=hosts, weighted_fns=cost_fns)
 | 
						|
 | 
						|
        weighted = []
 | 
						|
        weight_log = []
 | 
						|
        for cost, (hostname, caps) in zip(costs, hosts):
 | 
						|
            weight_log.append("%s: %s" % (hostname, "%.2f" % cost))
 | 
						|
            weight_dict = dict(weight=cost, hostname=hostname,
 | 
						|
                    capabilities=caps)
 | 
						|
            weighted.append(weight_dict)
 | 
						|
 | 
						|
        LOG.debug(_("Weighted Costs => %s") % weight_log)
 | 
						|
        return weighted
 | 
						|
 | 
						|
 | 
						|
def normalize_list(L):
 | 
						|
    """Normalize an array of numbers such that each element satisfies:
 | 
						|
        0 <= e <= 1"""
 | 
						|
    if not L:
 | 
						|
        return L
 | 
						|
    max_ = max(L)
 | 
						|
    if max_ > 0:
 | 
						|
        return [(float(e) / max_) for e in L]
 | 
						|
    return L
 | 
						|
 | 
						|
 | 
						|
def weighted_sum(domain, weighted_fns, normalize=True):
 | 
						|
    """Use the weighted-sum method to compute a score for an array of objects.
 | 
						|
    Normalize the results of the objective-functions so that the weights are
 | 
						|
    meaningful regardless of objective-function's range.
 | 
						|
 | 
						|
    domain - input to be scored
 | 
						|
    weighted_fns - list of weights and functions like:
 | 
						|
        [(weight, objective-functions)]
 | 
						|
 | 
						|
    Returns an unsorted list of scores. To pair with hosts do:
 | 
						|
        zip(scores, hosts)
 | 
						|
    """
 | 
						|
    # Table of form:
 | 
						|
    #   { domain1: [score1, score2, ..., scoreM]
 | 
						|
    #     ...
 | 
						|
    #     domainN: [score1, score2, ..., scoreM] }
 | 
						|
    score_table = collections.defaultdict(list)
 | 
						|
    for weight, fn in weighted_fns:
 | 
						|
        scores = [fn(elem) for elem in domain]
 | 
						|
 | 
						|
        if normalize:
 | 
						|
            norm_scores = normalize_list(scores)
 | 
						|
        else:
 | 
						|
            norm_scores = scores
 | 
						|
 | 
						|
        for idx, score in enumerate(norm_scores):
 | 
						|
            weighted_score = score * weight
 | 
						|
            score_table[idx].append(weighted_score)
 | 
						|
 | 
						|
    # Sum rows in table to compute score for each element in domain
 | 
						|
    domain_scores = []
 | 
						|
    for idx in sorted(score_table):
 | 
						|
        elem_score = sum(score_table[idx])
 | 
						|
        domain_scores.append(elem_score)
 | 
						|
 | 
						|
    return domain_scores
 |