Add ability to specify minimum value when using capacity based QoS

When using capacity based QoS, it is possible that a user provisions
a volume that is very small but it unusable because of the very small
calculated IOPs.

This patch adds a few options to allow operators to allow for a minimum
value of IOPs, where the greater of the calculated value and minimum
is used as the QoS value.

Change-Id: I0fac45847f991d1f81ddcdd8674f8634f1cc4943
This commit is contained in:
Mohammed Naser 2018-08-02 10:43:48 -04:00
parent b0e9ee1d50
commit 37f2bdcdec
4 changed files with 160 additions and 2 deletions

View File

@ -173,7 +173,7 @@ class VolumeConnectionTestCase(base.BaseVolumeTestCase):
_mock_volume_get,
_mock_volume_admin_metadata_get,
mock_get_target):
"""Make sure initialize_connection returns correct information."""
"""QoS test with no minimum value."""
_fake_admin_meta = [{'key': 'fake-key', 'value': 'fake-value'}]
_fake_volume = {'size': 3,
'volume_type_id': fake.VOLUME_TYPE_ID,
@ -226,6 +226,140 @@ class VolumeConnectionTestCase(base.BaseVolumeTestCase):
self.assertDictEqual(qos_specs_expected,
conn_info['data']['qos_specs'])
@mock.patch.object(cinder.volume.targets.iscsi.ISCSITarget,
'_get_target_chap_auth')
@mock.patch.object(db, 'volume_admin_metadata_get')
@mock.patch.object(db.sqlalchemy.api, 'volume_get')
@mock.patch.object(db, 'volume_update')
def test_initialize_connection_qos_per_gb_with_min_small(
self, _mock_volume_update, _mock_volume_get,
_mock_volume_admin_metadata_get, mock_get_target):
"""QoS test when volume size results in using minimum."""
_fake_admin_meta = [{'key': 'fake-key', 'value': 'fake-value'}]
_fake_volume = {'size': 1,
'volume_type_id': fake.VOLUME_TYPE_ID,
'name': 'fake_name',
'host': 'fake_host',
'id': fake.VOLUME_ID,
'volume_admin_metadata': _fake_admin_meta}
fake_volume_obj = fake_volume.fake_volume_obj(self.context,
**_fake_volume)
_mock_volume_get.return_value = _fake_volume
_mock_volume_update.return_value = _fake_volume
_mock_volume_admin_metadata_get.return_value = {
'fake-key': 'fake-value'}
connector = {'ip': 'IP', 'initiator': 'INITIATOR'}
qos_values = {'consumer': 'front-end',
'specs': {
'write_iops_sec_per_gb_min': 15,
'write_iops_sec_per_gb': 5,
'read_iops_sec_per_gb_min': 23100,
'read_iops_sec_per_gb': 7700,
'total_iops_sec_per_gb_min': 900000,
'total_iops_sec_per_gb': 300000,
'read_bytes_sec_per_gb_min': 30,
'read_bytes_sec_per_gb': 10,
'write_bytes_sec_per_gb_min': 120,
'write_bytes_sec_per_gb': 40,
'total_bytes_sec_per_gb_min': 3145728,
'total_bytes_sec_per_gb': 1048576}
}
with mock.patch.object(cinder.volume.volume_types,
'get_volume_type_qos_specs') as type_qos, \
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
'initialize_connection') as driver_init:
type_qos.return_value = dict(qos_specs=qos_values)
driver_init.return_value = {'data': {}}
mock_get_target.return_value = None
qos_specs_expected = {'write_iops_sec': 15,
'read_iops_sec': 23100,
'total_iops_sec': 900000,
'read_bytes_sec': 30,
'write_bytes_sec': 120,
'total_bytes_sec': 3145728}
# initialize_connection() passes qos_specs that is designated to
# be consumed by front-end or both front-end and back-end
conn_info = self.volume.initialize_connection(
self.context, fake_volume_obj, connector,)
self.assertDictEqual(qos_specs_expected,
conn_info['data']['qos_specs'])
qos_values.update({'consumer': 'both'})
conn_info = self.volume.initialize_connection(
self.context, fake_volume_obj, connector)
self.assertDictEqual(qos_specs_expected,
conn_info['data']['qos_specs'])
@mock.patch.object(cinder.volume.targets.iscsi.ISCSITarget,
'_get_target_chap_auth')
@mock.patch.object(db, 'volume_admin_metadata_get')
@mock.patch.object(db.sqlalchemy.api, 'volume_get')
@mock.patch.object(db, 'volume_update')
def test_initialize_connection_qos_per_gb_with_min_large(
self, _mock_volume_update, _mock_volume_get,
_mock_volume_admin_metadata_get, mock_get_target):
"""QoS test when volume size results in using per-gb values."""
_fake_admin_meta = [{'key': 'fake-key', 'value': 'fake-value'}]
_fake_volume = {'size': 100,
'volume_type_id': fake.VOLUME_TYPE_ID,
'name': 'fake_name',
'host': 'fake_host',
'id': fake.VOLUME_ID,
'volume_admin_metadata': _fake_admin_meta}
fake_volume_obj = fake_volume.fake_volume_obj(self.context,
**_fake_volume)
_mock_volume_get.return_value = _fake_volume
_mock_volume_update.return_value = _fake_volume
_mock_volume_admin_metadata_get.return_value = {
'fake-key': 'fake-value'}
connector = {'ip': 'IP', 'initiator': 'INITIATOR'}
qos_values = {'consumer': 'front-end',
'specs': {
'write_iops_sec_per_gb_min': 15,
'write_iops_sec_per_gb': 5,
'read_iops_sec_per_gb_min': 23100,
'read_iops_sec_per_gb': 7700,
'total_iops_sec_per_gb_min': 900000,
'total_iops_sec_per_gb': 300000,
'read_bytes_sec_per_gb_min': 30,
'read_bytes_sec_per_gb': 10,
'write_bytes_sec_per_gb_min': 120,
'write_bytes_sec_per_gb': 40,
'total_bytes_sec_per_gb_min': 3145728,
'total_bytes_sec_per_gb': 1048576}
}
with mock.patch.object(cinder.volume.volume_types,
'get_volume_type_qos_specs') as type_qos, \
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
'initialize_connection') as driver_init:
type_qos.return_value = dict(qos_specs=qos_values)
driver_init.return_value = {'data': {}}
mock_get_target.return_value = None
qos_specs_expected = {'write_iops_sec': 500,
'read_iops_sec': 770000,
'total_iops_sec': 30000000,
'read_bytes_sec': 1000,
'write_bytes_sec': 4000,
'total_bytes_sec': 104857600}
# initialize_connection() passes qos_specs that is designated to
# be consumed by front-end or both front-end and back-end
conn_info = self.volume.initialize_connection(
self.context, fake_volume_obj, connector,)
self.assertDictEqual(qos_specs_expected,
conn_info['data']['qos_specs'])
qos_values.update({'consumer': 'both'})
conn_info = self.volume.initialize_connection(
self.context, fake_volume_obj, connector)
self.assertDictEqual(qos_specs_expected,
conn_info['data']['qos_specs'])
@mock.patch.object(fake_driver.FakeLoggingVolumeDriver, 'create_export')
def test_initialize_connection_export_failure(self,
_mock_create_export):

