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
 | 
