Add stochastic weight handler to scheduler

Add a config option to the scheduler host manager to select host
weight managers other than the default. Rename the existing default
weight manager TopHostWeightHandler because its behavior is to
always choose the top weight. Add a new weight manager which
randomly chooses the host using weights as probabilities.

DocImpact

Implements blueprint stochastic-weighing-scheduler

Change-Id: Ie5985066e68aef25600f8b76948adacd4c04263a
This commit is contained in:
Ben Swartzlander 2016-07-22 14:04:59 -04:00
parent b7860e3afc
commit de66e8f811
8 changed files with 176 additions and 9 deletions

View File

@ -21,6 +21,7 @@ import collections
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
from oslo_utils import timeutils
from cinder import context as cinder_context
@ -29,7 +30,6 @@ from cinder import objects
from cinder import utils
from cinder.i18n import _LI, _LW
from cinder.scheduler import filters
from cinder.scheduler import weights
from cinder.volume import utils as vol_utils
@ -46,7 +46,11 @@ host_manager_opts = [
default=[
'CapacityWeigher'
],
help='Which weigher class names to use for weighing hosts.')
help='Which weigher class names to use for weighing hosts.'),
cfg.StrOpt('scheduler_weight_handler',
default='cinder.scheduler.weights.OrderedHostWeightHandler',
help='Which handler to use for selecting the host/pool '
'after weighing'),
]
CONF = cfg.CONF
@ -347,8 +351,9 @@ class HostManager(object):
self.filter_handler = filters.HostFilterHandler('cinder.scheduler.'
'filters')
self.filter_classes = self.filter_handler.get_all_classes()
self.weight_handler = weights.HostWeightHandler('cinder.scheduler.'
'weights')
self.weight_handler = importutils.import_object(
CONF.scheduler_weight_handler,
'cinder.scheduler.weights')
self.weight_classes = self.weight_handler.get_all_classes()
self._no_capabilities_hosts = set() # Hosts having no capabilities

View File

@ -37,8 +37,9 @@ class BaseHostWeigher(base_weight.BaseWeigher):
pass
class HostWeightHandler(base_weight.BaseWeightHandler):
class OrderedHostWeightHandler(base_weight.BaseWeightHandler):
object_class = WeighedHost
def __init__(self, namespace):
super(HostWeightHandler, self).__init__(BaseHostWeigher, namespace)
super(OrderedHostWeightHandler, self).__init__(BaseHostWeigher,
namespace)

View File

@ -0,0 +1,83 @@
#
# 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.
"""
Stochastic weight handler
This weight handler differs from the default weight
handler by giving every pool a chance to be chosed
wheret the probability is proportional to each pools'
weight.
"""
import random
from cinder.scheduler.base_weight import BaseWeightHandler
from cinder.scheduler.weights import BaseHostWeigher
from cinder.scheduler.weights import WeighedHost
class StochasticHostWeightHandler(BaseWeightHandler):
def __init__(self, namespace):
super(StochasticHostWeightHandler, self).__init__(BaseHostWeigher,
namespace)
def get_weighed_objects(self, weigher_classes, obj_list,
weighing_properties):
# The normalization performed in the superclass is nonlinear, which
# messes up the probabilities, so override it. The probabilistic
# approach we use here is self-normalizing.
# Also, the sorting done by the parent implementation is harmless but
# useless for us.
# Compute the object weights as the parent would but without sorting
# or normalization.
weighed_objs = [WeighedHost(obj, 0.0) for obj in obj_list]
for weigher_cls in weigher_classes:
weigher = weigher_cls()
weights = weigher.weigh_objects(weighed_objs, weighing_properties)
for i, weight in enumerate(weights):
obj = weighed_objs[i]
obj.weight += weigher.weight_multiplier() * weight
# Avoid processing empty lists
if not weighed_objs:
return []
# First compute the total weight of all the objects and the upper
# bound for each object to "win" the lottery.
total_weight = 0
table = []
for weighed_obj in weighed_objs:
total_weight += weighed_obj.weight
max_value = total_weight
table.append((max_value, weighed_obj))
# Now draw a random value with the computed range
winning_value = random.random() * total_weight
# Scan the table to find the first object with a maximum higher than
# the random number. Save the index of the winner.
winning_index = 0
for (i, (max_value, weighed_obj)) in enumerate(table):
if max_value > winning_value:
# Return a single element array with the winner.
winning_index = i
break
# It's theoretically possible for the above loop to terminate with no
# winner. This happens when winning_value >= total_weight, which
# could only occur with very large numbers and floating point
# rounding. In those cases the actual winner should have been the
# last element, so return it.
return weighed_objs[winning_index:] + weighed_objs[0:winning_index]

