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:
committed by
Sean Mooney
parent
420050cf33
commit
acd6c733c6
@@ -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