Merge "scheduler: add soft-(anti-)affinity weighers"
This commit is contained in:
@@ -390,6 +390,19 @@ The Filter Scheduler weighs hosts based on the config option
|
||||
hosts. If the multiplier is positive, the weigher prefer choosing heavy
|
||||
workload compute hosts, the weighing has the opposite effect of the default.
|
||||
|
||||
* |ServerGroupSoftAffinityWeigher| The weigher can compute the weight based
|
||||
on the number of instances that run on the same server group. The largest
|
||||
weight defines the preferred host for the new instance. For the multiplier
|
||||
only a positive value is meaningful for the calculation as a negative value
|
||||
would mean that the affinity weigher would prefer non collocating placement.
|
||||
|
||||
* |ServerGroupSoftAntiAffinityWeigher| The weigher can compute the weight based
|
||||
on the number of instances that run on the same server group as a negative
|
||||
value. The largest weight defines the preferred host for the new instance.
|
||||
For the multiplier only a positive value is meaningful for the calculation as
|
||||
a negative value would mean that the anti-affinity weigher would prefer
|
||||
collocating placement.
|
||||
|
||||
Filter Scheduler makes a local list of acceptable hosts by repeated filtering and
|
||||
weighing. Each time it chooses a host, it virtually consumes resources on it,
|
||||
so subsequent selections can adjust accordingly. It is useful if the customer
|
||||
@@ -440,3 +453,5 @@ in :mod:`nova.tests.scheduler`.
|
||||
.. |MetricsFilter| replace:: :class:`MetricsFilter <nova.scheduler.filters.metrics_filter.MetricsFilter>`
|
||||
.. |MetricsWeigher| replace:: :class:`MetricsWeigher <nova.scheduler.weights.metrics.MetricsWeigher>`
|
||||
.. |IoOpsWeigher| replace:: :class:`IoOpsWeigher <nova.scheduler.weights.io_ops.IoOpsWeigher>`
|
||||
.. |ServerGroupSoftAffinityWeigher| replace:: :class:`ServerGroupSoftAffinityWeigher <nova.scheduler.weights.affinity.ServerGroupSoftAffinityWeigher>`
|
||||
.. |ServerGroupSoftAntiAffinityWeigher| replace:: :class:`ServerGroupSoftAntiAffinityWeigher <nova.scheduler.weights.affinity.ServerGroupSoftAntiAffinityWeigher>`
|
||||
|
||||
@@ -208,6 +208,23 @@ scheduler_max_att_opt = cfg.IntOpt("scheduler_max_attempts",
|
||||
default=3,
|
||||
help="Maximum number of attempts to schedule an instance")
|
||||
|
||||
soft_affinity_weight_opt = cfg.FloatOpt('soft_affinity_weight_multiplier',
|
||||
default=1.0,
|
||||
help='Multiplier used for weighing hosts '
|
||||
'for group soft-affinity. Only a '
|
||||
'positive value is meaningful. Negative '
|
||||
'means that the behavior will change to '
|
||||
'the opposite, which is soft-anti-affinity.')
|
||||
|
||||
soft_anti_affinity_weight_opt = cfg.FloatOpt(
|
||||
'soft_anti_affinity_weight_multiplier',
|
||||
default=1.0,
|
||||
help='Multiplier used for weighing hosts '
|
||||
'for group soft-anti-affinity. Only a '
|
||||
'positive value is meaningful. Negative '
|
||||
'means that the behavior will change to '
|
||||
'the opposite, which is soft-affinity.')
|
||||
|
||||
|
||||
SIMPLE_OPTS = [host_subset_size_opt,
|
||||
bm_default_filter_opt,
|
||||
@@ -232,6 +249,8 @@ SIMPLE_OPTS = [host_subset_size_opt,
|
||||
ram_weight_mult_opt,
|
||||
io_ops_weight_mult_opt,
|
||||
scheduler_max_att_opt,
|
||||
soft_affinity_weight_opt,
|
||||
soft_anti_affinity_weight_opt,
|
||||
]
|
||||
|
||||
ALL_OPTS = itertools.chain(
|
||||
|
||||
90
nova/scheduler/weights/affinity.py
Normal file
90
nova/scheduler/weights/affinity.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) 2015 Ericsson AB
|
||||
# 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.
|
||||
"""
|
||||
Affinity Weighers. Weigh hosts by the number of instances from a given host.
|
||||
|
||||
AffinityWeigher implements the soft-affinity policy for server groups by
|
||||
preferring the hosts that has more instances from the given group.
|
||||
|
||||
AntiAffinityWeigher implements the soft-anti-affinity policy for server groups
|
||||
by preferring the hosts that has less instances from the given group.
|
||||
|
||||
"""
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from nova.i18n import _LW
|
||||
from nova.scheduler import weights
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _SoftAffinityWeigherBase(weights.BaseHostWeigher):
|
||||
policy_name = None
|
||||
|
||||
def _weigh_object(self, host_state, request_spec):
|
||||
"""Higher weights win."""
|
||||
if not request_spec.instance_group:
|
||||
return 0
|
||||
|
||||
policies = request_spec.instance_group.policies
|
||||
|
||||
if self.policy_name not in policies:
|
||||
return 0
|
||||
|
||||
instances = set(host_state.instances.keys())
|
||||
members = set(request_spec.instance_group.members)
|
||||
member_on_host = instances.intersection(members)
|
||||
|
||||
return len(member_on_host)
|
||||
|
||||
|
||||
class ServerGroupSoftAffinityWeigher(_SoftAffinityWeigherBase):
|
||||
policy_name = 'soft-affinity'
|
||||
warning_sent = False
|
||||
|
||||
def weight_multiplier(self):
|
||||
if (CONF.soft_affinity_weight_multiplier < 0 and
|
||||
not self.warning_sent):
|
||||
LOG.warn(_LW('For the soft_affinity_weight_multiplier only a '
|
||||
'positive value is meaningful as a negative value '
|
||||
'would mean that the affinity weigher would '
|
||||
'prefer non-collocating placement.'))
|
||||
self.warning_sent = True
|
||||
|
||||
return CONF.soft_affinity_weight_multiplier
|
||||
|
||||
|
||||
class ServerGroupSoftAntiAffinityWeigher(_SoftAffinityWeigherBase):
|
||||
policy_name = 'soft-anti-affinity'
|
||||
warning_sent = False
|
||||
|
||||
def weight_multiplier(self):
|
||||
if (CONF.soft_anti_affinity_weight_multiplier < 0 and
|
||||
not self.warning_sent):
|
||||
LOG.warn(_LW('For the soft_anti_affinity_weight_multiplier only a '
|
||||
'positive value is meaningful as a negative value '
|
||||
'would mean that the anti-affinity weigher would '
|
||||
'prefer collocating placement.'))
|
||||
self.warning_sent = True
|
||||
|
||||
return CONF.soft_anti_affinity_weight_multiplier
|
||||
|
||||
def _weigh_object(self, host_state, request_spec):
|
||||
weight = super(ServerGroupSoftAntiAffinityWeigher, self)._weigh_object(
|
||||
host_state, request_spec)
|
||||
return -1 * weight
|
||||
153
nova/tests/unit/scheduler/weights/test_weights_affinity.py
Normal file
153
nova/tests/unit/scheduler/weights/test_weights_affinity.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2015 Ericsson AB
|
||||
# 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.
|
||||
|
||||
import mock
|
||||
|
||||
from nova import objects
|
||||
from nova.scheduler import weights
|
||||
from nova.scheduler.weights import affinity
|
||||
from nova import test
|
||||
from nova.tests.unit.scheduler import fakes
|
||||
|
||||
|
||||
class SoftWeigherTestBase(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftWeigherTestBase, self).setUp()
|
||||
self.weight_handler = weights.HostWeightHandler()
|
||||
self.weighers = []
|
||||
|
||||
def _get_weighed_host(self, hosts, policy):
|
||||
request_spec = objects.RequestSpec(
|
||||
instance_group=objects.InstanceGroup(
|
||||
policies=[policy],
|
||||
members=['member1',
|
||||
'member2',
|
||||
'member3',
|
||||
'member4',
|
||||
'member5',
|
||||
'member6',
|
||||
'member7']))
|
||||
return self.weight_handler.get_weighed_objects(self.weighers,
|
||||
hosts,
|
||||
request_spec)[0]
|
||||
|
||||
def _get_all_hosts(self):
|
||||
host_values = [
|
||||
('host1', 'node1', {'instances': {
|
||||
'member1': mock.sentinel,
|
||||
'instance13': mock.sentinel
|
||||
}}),
|
||||
('host2', 'node2', {'instances': {
|
||||
'member2': mock.sentinel,
|
||||
'member3': mock.sentinel,
|
||||
'member4': mock.sentinel,
|
||||
'member5': mock.sentinel,
|
||||
'instance14': mock.sentinel
|
||||
}}),
|
||||
('host3', 'node3', {'instances': {
|
||||
'instance15': mock.sentinel
|
||||
}}),
|
||||
('host4', 'node4', {'instances': {
|
||||
'member6': mock.sentinel,
|
||||
'member7': mock.sentinel,
|
||||
'instance16': mock.sentinel
|
||||
}})]
|
||||
return [fakes.FakeHostState(host, node, values)
|
||||
for host, node, values in host_values]
|
||||
|
||||
def _do_test(self, policy, expected_weight,
|
||||
expected_host):
|
||||
hostinfo_list = self._get_all_hosts()
|
||||
weighed_host = self._get_weighed_host(hostinfo_list,
|
||||
policy)
|
||||
self.assertEqual(expected_weight, weighed_host.weight)
|
||||
if expected_host:
|
||||
self.assertEqual(expected_host, weighed_host.obj.host)
|
||||
|
||||
|
||||
class SoftAffinityWeigherTestCase(SoftWeigherTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftAffinityWeigherTestCase, self).setUp()
|
||||
self.weighers = [affinity.ServerGroupSoftAffinityWeigher()]
|
||||
|
||||
def test_soft_affinity_weight_multiplier_by_default(self):
|
||||
self._do_test(policy='soft-affinity',
|
||||
expected_weight=1.0,
|
||||
expected_host='host2')
|
||||
|
||||
def test_soft_affinity_weight_multiplier_zero_value(self):
|
||||
# We do not know the host, all have same weight.
|
||||
self.flags(soft_affinity_weight_multiplier=0.0)
|
||||
self._do_test(policy='soft-affinity',
|
||||
expected_weight=0.0,
|
||||
expected_host=None)
|
||||
|
||||
def test_soft_affinity_weight_multiplier_positive_value(self):
|
||||
self.flags(soft_affinity_weight_multiplier=2.0)
|
||||
self._do_test(policy='soft-affinity',
|
||||
expected_weight=2.0,
|
||||
expected_host='host2')
|
||||
|
||||
@mock.patch.object(affinity, 'LOG')
|
||||
def test_soft_affinity_weight_multiplier_negative_value(self, mock_log):
|
||||
self.flags(soft_affinity_weight_multiplier=-1.0)
|
||||
self._do_test(policy='soft-affinity',
|
||||
expected_weight=0.0,
|
||||
expected_host='host3')
|
||||
# call twice and assert that only one warning is emitted
|
||||
self._do_test(policy='soft-affinity',
|
||||
expected_weight=0.0,
|
||||
expected_host='host3')
|
||||
self.assertEqual(1, mock_log.warn.call_count)
|
||||
|
||||
|
||||
class SoftAntiAffinityWeigherTestCase(SoftWeigherTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftAntiAffinityWeigherTestCase, self).setUp()
|
||||
self.weighers = [affinity.ServerGroupSoftAntiAffinityWeigher()]
|
||||
|
||||
def test_soft_anti_affinity_weight_multiplier_by_default(self):
|
||||
self._do_test(policy='soft-anti-affinity',
|
||||
expected_weight=1.0,
|
||||
expected_host='host3')
|
||||
|
||||
def test_soft_anti_affinity_weight_multiplier_zero_value(self):
|
||||
# We do not know the host, all have same weight.
|
||||
self.flags(soft_anti_affinity_weight_multiplier=0.0)
|
||||
self._do_test(policy='soft-anti-affinity',
|
||||
expected_weight=0.0,
|
||||
expected_host=None)
|
||||
|
||||
def test_soft_anti_affinity_weight_multiplier_positive_value(self):
|
||||
self.flags(soft_anti_affinity_weight_multiplier=2.0)
|
||||
self._do_test(policy='soft-anti-affinity',
|
||||
expected_weight=2.0,
|
||||
expected_host='host3')
|
||||
|
||||
@mock.patch.object(affinity, 'LOG')
|
||||
def test_soft_anti_affinity_weight_multiplier_negative_value(self,
|
||||
mock_log):
|
||||
self.flags(soft_anti_affinity_weight_multiplier=-1.0)
|
||||
self._do_test(policy='soft-anti-affinity',
|
||||
expected_weight=0.0,
|
||||
expected_host='host2')
|
||||
# call twice and assert that only one warning is emitted
|
||||
self._do_test(policy='soft-anti-affinity',
|
||||
expected_weight=0.0,
|
||||
expected_host='host2')
|
||||
self.assertEqual(1, mock_log.warn.call_count)
|
||||
@@ -17,6 +17,7 @@ Tests For Scheduler weights.
|
||||
"""
|
||||
|
||||
from nova.scheduler import weights
|
||||
from nova.scheduler.weights import affinity
|
||||
from nova.scheduler.weights import io_ops
|
||||
from nova.scheduler.weights import metrics
|
||||
from nova.scheduler.weights import ram
|
||||
@@ -38,3 +39,5 @@ class TestWeighedHost(test.NoDBTestCase):
|
||||
self.assertIn(ram.RAMWeigher, classes)
|
||||
self.assertIn(metrics.MetricsWeigher, classes)
|
||||
self.assertIn(io_ops.IoOpsWeigher, classes)
|
||||
self.assertIn(affinity.ServerGroupSoftAffinityWeigher, classes)
|
||||
self.assertIn(affinity.ServerGroupSoftAntiAffinityWeigher, classes)
|
||||
|
||||
Reference in New Issue
Block a user