Add a new ImagePropertiesWeigher

This weigher will check how many instances in the host have the image
properties that are requested and will prefer by default to pack
instances with the same properties.

Implements blueprint: image-metadata-props-weigher

Change-Id: I3bfed44bd089c6b226d13c3ac4a0003411737cbd
This commit is contained in:
Sylvain Bauza
2025-01-27 11:35:31 +01:00
committed by Sean Mooney
parent 420050cf33
commit acd6c733c6
7 changed files with 321 additions and 0 deletions
+13
View File
@@ -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
----------------------------
+44
View File
@@ -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",
+4
View File
@@ -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
+86
View File
@@ -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.