View File

@ -33,7 +33,7 @@ class AllocatedCapacityWeigherTestCase(test.TestCase):
def setUp(self):
super(AllocatedCapacityWeigherTestCase, self).setUp()
self.host_manager = fakes.FakeHostManager()
self.weight_handler = weights.HostWeightHandler(
self.weight_handler = weights.OrderedHostWeightHandler(
'cinder.scheduler.weights')
def _get_weighed_host(self, hosts, weight_properties=None):

View File

@ -32,7 +32,7 @@ class CapacityWeigherTestCase(test.TestCase):
def setUp(self):
super(CapacityWeigherTestCase, self).setUp()
self.host_manager = fakes.FakeHostManager()
self.weight_handler = weights.HostWeightHandler(
self.weight_handler = weights.OrderedHostWeightHandler(
'cinder.scheduler.weights')
def _get_weighed_hosts(self, hosts, weight_properties=None):

View File

@ -0,0 +1,67 @@
#
# 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 stochastic weight handler
"""
import ddt
import mock
import random
from cinder.scheduler import base_weight
from cinder.scheduler.weights.stochastic import StochasticHostWeightHandler
from cinder import test
@ddt.ddt
class StochasticWeightHandlerTestCase(test.TestCase):
"""Test case for StochasticHostWeightHandler."""
def setUp(self):
super(StochasticWeightHandlerTestCase, self).setUp()
@ddt.data(
(0.0, 'A'),
(0.1, 'A'),
(0.2, 'B'),
(0.3, 'B'),
(0.4, 'B'),
(0.5, 'B'),
(0.6, 'B'),
(0.7, 'C'),
(0.8, 'C'),
(0.9, 'C'),
)
@ddt.unpack
def test_get_weighed_objects_correct(self, rand_value, expected_obj):
self.mock_object(random,
'random',
mock.Mock(return_value=rand_value))
class MapWeigher(base_weight.BaseWeigher):
minval = 0
maxval = 100
def _weigh_object(self, obj, weight_map):
return weight_map[obj]
weight_map = {'A': 1, 'B': 3, 'C': 2}
objs = sorted(weight_map.keys())
weigher_classes = [MapWeigher]
handler = StochasticHostWeightHandler('fake_namespace')
weighted_objs = handler.get_weighed_objects(weigher_classes,
objs,
weight_map)
winner = weighted_objs[0].obj
self.assertEqual(expected_obj, winner)

View File

@ -58,7 +58,7 @@ class VolumeNumberWeigherTestCase(test.TestCase):
read_deleted="no",
overwrite=False)
self.host_manager = fakes.FakeHostManager()
self.weight_handler = weights.HostWeightHandler(
self.weight_handler = weights.OrderedHostWeightHandler(
'cinder.scheduler.weights')
def _get_weighed_host(self, hosts, weight_properties=None):

View File

@ -0,0 +1,11 @@
---
features:
- Added a new config option `scheduler_weight_handler`. This is a global
option which specifies how the scheduler should choose from a listed of
weighted pools. By default the existing weigher is used which always
chooses the highest weight.
- Added a new weight handler `StochasticHostWeightHandler`. This weight
handler chooses pools randomly, where the random probabilities are
proportional to the weights, so higher weighted pools are chosen more
frequently, but not all the time. This weight handler spreads new
shares across available pools more fairly.