diff --git a/doc/source/devref/filter_scheduler.rst b/doc/source/devref/filter_scheduler.rst index 9efba7ed2817..dc217099f677 100644 --- a/doc/source/devref/filter_scheduler.rst +++ b/doc/source/devref/filter_scheduler.rst @@ -263,27 +263,35 @@ default when no filters are specified in the request. Weights ------- -Filter Scheduler uses so-called **weights** during its work. +Filter Scheduler uses the so called **weights** during its work. A weigher is a +way to select the best suitable host from a group of valid hosts by giving +weights to all the hosts in the list. -The Filter Scheduler weights hosts based on the config option +In order to prioritize one weigher against another, all the weighers have to +define a multiplier that will be applied before computing the weight for a node. +All the weights are normalized beforehand so that the multiplier can be applied +easily. Therefore the final weight for the object will be:: + + weight = w1_multiplier * norm(w1) + w2_multiplier * norm(w2) + ... + +A weigher should be a subclass of ``weights.BaseHostWeigher`` and they must +implement the ``weight_multiplier`` and ``weight_object`` methods. If the +``weight_objects`` method is overriden it just return a list of weights, and not +modify the weight of the object directly, since final weights are normalized and +computed by ``weight.BaseWeightHandler``. + +The Filter Scheduler weighs hosts based on the config option `scheduler_weight_classes`, this defaults to -`nova.scheduler.weights.all_weighers`, which selects all the available weighers -in the package nova.scheduler.weights. Hosts are then weighted and sorted with -the largest weight winning. For each host, the final weight is calculated by -summing up all weigher's weight value multiplying its own weight_mutiplier: +`nova.scheduler.weights.all_weighers`, which selects the following weighers: -:: +* |RamWeigher| Hosts are then weighted and sorted with the largest weight winning. + If the multiplier is negative, the host with less RAM available will win (useful + for stacking hosts, instead of spreading). +* |MetricsWeigher| This weigher can compute the weight based on the compute node + host's various metrics. The to-be weighed metrics and their weighing ratio + are specified in the configuration file as the followings:: - final_weight = 0 - for each weigher: - final_weight += weigher's weight_mutiplier * weigher's calculated weight value - -The weigher's weight_mutiplier can be set in the configuration file, e.g. - -:: - - [metrics] - weight_multiplier=1.0 + metrics_weight_setting = name1=1.0, name2=-1.0 Filter Scheduler finds local list of acceptable hosts by repeated filtering and weighing. Each time it chooses a host, it virtually consumes resources on it, @@ -322,3 +330,5 @@ in :mod:``nova.tests.scheduler``. .. |AggregateTypeAffinityFilter| replace:: :class:`AggregateTypeAffinityFilter ` .. |AggregateInstanceExtraSpecsFilter| replace:: :class:`AggregateInstanceExtraSpecsFilter ` .. |AggregateMultiTenancyIsolation| replace:: :class:`AggregateMultiTenancyIsolation ` + +.. |RamWeigher| replace:: :class:`RamWeigher ` diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index 9164600ec1b5..3835690340a5 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -3058,6 +3058,15 @@ #ram_weight_multiplier=10.0 +# +# Options defined in nova.cells.weights.weight_offset +# + +# Multiplier used to weigh offset weigher. (floating point +# value) +#offset_weight_multiplier=1.0 + + [baremetal] # diff --git a/nova/cells/weights/mute_child.py b/nova/cells/weights/mute_child.py index 7bffb739b476..b21670ad88de 100644 --- a/nova/cells/weights/mute_child.py +++ b/nova/cells/weights/mute_child.py @@ -48,7 +48,7 @@ class MuteChildWeigher(weights.BaseCellWeigher): weight. """ - def _weight_multiplier(self): + def weight_multiplier(self): # negative multiplier => lower weight return CONF.cells.mute_weight_multiplier diff --git a/nova/cells/weights/ram_by_instance_type.py b/nova/cells/weights/ram_by_instance_type.py index 1a1d164d2a1a..aa7fe768a720 100644 --- a/nova/cells/weights/ram_by_instance_type.py +++ b/nova/cells/weights/ram_by_instance_type.py @@ -34,7 +34,7 @@ CONF.register_opts(ram_weigher_opts, group='cells') class RamByInstanceTypeWeigher(weights.BaseCellWeigher): """Weigh cells by instance_type requested.""" - def _weight_multiplier(self): + def weight_multiplier(self): return CONF.cells.ram_weight_multiplier def _weigh_object(self, cell, weight_properties): diff --git a/nova/cells/weights/weight_offset.py b/nova/cells/weights/weight_offset.py index 14348ee20345..b2d9f31e4100 100644 --- a/nova/cells/weights/weight_offset.py +++ b/nova/cells/weights/weight_offset.py @@ -18,8 +18,19 @@ Weigh cells by their weight_offset in the DB. Cells with higher weight_offsets in the DB will be preferred. """ +from oslo.config import cfg + from nova.cells import weights +weigher_opts = [ + cfg.FloatOpt('offset_weight_multiplier', + default=1.0, + help='Multiplier used to weigh offset weigher.'), +] + +CONF = cfg.CONF +CONF.register_opts(weigher_opts, group='cells') + class WeightOffsetWeigher(weights.BaseCellWeigher): """ @@ -28,6 +39,9 @@ class WeightOffsetWeigher(weights.BaseCellWeigher): its weight_offset to 999999999999999 (highest weight wins) """ + def weight_multiplier(self): + return CONF.cells.offset_weight_multiplier + def _weigh_object(self, cell, weight_properties): """Returns whatever was in the DB for weight_offset.""" return cell.db_info.get('weight_offset', 0) diff --git a/nova/scheduler/weights/metrics.py b/nova/scheduler/weights/metrics.py index 89d87591c0d0..7dd2b78f32ea 100644 --- a/nova/scheduler/weights/metrics.py +++ b/nova/scheduler/weights/metrics.py @@ -76,7 +76,7 @@ class MetricsWeigher(weights.BaseHostWeigher): " metrics_weight_setting: %s"), ",".join(bad)) - def _weight_multiplier(self): + def weight_multiplier(self): """Override the weight multiplier.""" return CONF.metrics.weight_multiplier diff --git a/nova/scheduler/weights/ram.py b/nova/scheduler/weights/ram.py index b8478c7bd59d..b17ccbe0fae2 100644 --- a/nova/scheduler/weights/ram.py +++ b/nova/scheduler/weights/ram.py @@ -36,7 +36,9 @@ CONF.register_opts(ram_weight_opts) class RAMWeigher(weights.BaseHostWeigher): - def _weight_multiplier(self): + minval = 0 + + def weight_multiplier(self): """Override the weight multiplier.""" return CONF.ram_weight_multiplier diff --git a/nova/tests/cells/test_cells_weights.py b/nova/tests/cells/test_cells_weights.py index 1c5e8c24b50c..fc46e10279e0 100644 --- a/nova/tests/cells/test_cells_weights.py +++ b/nova/tests/cells/test_cells_weights.py @@ -213,5 +213,5 @@ class MuteWeigherTestClass(_WeigherTestClass): for i in range(2): weighed_cell = weighed_cells.pop(0) - self.assertEqual(1000 * -10.0, weighed_cell.weight) + self.assertEqual(-10.0, weighed_cell.weight) self.assertIn(weighed_cell.obj.name, ['cell1', 'cell2']) diff --git a/nova/tests/scheduler/test_weights.py b/nova/tests/scheduler/test_weights.py index dfbe3d6f2d7a..619c47b22edf 100644 --- a/nova/tests/scheduler/test_weights.py +++ b/nova/tests/scheduler/test_weights.py @@ -72,37 +72,62 @@ class RamWeigherTestCase(test.NoDBTestCase): # so, host4 should win: weighed_host = self._get_weighed_host(hostinfo_list) - self.assertEqual(weighed_host.weight, 8192) + self.assertEqual(weighed_host.weight, 1.0) self.assertEqual(weighed_host.obj.host, 'host4') def test_ram_filter_multiplier1(self): - self.flags(ram_weight_multiplier=-1.0) + self.flags(ram_weight_multiplier=0.0) hostinfo_list = self._get_all_hosts() - # host1: free_ram_mb=-512 - # host2: free_ram_mb=-1024 - # host3: free_ram_mb=-3072 - # host4: free_ram_mb=-8192 + # host1: free_ram_mb=512 + # host2: free_ram_mb=1024 + # host3: free_ram_mb=3072 + # host4: free_ram_mb=8192 - # so, host1 should win: + # We do not know the host, all have same weight. weighed_host = self._get_weighed_host(hostinfo_list) - self.assertEqual(weighed_host.weight, -512) - self.assertEqual(weighed_host.obj.host, 'host1') + self.assertEqual(weighed_host.weight, 0.0) def test_ram_filter_multiplier2(self): self.flags(ram_weight_multiplier=2.0) hostinfo_list = self._get_all_hosts() - # host1: free_ram_mb=512 * 2 - # host2: free_ram_mb=1024 * 2 - # host3: free_ram_mb=3072 * 2 - # host4: free_ram_mb=8192 * 2 + # host1: free_ram_mb=512 + # host2: free_ram_mb=1024 + # host3: free_ram_mb=3072 + # host4: free_ram_mb=8192 # so, host4 should win: weighed_host = self._get_weighed_host(hostinfo_list) - self.assertEqual(weighed_host.weight, 8192 * 2) + self.assertEqual(weighed_host.weight, 1.0 * 2) self.assertEqual(weighed_host.obj.host, 'host4') + def test_ram_filter_negative(self): + self.flags(ram_weight_multiplier=1.0) + hostinfo_list = self._get_all_hosts() + host_attr = {'id': 100, 'memory_mb': 8192, 'free_ram_mb': -512} + host_state = fakes.FakeHostState('negative', 'negative', host_attr) + hostinfo_list = list(hostinfo_list) + [host_state] + + # host1: free_ram_mb=512 + # host2: free_ram_mb=1024 + # host3: free_ram_mb=3072 + # host4: free_ram_mb=8192 + # negativehost: free_ram_mb=-512 + + # so, host4 should win + weights = self.weight_handler.get_weighed_objects(self.weight_classes, + hostinfo_list, {}) + + weighed_host = weights[0] + self.assertEqual(weighed_host.weight, 1) + self.assertEqual(weighed_host.obj.host, "host4") + + # and negativehost should lose + weighed_host = weights[-1] + self.assertEqual(weighed_host.weight, 0) + self.assertEqual(weighed_host.obj.host, "negative") + class MetricsWeigherTestCase(test.NoDBTestCase): def setUp(self): @@ -139,7 +164,7 @@ class MetricsWeigherTestCase(test.NoDBTestCase): # host4: foo=8192 # so, host4 should win: setting = ['foo=1'] - self._do_test(setting, 8192, 'host4') + self._do_test(setting, 1.0, 'host4') def test_multiple_resource(self): # host1: foo=512, bar=1 @@ -148,7 +173,7 @@ class MetricsWeigherTestCase(test.NoDBTestCase): # host4: foo=8192, bar=0 # so, host2 should win: setting = ['foo=0.0001', 'bar=1'] - self._do_test(setting, 2.1024, 'host2') + self._do_test(setting, 1.0, 'host2') def test_single_resourcenegtive_ratio(self): # host1: foo=512 @@ -157,7 +182,7 @@ class MetricsWeigherTestCase(test.NoDBTestCase): # host4: foo=8192 # so, host1 should win: setting = ['foo=-1'] - self._do_test(setting, -512, 'host1') + self._do_test(setting, 1.0, 'host1') def test_multiple_resource_missing_ratio(self): # host1: foo=512, bar=1 @@ -166,7 +191,7 @@ class MetricsWeigherTestCase(test.NoDBTestCase): # host4: foo=8192, bar=0 # so, host4 should win: setting = ['foo=0.0001', 'bar'] - self._do_test(setting, 0.8192, 'host4') + self._do_test(setting, 1.0, 'host4') def test_multiple_resource_wrong_ratio(self): # host1: foo=512, bar=1 @@ -175,7 +200,7 @@ class MetricsWeigherTestCase(test.NoDBTestCase): # host4: foo=8192, bar=0 # so, host4 should win: setting = ['foo=0.0001', 'bar = 2.0t'] - self._do_test(setting, 0.8192, 'host4') + self._do_test(setting, 1.0, 'host4') def _check_parsing_result(self, weigher, setting, results): self.flags(weight_setting=setting, group='metrics') diff --git a/nova/tests/test_weights.py b/nova/tests/test_weights.py new file mode 100644 index 000000000000..33eafadf69ac --- /dev/null +++ b/nova/tests/test_weights.py @@ -0,0 +1,54 @@ +# Copyright 2011-2012 OpenStack Foundation +# 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. +""" +Tests For weights. +""" + +from nova import test +from nova import weights + + +class TestWeigher(test.NoDBTestCase): + def test_no_multiplier(self): + class FakeWeigher(weights.BaseWeigher): + def _weigh_object(self, *args, **kwargs): + pass + + self.assertEqual(1.0, + FakeWeigher().weight_multiplier()) + + def test_no_weight_object(self): + class FakeWeigher(weights.BaseWeigher): + def weight_multiplier(self, *args, **kwargs): + pass + self.assertRaises(TypeError, + FakeWeigher) + + def test_normalization(self): + # weight_list, expected_result, minval, maxval + map_ = ( + ((), (), None, None), + ((0.0, 0.0), (0.0, 0.0), None, None), + ((1.0, 1.0), (0.0, 0.0), None, None), + + ((20.0, 50.0), (0.0, 1.0), None, None), + ((20.0, 50.0), (0.0, 0.375), None, 100.0), + ((20.0, 50.0), (0.4, 1.0), 0.0, None), + ((20.0, 50.0), (0.2, 0.5), 0.0, 100.0), + ) + normalize_to = (1.0, 10.0) + for seq, result, minval, maxval in map_: + ret = weights.normalize(seq, minval=minval, maxval=maxval) + self.assertEqual(tuple(ret), result) diff --git a/nova/weights.py b/nova/weights.py index da5c93a7adbd..c8ee1cd35574 100644 --- a/nova/weights.py +++ b/nova/weights.py @@ -17,9 +17,40 @@ Pluggable Weighing support """ +import abc + from nova import loadables +def normalize(weight_list, minval=None, maxval=None): + """Normalize the values in a list between 0 and 1.0. + + The normalization is made regarding the lower and upper values present in + weight_list. If the minval and/or maxval parameters are set, these values + will be used instead of the minimum and maximum from the list. + + If all the values are equal, they are normalized to 0. + """ + + if not weight_list: + return () + + if maxval is None: + maxval = max(weight_list) + + if minval is None: + minval = min(weight_list) + + maxval = float(maxval) + minval = float(minval) + + if minval == maxval: + return [0] * len(weight_list) + + range_ = maxval - minval + return ((i - minval) / range_ for i in weight_list) + + class WeighedObject(object): """Object with weight information.""" def __init__(self, obj, weight): @@ -31,26 +62,59 @@ class WeighedObject(object): class BaseWeigher(object): - """Base class for pluggable weighers.""" - def _weight_multiplier(self): - """How weighted this weigher should be. Normally this would - be overridden in a subclass based on a config value. + """Base class for pluggable weighers. + + The attributes maxval and minval can be specified to set up the maximum + and minimum values for the weighed objects. These values will then be + taken into account in the normalization step, instead of taking the values + from the calculated weights. + """ + + __metaclass__ = abc.ABCMeta + + minval = None + maxval = None + + def weight_multiplier(self): + """How weighted this weigher should be. + + Override this method in a subclass, so that the returned value is + read from a configuration option to permit operators specify a + multiplier for the weigher. """ return 1.0 + @abc.abstractmethod def _weigh_object(self, obj, weight_properties): - """Override in a subclass to specify a weight for a specific - object. - """ - return 0.0 + """Weigh an specific object.""" def weigh_objects(self, weighed_obj_list, weight_properties): - """Weigh multiple objects. Override in a subclass if you need - need access to all objects in order to manipulate weights. + """Weigh multiple objects. + + Override in a subclass if you need access to all objects in order + to calculate weights. Do not modify the weight of an object here, + just return a list of weights. """ + # Calculate the weights + weights = [] for obj in weighed_obj_list: - obj.weight += (self._weight_multiplier() * - self._weigh_object(obj.obj, weight_properties)) + weight = self._weigh_object(obj.obj, weight_properties) + + # Record the min and max values if they are None. If they anything + # but none we assume that the weigher has set them + if self.minval is None: + self.minval = weight + if self.maxval is None: + self.maxval = weight + + if weight < self.minval: + self.minval = weight + elif weight > self.maxval: + self.maxval = weight + + weights.append(weight) + + return weights class BaseWeightHandler(loadables.BaseLoader): @@ -58,7 +122,7 @@ class BaseWeightHandler(loadables.BaseLoader): def get_weighed_objects(self, weigher_classes, obj_list, weighing_properties): - """Return a sorted (highest score first) list of WeighedObjects.""" + """Return a sorted (descending), normalized list of WeighedObjects.""" if not obj_list: return [] @@ -66,6 +130,15 @@ class BaseWeightHandler(loadables.BaseLoader): weighed_objs = [self.object_class(obj, 0.0) for obj in obj_list] for weigher_cls in weigher_classes: weigher = weigher_cls() - weigher.weigh_objects(weighed_objs, weighing_properties) + weights = weigher.weigh_objects(weighed_objs, weighing_properties) + + # Normalize the weights + weights = normalize(weights, + minval=weigher.minval, + maxval=weigher.maxval) + + for i, weight in enumerate(weights): + obj = weighed_objs[i] + obj.weight += weigher.weight_multiplier() * weight return sorted(weighed_objs, key=lambda x: x.weight, reverse=True)