View File

@ -1612,8 +1612,11 @@ class VolumeManager(manager.CleanableManager,
for option in tune_opts:
option_per_gb = '%s_per_gb' % option
option_per_gb_min = '%s_per_gb_min' % option
if option_per_gb in specs:
specs[option] = int(specs[option_per_gb]) * volume_size
minimum_value = specs.pop(option_per_gb_min, 0)
value = int(specs[option_per_gb]) * volume_size
specs[option] = max(minimum_value, value)
specs.pop(option_per_gb)
qos_spec = dict(qos_specs=specs)

View File

@ -31,6 +31,17 @@ which values should be fairly self explanatory:
* `write_bytes_sec_per_gb`
* `total_bytes_sec_per_gb`
In addition, there are 6 more options which allow you to control the minimum
possible value. This can be useful in cases where a user creates a volume that
is very small and ends up with an unusable volume because of performance.
* `read_iops_sec_per_gb_min`
* `write_iops_sec_per_gb_min`
* `total_iops_sec_per_gb_min`
* `read_bytes_sec_per_gb_min`
* `write_bytes_sec_per_gb_min`
* `total_bytes_sec_per_gb_min`
For example, in order to create a QoS with 30 IOPs total writes per GB and
a throughput of 1MB per GB, you might use the Cinder client in the following
way:

View File

@ -0,0 +1,10 @@
---
features:
- Cinder now allows for a minimum value when using the capacity based QoS
in order to make sure small volumes can get a minimum allocation for them
to be usable.
The newly added QoS specs are `read_iops_sec_per_gb_min`,
`write_iops_sec_per_gb_min`, `total_iops_sec_per_gb_min`,
`read_bytes_sec_per_gb_min`, `write_bytes_sec_per_gb_min` and
`total_bytes_sec_per_gb_min`