Merge "Add a new ImagePropertiesWeigher"
This commit is contained in:
@@ -1073,6 +1073,19 @@ strategy) while a negative value will follow a spread strategy that will
|
||||
favor hosts with the lesser number of instances.
|
||||
|
||||
|
||||
``ImagePropertiesWeigher``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 31.0.0 (Epoxy)
|
||||
|
||||
This weigher compares hosts and orders them based on the existing instances
|
||||
image properties respectively. By default the weigher is doing nothing but you
|
||||
can change its behaviour by modifying the value of
|
||||
:oslo.config:option:`filter_scheduler.image_props_weight_multiplier`.
|
||||
A positive value will favor hosts with the same image properties (packing
|
||||
strategy) while a negative value will follow a spread strategy that will
|
||||
favor hosts not already having instances with those image properties.
|
||||
|
||||
Utilization-aware scheduling
|
||||
----------------------------
|
||||
|
||||
|
||||
@@ -544,6 +544,50 @@ Possible values:
|
||||
|
||||
Related options:
|
||||
|
||||
* ``[filter_scheduler] weight_classes``
|
||||
"""),
|
||||
cfg.FloatOpt("image_props_weight_multiplier",
|
||||
default=0.0,
|
||||
help="""
|
||||
Image Properties weight multiplier ratio.
|
||||
|
||||
The multiplier is used for weighting hosts based on the reported
|
||||
image properties for the instances they have.
|
||||
A positive value will favor hosts with the same image properties (packing
|
||||
strategy) while a negative value will follow a spread strategy that will favor
|
||||
hosts not already having instances with those image properties.
|
||||
The default value of the multiplier is 0, which disables the weigher.
|
||||
|
||||
Possible values:
|
||||
|
||||
* An integer or float value, where the value corresponds to the multiplier
|
||||
ratio for this weigher.
|
||||
|
||||
Example:
|
||||
|
||||
* Strongly prefer to pack instances with related image properties.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[filter_scheduler]
|
||||
image_props_weight_multiplier=1000
|
||||
|
||||
* Softly prefer to spread instances having same properties between hosts
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[filter_scheduler]
|
||||
image_props_weight_multiplier=-1.0
|
||||
|
||||
* Disable weigher influence
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[filter_scheduler]
|
||||
image_props_weight_multiplier=0
|
||||
|
||||
Related options:
|
||||
|
||||
* ``[filter_scheduler] weight_classes``
|
||||
"""),
|
||||
cfg.FloatOpt("pci_weight_multiplier",
|
||||
|
||||
@@ -736,6 +736,10 @@ class ImageMetaProps(base.NovaObject):
|
||||
|
||||
return obj
|
||||
|
||||
def to_dict(self):
|
||||
"""Returns a dictionary of image properties that are set."""
|
||||
return base.obj_to_primitive(self)
|
||||
|
||||
def get(self, name, defvalue=None):
|
||||
"""Get the value of an attribute
|
||||
:param name: the attribute to request
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Image Properties Weigher. Weigh hosts by the image metadata properties
|
||||
related to the existing instances.
|
||||
|
||||
A positive value will favor hosts with the same image properties (packing
|
||||
strategy) while a negative value will follow a spread strategy that will favor
|
||||
hosts not already having instances with those image properties.
|
||||
The default value of the multiplier is 0, which disables the weigher.
|
||||
"""
|
||||
|
||||
import nova.conf
|
||||
from nova import exception
|
||||
from nova import objects
|
||||
from nova.scheduler import utils
|
||||
from nova.scheduler import weights
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
||||
class ImagePropertiesWeigher(weights.BaseHostWeigher):
|
||||
|
||||
def weight_multiplier(self, host_state):
|
||||
"""Override the weight multiplier."""
|
||||
return utils.get_weight_multiplier(
|
||||
host_state, 'image_props_weight_multiplier',
|
||||
CONF.filter_scheduler.image_props_weight_multiplier)
|
||||
|
||||
def _weigh_object(self, host_state, request_spec):
|
||||
"""Higher weights win. We want to choose hosts with the more common
|
||||
existing image properties that are used by instances by default.
|
||||
If you want to spread instances with the same properties between
|
||||
hosts, change the multiplier value to a negative number.
|
||||
"""
|
||||
|
||||
weight = 0.0
|
||||
|
||||
# Disable this weigher if we don't use it as it's a bit costly.
|
||||
if CONF.filter_scheduler.image_props_weight_multiplier == 0.0:
|
||||
return weight
|
||||
|
||||
# request_spec is a RequestSpec object which can have its image
|
||||
# field set to None
|
||||
if request_spec.image:
|
||||
# List values aren't hashable so we need to stringify them.
|
||||
requested_props = {(key, f"{value}") for key, value in
|
||||
request_spec.image.properties.to_dict().items()}
|
||||
else:
|
||||
requested_props = set()
|
||||
|
||||
existing_props = []
|
||||
|
||||
insts = objects.InstanceList(objects=host_state.instances.values())
|
||||
# system_metadata isn't loaded yet, let's do this.
|
||||
insts.fill_metadata()
|
||||
|
||||
for inst in insts:
|
||||
try:
|
||||
props = {(key, str(value)) for key, value in
|
||||
inst.image_meta.properties.to_dict().items()
|
||||
} if inst.image_meta else set()
|
||||
except exception.InstanceNotFound:
|
||||
# the host state can be a bit stale as the instance could no
|
||||
# longer exist on the host if the instance deletion arrives
|
||||
# before the scheduler gets the RPC message of the deletion
|
||||
props = set()
|
||||
# We want to unpack the set of tuples as items to the total list
|
||||
# of properties we need to compare.
|
||||
existing_props.extend(tup for tup in props)
|
||||
|
||||
common_props = requested_props & set(existing_props)
|
||||
|
||||
for prop in common_props:
|
||||
weight += existing_props.count(prop)
|
||||
return weight
|
||||
@@ -146,6 +146,12 @@ class TestImageMetaProps(test.NoDBTestCase):
|
||||
virtprops.get,
|
||||
"doesnotexist")
|
||||
|
||||
def test_to_dict(self):
|
||||
props = {'os_type': 'windows'}
|
||||
virtprops = objects.ImageMetaProps.from_dict(props)
|
||||
self.assertEqual({'os_type': 'windows'},
|
||||
virtprops.to_dict())
|
||||
|
||||
def test_legacy_compat(self):
|
||||
legacy_props = {
|
||||
'architecture': 'x86_64',
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# 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 Scheduler image properties weights.
|
||||
"""
|
||||
from unittest import mock
|
||||
|
||||
from oslo_utils.fixture import uuidsentinel as uuids
|
||||
|
||||
from nova import objects
|
||||
from nova.scheduler import weights
|
||||
from nova.scheduler.weights import image_props
|
||||
from nova import test
|
||||
from nova.tests.unit.scheduler import fakes
|
||||
|
||||
PROP_PC = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(hw_machine_type='pc'))
|
||||
|
||||
PROP_WIN_PC = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(os_distro='windows',
|
||||
hw_machine_type='pc'))
|
||||
|
||||
PROP_LIN = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(os_distro='linux'))
|
||||
|
||||
PROP_LIN_PC = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(os_distro='linux', hw_machine_type='pc'))
|
||||
|
||||
|
||||
class ImagePropertiesWeigherTestCase(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.weight_handler = weights.HostWeightHandler()
|
||||
self.weighers = [image_props.ImagePropertiesWeigher()]
|
||||
|
||||
self.stub_out('nova.objects.ImageMeta.from_instance',
|
||||
self._fake_image_meta_from_instance)
|
||||
|
||||
@staticmethod
|
||||
def _fake_image_meta_from_instance(self, inst):
|
||||
# inst1 is on host2
|
||||
if inst.uuid == uuids.inst1:
|
||||
return PROP_WIN_PC
|
||||
# inst2 and inst3 are on host3
|
||||
elif inst.uuid == uuids.inst2:
|
||||
return PROP_LIN
|
||||
elif inst.uuid == uuids.inst3:
|
||||
return PROP_LIN_PC
|
||||
# inst4 is on host4
|
||||
elif inst.uuid == uuids.inst4:
|
||||
return PROP_LIN
|
||||
else:
|
||||
return objects.ImageMeta(properties=objects.ImageMetaProps())
|
||||
|
||||
def _get_weighed_host(self, hosts, request_spec=None):
|
||||
if request_spec is None:
|
||||
request_spec = objects.RequestSpec(image=None)
|
||||
return self.weight_handler.get_weighed_objects(self.weighers,
|
||||
hosts, request_spec)[0]
|
||||
|
||||
def _get_all_hosts(self):
|
||||
|
||||
host_values = [
|
||||
# host1 has no instances
|
||||
('host1', 'node1', {'instances': {}}),
|
||||
# host2 has one instance with os_distro=win and hw_machine_type=pc
|
||||
('host2', 'node2', {'instances': {uuids.inst1: objects.Instance(
|
||||
uuid=uuids.inst1)}}),
|
||||
# host3 has two instances
|
||||
# (os_distro=lin twice, hw_machine_type=pc only once)
|
||||
('host3', 'node3', {'instances': {uuids.inst2: objects.Instance(
|
||||
uuid=uuids.inst2),
|
||||
uuids.inst3: objects.Instance(
|
||||
uuid=uuids.inst3)}}),
|
||||
# host4 has one instance with os_distro=lin only
|
||||
('host4', 'node4', {'instances': {uuids.inst2: objects.Instance(
|
||||
uuid=uuids.inst4)}}),
|
||||
|
||||
]
|
||||
return [fakes.FakeHostState(host, node, values)
|
||||
for host, node, values in host_values]
|
||||
|
||||
@mock.patch('nova.objects.InstanceList.fill_metadata')
|
||||
def test_multiplier_default(self, mock_fm):
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
weighed_host = self._get_weighed_host(hostinfo_list)
|
||||
self.assertEqual(0.0, weighed_host.weight)
|
||||
# Nothing changes, we just returns the first in the list
|
||||
self.assertEqual('host1', weighed_host.obj.host)
|
||||
mock_fm.assert_not_called()
|
||||
|
||||
@mock.patch('nova.objects.InstanceList.fill_metadata')
|
||||
def test_multiplier_windows_and_pc(self, mock_fm):
|
||||
self.flags(image_props_weight_multiplier=1.0, group='filter_scheduler')
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
# we request for both windows and pc machine type
|
||||
weighed_host = self._get_weighed_host(
|
||||
hostinfo_list,
|
||||
request_spec=objects.RequestSpec(
|
||||
image=PROP_WIN_PC))
|
||||
self.assertEqual(1.0, weighed_host.weight)
|
||||
# only host2 has instances with both os_distro=windows and
|
||||
# hw_machine_type=pc
|
||||
self.assertEqual('host2', weighed_host.obj.host)
|
||||
mock_fm.assert_has_calls([mock.call(), mock.call(),
|
||||
mock.call(), mock.call()])
|
||||
|
||||
@mock.patch('nova.objects.InstanceList.fill_metadata')
|
||||
def test_multiplier_pc(self, mock_fm):
|
||||
self.flags(image_props_weight_multiplier=1.0, group='filter_scheduler')
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
weights = self.weight_handler.get_weighed_objects(
|
||||
self.weighers, hostinfo_list,
|
||||
weighing_properties=objects.RequestSpec(image=PROP_PC))
|
||||
# host2 and host3 have instances with pc machine type so are equally
|
||||
# weighed so we return the first of both.
|
||||
expected_weights = [{'weight': 1.0, 'host': 'host2'},
|
||||
{'weight': 1.0, 'host': 'host3'},
|
||||
{'weight': 0.0, 'host': 'host1'},
|
||||
{'weight': 0.0, 'host': 'host4'}]
|
||||
self.assertEqual(expected_weights, [weigh.to_dict()
|
||||
for weigh in weights])
|
||||
self.assertEqual('host2', weights[0].obj.host)
|
||||
mock_fm.assert_has_calls([mock.call(), mock.call(),
|
||||
mock.call(), mock.call()])
|
||||
|
||||
@mock.patch('nova.objects.InstanceList.fill_metadata')
|
||||
def test_multiplier_linux(self, mock_fm):
|
||||
self.flags(image_props_weight_multiplier=1.0, group='filter_scheduler')
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
weights = self.weight_handler.get_weighed_objects(
|
||||
self.weighers, hostinfo_list,
|
||||
weighing_properties=objects.RequestSpec(image=PROP_LIN))
|
||||
# host3 and host4 have instances with linux distro but we favor
|
||||
# host3 given he has more instances having the same requested property
|
||||
expected_weights = [{'weight': 1.0, 'host': 'host3'},
|
||||
{'weight': 0.5, 'host': 'host4'},
|
||||
{'weight': 0.0, 'host': 'host1'},
|
||||
{'weight': 0.0, 'host': 'host2'}]
|
||||
self.assertEqual(expected_weights, [weigh.to_dict()
|
||||
for weigh in weights])
|
||||
self.assertEqual('host3', weights[0].obj.host)
|
||||
mock_fm.assert_has_calls([mock.call(), mock.call(),
|
||||
mock.call(), mock.call()])
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
A new `ImagePropertiesWeigher` has been added. It will compare the number
|
||||
of image properties of the image being booted for each of the host with how
|
||||
many existing instances use them. By default this weigher is enabled but
|
||||
with a value of 0.0 for
|
||||
`[filter_scheduler]/image_props_weight_multiplier` which
|
||||
won't modify the existing scheduling behavior.
|
||||
If you want to pack instances having the same image properties on the same
|
||||
hosts, modify `image_props_weight_multiplier` to a positive value. If you
|
||||
want to spread instances with the same properties around all hosts, then
|
||||
please modify `image_props_weight_multiplier` to a negative value.
|
||||
|
||||
Reference in New Issue
Block a user