From d7dfdb18db483f9dd015c5f7c235e0d82b165b44 Mon Sep 17 00:00:00 2001 From: Xinyuan Huang Date: Tue, 9 Jun 2015 23:39:44 +0800 Subject: [PATCH] Affiniy/anti-affinity costs for soft affinity placement in solver scheduler This enables solver scheduler to do soft affinity placement by adding two cost classes: affinity cost and anti-affinity cost. User can specify scheduler hint soft_same_host/soft_different_host=. The scheduler will tend to put instances in same/different hosts as those existing instances specified by user. If no instance uuid is provided, then scheduler should tend to schedule all requested instances in same/different hosts. The affinity and anti-affinity costs are not treated as hard restrictions which means if there is no host that can meet the same/different host requirements then other hosts may be chosen. Change-Id: Ie12af12d6e11c1dd815a2b1a81ef5376a02be057 --- .../scheduler/solvers/costs/affinity_cost.py | 133 ++++++ .../solvers/costs/test_affinity_cost.py | 400 ++++++++++++++++++ 2 files changed, 533 insertions(+) create mode 100644 nova_solverscheduler/scheduler/solvers/costs/affinity_cost.py create mode 100644 nova_solverscheduler/tests/scheduler/solvers/costs/test_affinity_cost.py diff --git a/nova_solverscheduler/scheduler/solvers/costs/affinity_cost.py b/nova_solverscheduler/scheduler/solvers/costs/affinity_cost.py new file mode 100644 index 0000000..694988b --- /dev/null +++ b/nova_solverscheduler/scheduler/solvers/costs/affinity_cost.py @@ -0,0 +1,133 @@ +# Copyright (c) 2015 Cisco Systems, Inc. +# 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 six + +from oslo_config import cfg +from oslo_log import log as logging + +from nova.i18n import _ +from nova_solverscheduler.scheduler.solvers import costs as solver_costs +from nova_solverscheduler.scheduler.solvers import utils as solver_utils + +affinity_cost_opts = [ + cfg.FloatOpt('affinity_cost_multiplier', + default=1.0, + help='Multiplier used for affinity cost. Must be a ' + 'positive number.'), + cfg.FloatOpt('anti_affinity_cost_multiplier', + default=1.0, + help='Multiplier used for anti-affinity cost. Must be ' + 'a positive number.'), +] + +CONF = cfg.CONF +CONF.register_opts(affinity_cost_opts, group='solver_scheduler') + +LOG = logging.getLogger(__name__) + + +class AffinityCost(solver_costs.BaseLinearCost): + + def __init__(self): + super(AffinityCost, self).__init__() + + def cost_multiplier(self): + return CONF.solver_scheduler.affinity_cost_multiplier + + def get_extended_cost_matrix(self, hosts, filter_properties): + num_hosts = len(hosts) + num_instances = filter_properties.get('num_instances') + + multiplier = self.cost_multiplier() + if multiplier == 0: + extended_cost_matrix = [[0 for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + return extended_cost_matrix + + scheduler_hints = filter_properties.get('scheduler_hints') or {} + affinity_uuids = scheduler_hints.get('soft_same_host', []) + + if affinity_uuids == '': + extended_cost_matrix = [[float(-j) / multiplier + for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + LOG.debug(_('No instance specified for AffinityCost.')) + return extended_cost_matrix + + if isinstance(affinity_uuids, six.string_types): + affinity_uuids = [affinity_uuids] + + if affinity_uuids: + extended_cost_matrix = [[1 - (float(j) / multiplier) + for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + for i in xrange(num_hosts): + if solver_utils.instance_uuids_overlap(hosts[i], + affinity_uuids): + extended_cost_matrix[i] = [float(-j) / multiplier for j in + xrange(num_instances + 1)] + else: + extended_cost_matrix = [[0 for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + + return extended_cost_matrix + + +class AntiAffinityCost(solver_costs.BaseLinearCost): + + def __init__(self): + super(AntiAffinityCost, self).__init__() + + def cost_multiplier(self): + return CONF.solver_scheduler.anti_affinity_cost_multiplier + + def get_extended_cost_matrix(self, hosts, filter_properties): + num_hosts = len(hosts) + num_instances = filter_properties.get('num_instances') + + multiplier = self.cost_multiplier() + if multiplier == 0: + extended_cost_matrix = [[0 for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + return extended_cost_matrix + + scheduler_hints = filter_properties.get('scheduler_hints') or {} + affinity_uuids = scheduler_hints.get('soft_different_host', []) + + if affinity_uuids == '': + extended_cost_matrix = [[float(j) / multiplier + for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + LOG.debug(_('No instance specified for AntiAffinityCost.')) + return extended_cost_matrix + + if isinstance(affinity_uuids, six.string_types): + affinity_uuids = [affinity_uuids] + + if affinity_uuids: + extended_cost_matrix = [[float(j) / multiplier + for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + for i in xrange(num_hosts): + if solver_utils.instance_uuids_overlap(hosts[i], + affinity_uuids): + extended_cost_matrix[i] = [1 + (float(j) / multiplier) + for j in xrange(num_instances + 1)] + else: + extended_cost_matrix = [[0 for j in xrange(num_instances + 1)] + for i in xrange(num_hosts)] + + return extended_cost_matrix diff --git a/nova_solverscheduler/tests/scheduler/solvers/costs/test_affinity_cost.py b/nova_solverscheduler/tests/scheduler/solvers/costs/test_affinity_cost.py new file mode 100644 index 0000000..30725e8 --- /dev/null +++ b/nova_solverscheduler/tests/scheduler/solvers/costs/test_affinity_cost.py @@ -0,0 +1,400 @@ +# Copyright (c) 2014 Cisco Systems, Inc. +# 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. + +"""Test case for solver scheduler affinity-cost.""" + +from nova import context +from nova import objects +from nova import test +from nova_solverscheduler.scheduler.solvers import costs +from nova_solverscheduler.scheduler.solvers.costs import affinity_cost +from nova_solverscheduler.tests.scheduler import solver_scheduler_fakes \ + as fakes + + +class TestAffinityCost(test.NoDBTestCase): + USES_DB = True + + def setUp(self): + super(TestAffinityCost, self).setUp() + self.context = context.RequestContext('fake_usr', 'fake_proj') + self.cost_handler = costs.CostHandler() + self.cost_classes = self.cost_handler.get_matching_classes( + ['nova_solverscheduler.scheduler.solvers.costs.' + 'affinity_cost.AffinityCost']) + + def _get_fake_hosts(self): + host1 = fakes.FakeSolverSchedulerHostState('host1', 'node1', {}) + host2 = fakes.FakeSolverSchedulerHostState('host2', 'node2', {}) + host3 = fakes.FakeSolverSchedulerHostState('host3', 'node3', {}) + host4 = fakes.FakeSolverSchedulerHostState('host4', 'node4', {}) + return [host1, host2, host3, host4] + + def test_affinity_cost_multiplier(self): + self.flags(affinity_cost_multiplier=0.5, group='solver_scheduler') + self.assertEqual(0.5, affinity_cost.AffinityCost().cost_multiplier()) + + def test_get_extended_cost_matrix_one_inst(self): + fake_hosts = self._get_fake_hosts() + + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_same_host': instance_uuid} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [1, 0, -1, -2], + [0, -1, -2, -3], + [1, 0, -1, -2], + [1, 0, -1, -2]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_multi_inst(self): + fake_hosts = self._get_fake_hosts() + + instance1 = objects.Instance(uuid='inst1') + instance2 = objects.Instance(uuid='inst2') + instance1_uuid = instance1.uuid + instance2_uuid = instance2.uuid + # let these instances be in host1 and host3 + fake_hosts[0].instances = {instance1_uuid: instance1} + fake_hosts[2].instances = {instance2_uuid: instance2} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_same_host': + [instance1_uuid, instance2_uuid]} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, -1, -2, -3], + [1, 0, -1, -2], + [0, -1, -2, -3], + [1, 0, -1, -2]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_change_multiplier(self): + self.flags(affinity_cost_multiplier=0.5, group='solver_scheduler') + + fake_hosts = self._get_fake_hosts() + + instance1 = objects.Instance(uuid='inst1') + instance2 = objects.Instance(uuid='inst2') + instance1_uuid = instance1.uuid + instance2_uuid = instance2.uuid + # let these instances be in host1 and host3 + fake_hosts[0].instances = {instance1_uuid: instance1} + fake_hosts[2].instances = {instance2_uuid: instance2} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_same_host': + [instance1_uuid, instance2_uuid]} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, -2, -4, -6], + [1, -1, -3, -5], + [0, -2, -4, -6], + [1, -1, -3, -5]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_zero_multiplier(self): + self.flags(affinity_cost_multiplier=0, group='solver_scheduler') + + fake_hosts = self._get_fake_hosts() + + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_same_host': instance_uuid} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_no_instance_list(self): + fake_hosts = self._get_fake_hosts() + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_same_host': ''} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [-0, -1, -2, -3], + [-0, -1, -2, -3], + [-0, -1, -2, -3], + [-0, -1, -2, -3]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_no_hint(self): + fake_hosts = self._get_fake_hosts() + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + +class TestAntiAffinityCost(test.NoDBTestCase): + USES_DB = True + + def setUp(self): + super(TestAntiAffinityCost, self).setUp() + self.context = context.RequestContext('fake_usr', 'fake_proj') + self.cost_handler = costs.CostHandler() + self.cost_classes = self.cost_handler.get_matching_classes( + ['nova_solverscheduler.scheduler.solvers.costs.' + 'affinity_cost.AntiAffinityCost']) + + def _get_fake_hosts(self): + host1 = fakes.FakeSolverSchedulerHostState('host1', 'node1', {}) + host2 = fakes.FakeSolverSchedulerHostState('host2', 'node2', {}) + host3 = fakes.FakeSolverSchedulerHostState('host3', 'node3', {}) + host4 = fakes.FakeSolverSchedulerHostState('host4', 'node4', {}) + return [host1, host2, host3, host4] + + def test_anti_affinity_cost_multiplier(self): + self.flags(anti_affinity_cost_multiplier=2, group='solver_scheduler') + self.assertEqual(2, + affinity_cost.AntiAffinityCost().cost_multiplier()) + + def test_get_extended_cost_matrix_one_inst(self): + fake_hosts = self._get_fake_hosts() + + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_different_host': instance_uuid} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, 1, 2, 3], + [1, 2, 3, 4], + [0, 1, 2, 3], + [0, 1, 2, 3]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_multi_inst(self): + fake_hosts = self._get_fake_hosts() + + instance1 = objects.Instance(uuid='inst1') + instance2 = objects.Instance(uuid='inst2') + instance1_uuid = instance1.uuid + instance2_uuid = instance2.uuid + # let these instances be in host1 and host3 + fake_hosts[0].instances = {instance1_uuid: instance1} + fake_hosts[2].instances = {instance2_uuid: instance2} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_different_host': + [instance1_uuid, instance2_uuid]} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [1, 2, 3, 4], + [0, 1, 2, 3], + [1, 2, 3, 4], + [0, 1, 2, 3]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_change_multiplier(self): + self.flags(anti_affinity_cost_multiplier=0.5, + group='solver_scheduler') + + fake_hosts = self._get_fake_hosts() + + instance1 = objects.Instance(uuid='inst1') + instance2 = objects.Instance(uuid='inst2') + instance1_uuid = instance1.uuid + instance2_uuid = instance2.uuid + # let these instances be in host1 and host3 + fake_hosts[0].instances = {instance1_uuid: instance1} + fake_hosts[2].instances = {instance2_uuid: instance2} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_different_host': + [instance1_uuid, instance2_uuid]} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [1, 3, 5, 7], + [0, 2, 4, 6], + [1, 3, 5, 7], + [0, 2, 4, 6]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_zero_multiplier(self): + self.flags(anti_affinity_cost_multiplier=0, group='solver_scheduler') + + fake_hosts = self._get_fake_hosts() + + instance = objects.Instance(uuid='inst1') + instance_uuid = instance.uuid + # let this instance be in host2 + fake_hosts[1].instances = {instance_uuid: instance} + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_different_host': instance_uuid} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_no_instance_list(self): + fake_hosts = self._get_fake_hosts() + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {'soft_different_host': ''} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat) + + def test_get_extended_cost_matrix_no_hint(self): + fake_hosts = self._get_fake_hosts() + + fake_filter_properties = { + 'context': self.context.elevated(), + 'num_instances': 3, + 'instance_uuids': ['fake_uuid_%s' % x for x in range(3)], + 'scheduler_hints': {} + } + + fake_cost = self.cost_classes[0]() + + expected_x_cost_mat = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]] + + x_cost_mat = fake_cost.get_extended_cost_matrix(fake_hosts, + fake_filter_properties) + self.assertEqual(expected_x_cost_mat, x_cost_mat)