diff --git a/doc/source/devref/filter_scheduler.rst b/doc/source/devref/filter_scheduler.rst index 9b75ab4a4..9efba7ed2 100644 --- a/doc/source/devref/filter_scheduler.rst +++ b/doc/source/devref/filter_scheduler.rst @@ -266,9 +266,24 @@ Weights Filter Scheduler uses so-called **weights** during its work. The Filter Scheduler weights hosts based on the config option -``scheduler_weight_classes``, this defaults to -``nova.scheduler.weights.all_weighers``, which selects the only weigher available --- the RamWeigher. Hosts are then weighted and sorted with the largest weight winning. +`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: + +:: + + 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 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, diff --git a/nova/exception.py b/nova/exception.py index a7508c75c..164cfd8cc 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -960,6 +960,11 @@ class FlavorExtraSpecsNotFound(NotFound): "key %(extra_specs_key)s.") +class ComputeHostMetricNotFound(NotFound): + msg_fmt = _("Metric %(name)s could not be found on the compute " + "host node %(host)s.%(node)s.") + + class FileNotFound(NotFound): msg_fmt = _("File %(file_path)s could not be found.") diff --git a/nova/scheduler/weights/metrics.py b/nova/scheduler/weights/metrics.py new file mode 100644 index 000000000..89d87591c --- /dev/null +++ b/nova/scheduler/weights/metrics.py @@ -0,0 +1,94 @@ +# Copyright (c) 2011 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. + +""" +Metrics Weigher. Weigh hosts by their metrics. + +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: + + metrics_weight_setting = name1=1.0, name2=-1.0 + + The final weight would be name1.value * 1.0 + name2.value * -1.0. +""" + +from oslo.config import cfg + +from nova import exception +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova.scheduler import weights + +metrics_weight_opts = [ + cfg.FloatOpt('weight_multiplier', + default=1.0, + help='Multiplier used for weighing metrics.'), + cfg.ListOpt('weight_setting', + default=[], + help='How the metrics are going to be weighed. This ' + 'should be in the form of "=, ' + '=, ...", where is one ' + 'of the metric to be weighed, and is ' + 'the corresponding ratio. So for "name1=1.0, ' + 'name2=-1.0" The final weight would be ' + 'name1.value * 1.0 + name2.value * -1.0.'), +] + +CONF = cfg.CONF +CONF.register_opts(metrics_weight_opts, group='metrics') + +LOG = logging.getLogger(__name__) + + +class MetricsWeigher(weights.BaseHostWeigher): + def __init__(self): + self._parse_setting() + + def _parse_setting(self): + self.setting = [] + bad = [] + for item in CONF.metrics.weight_setting: + try: + (name, ratio) = item.split('=') + ratio = float(ratio) + except ValueError: + name = None + ratio = None + if name and ratio is not None: + self.setting.append((name, ratio)) + else: + bad.append(item) + if bad: + LOG.error(_("Ignoring the invalid elements of" + " metrics_weight_setting: %s"), + ",".join(bad)) + + def _weight_multiplier(self): + """Override the weight multiplier.""" + return CONF.metrics.weight_multiplier + + def _weigh_object(self, host_state, weight_properties): + value = 0.0 + + for (name, ratio) in self.setting: + try: + value += host_state.metrics[name].value * ratio + except KeyError: + raise exception.ComputeHostMetricNotFound( + host=host_state.host, + node=host_state.nodename, + name=name) + return value diff --git a/nova/tests/scheduler/fakes.py b/nova/tests/scheduler/fakes.py index 8ed23d897..6d020c760 100644 --- a/nova/tests/scheduler/fakes.py +++ b/nova/tests/scheduler/fakes.py @@ -20,6 +20,7 @@ import mox from nova.compute import vm_states from nova import db +from nova.openstack.common import jsonutils from nova.scheduler import filter_scheduler from nova.scheduler import host_manager @@ -53,6 +54,77 @@ COMPUTE_NODES = [ dict(id=5, local_gb=1024, memory_mb=1024, vcpus=1, service=None), ] +COMPUTE_NODES_METRICS = [ + dict(id=1, local_gb=1024, memory_mb=1024, vcpus=1, + disk_available_least=512, free_ram_mb=512, vcpus_used=1, + free_disk_mb=512, local_gb_used=0, updated_at=None, + service=dict(host='host1', disabled=False), + hypervisor_hostname='node1', host_ip='127.0.0.1', + hypervisor_version=0, + metrics=jsonutils.dumps([{'name': 'foo', + 'value': 512, + 'timestamp': None, + 'source': 'host1' + }, + {'name': 'bar', + 'value': 1.0, + 'timestamp': None, + 'source': 'host1' + }, + ])), + dict(id=2, local_gb=2048, memory_mb=2048, vcpus=2, + disk_available_least=1024, free_ram_mb=1024, vcpus_used=2, + free_disk_mb=1024, local_gb_used=0, updated_at=None, + service=dict(host='host2', disabled=True), + hypervisor_hostname='node2', host_ip='127.0.0.1', + hypervisor_version=0, + metrics=jsonutils.dumps([{'name': 'foo', + 'value': 1024, + 'timestamp': None, + 'source': 'host2' + }, + {'name': 'bar', + 'value': 2.0, + 'timestamp': None, + 'source': 'host2' + }, + ])), + dict(id=3, local_gb=4096, memory_mb=4096, vcpus=4, + disk_available_least=3072, free_ram_mb=3072, vcpus_used=1, + free_disk_mb=3072, local_gb_used=0, updated_at=None, + service=dict(host='host3', disabled=False), + hypervisor_hostname='node3', host_ip='127.0.0.1', + hypervisor_version=0, + metrics=jsonutils.dumps([{'name': 'foo', + 'value': 3072, + 'timestamp': None, + 'source': 'host3' + }, + {'name': 'bar', + 'value': 1.0, + 'timestamp': None, + 'source': 'host3' + }, + ])), + dict(id=4, local_gb=8192, memory_mb=8192, vcpus=8, + disk_available_least=8192, free_ram_mb=8192, vcpus_used=0, + free_disk_mb=8192, local_gb_used=0, updated_at=None, + service=dict(host='host4', disabled=False), + hypervisor_hostname='node4', host_ip='127.0.0.1', + hypervisor_version=0, + metrics=jsonutils.dumps([{'name': 'foo', + 'value': 8192, + 'timestamp': None, + 'source': 'host4' + }, + {'name': 'bar', + 'value': 0, + 'timestamp': None, + 'source': 'host4' + }, + ])), +] + INSTANCES = [ dict(root_gb=512, ephemeral_gb=0, memory_mb=512, vcpus=1, host='host1', node='node1'), diff --git a/nova/tests/scheduler/test_weights.py b/nova/tests/scheduler/test_weights.py index 7495edacc..dfbe3d6f2 100644 --- a/nova/tests/scheduler/test_weights.py +++ b/nova/tests/scheduler/test_weights.py @@ -17,6 +17,8 @@ Tests For Scheduler weights. """ from nova import context +from nova import exception +from nova.openstack.common.fixture import mockpatch from nova.scheduler import weights from nova import test from nova.tests import matchers @@ -34,13 +36,17 @@ class TestWeighedHost(test.NoDBTestCase): def test_all_weighers(self): classes = weights.all_weighers() class_names = [cls.__name__ for cls in classes] - self.assertEqual(len(classes), 1) + self.assertEqual(len(classes), 2) self.assertIn('RAMWeigher', class_names) + self.assertIn('MetricsWeigher', class_names) class RamWeigherTestCase(test.NoDBTestCase): def setUp(self): super(RamWeigherTestCase, self).setUp() + self.useFixture(mockpatch.Patch( + 'nova.db.compute_node_get_all', + return_value=fakes.COMPUTE_NODES)) self.host_manager = fakes.FakeHostManager() self.weight_handler = weights.HostWeightHandler() self.weight_classes = self.weight_handler.get_matching_classes( @@ -54,12 +60,7 @@ class RamWeigherTestCase(test.NoDBTestCase): def _get_all_hosts(self): ctxt = context.get_admin_context() - fakes.mox_host_manager_db_calls(self.mox, ctxt) - self.mox.ReplayAll() - host_states = self.host_manager.get_all_host_states(ctxt) - self.mox.VerifyAll() - self.mox.ResetAll() - return host_states + return self.host_manager.get_all_host_states(ctxt) def test_default_of_spreading_first(self): hostinfo_list = self._get_all_hosts() @@ -101,3 +102,110 @@ class RamWeigherTestCase(test.NoDBTestCase): weighed_host = self._get_weighed_host(hostinfo_list) self.assertEqual(weighed_host.weight, 8192 * 2) self.assertEqual(weighed_host.obj.host, 'host4') + + +class MetricsWeigherTestCase(test.NoDBTestCase): + def setUp(self): + super(MetricsWeigherTestCase, self).setUp() + self.useFixture(mockpatch.Patch( + 'nova.db.compute_node_get_all', + return_value=fakes.COMPUTE_NODES_METRICS)) + self.host_manager = fakes.FakeHostManager() + self.weight_handler = weights.HostWeightHandler() + self.weight_classes = self.weight_handler.get_matching_classes( + ['nova.scheduler.weights.metrics.MetricsWeigher']) + + def _get_weighed_host(self, hosts, setting, weight_properties=None): + if not weight_properties: + weight_properties = {} + self.flags(weight_setting=setting, group='metrics') + return self.weight_handler.get_weighed_objects(self.weight_classes, + hosts, weight_properties)[0] + + def _get_all_hosts(self): + ctxt = context.get_admin_context() + return self.host_manager.get_all_host_states(ctxt) + + def _do_test(self, settings, expected_weight, expected_host): + hostinfo_list = self._get_all_hosts() + weighed_host = self._get_weighed_host(hostinfo_list, settings) + self.assertEqual(weighed_host.weight, expected_weight) + self.assertEqual(weighed_host.obj.host, expected_host) + + def test_single_resource(self): + # host1: foo=512 + # host2: foo=1024 + # host3: foo=3072 + # host4: foo=8192 + # so, host4 should win: + setting = ['foo=1'] + self._do_test(setting, 8192, 'host4') + + def test_multiple_resource(self): + # host1: foo=512, bar=1 + # host2: foo=1024, bar=2 + # host3: foo=3072, bar=1 + # host4: foo=8192, bar=0 + # so, host2 should win: + setting = ['foo=0.0001', 'bar=1'] + self._do_test(setting, 2.1024, 'host2') + + def test_single_resourcenegtive_ratio(self): + # host1: foo=512 + # host2: foo=1024 + # host3: foo=3072 + # host4: foo=8192 + # so, host1 should win: + setting = ['foo=-1'] + self._do_test(setting, -512, 'host1') + + def test_multiple_resource_missing_ratio(self): + # host1: foo=512, bar=1 + # host2: foo=1024, bar=2 + # host3: foo=3072, bar=1 + # host4: foo=8192, bar=0 + # so, host4 should win: + setting = ['foo=0.0001', 'bar'] + self._do_test(setting, 0.8192, 'host4') + + def test_multiple_resource_wrong_ratio(self): + # host1: foo=512, bar=1 + # host2: foo=1024, bar=2 + # host3: foo=3072, bar=1 + # host4: foo=8192, bar=0 + # so, host4 should win: + setting = ['foo=0.0001', 'bar = 2.0t'] + self._do_test(setting, 0.8192, 'host4') + + def _check_parsing_result(self, weigher, setting, results): + self.flags(weight_setting=setting, group='metrics') + weigher._parse_setting() + self.assertTrue(len(results) == len(weigher.setting)) + for item in results: + self.assertTrue(item in weigher.setting) + + def test_parse_setting(self): + weigher = self.weight_classes[0]() + self._check_parsing_result(weigher, + ['foo=1'], + [('foo', 1.0)]) + self._check_parsing_result(weigher, + ['foo=1', 'bar=-2.1'], + [('foo', 1.0), ('bar', -2.1)]) + self._check_parsing_result(weigher, + ['foo=a1', 'bar=-2.1'], + [('bar', -2.1)]) + self._check_parsing_result(weigher, + ['foo', 'bar=-2.1'], + [('bar', -2.1)]) + self._check_parsing_result(weigher, + ['=5', 'bar=-2.1'], + [('bar', -2.1)]) + + def test_metric_not_found(self): + setting = ['foo=1', 'zot=2'] + self.assertRaises(exception.ComputeHostMetricNotFound, + self._do_test, + setting, + 8192, + 'host4')