diff --git a/nova/compute/manager.py b/nova/compute/manager.py index beec08f96b3..7b860b6df1f 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1192,6 +1192,15 @@ def init_host(self): nova.conf.neutron.register_dynamic_opts(CONF) + # one-time initialization + if CONF.compute.max_concurrent_disk_ops != 0: + compute_utils.disk_ops_semaphore = \ + eventlet.semaphore.BoundedSemaphore( + CONF.compute.max_concurrent_disk_ops) + else: + compute_utils.disk_ops_semaphore = \ + compute_utils.UnlimitedSemaphore() + self.driver.init_host(host=self.host) context = nova.context.get_admin_context() instances = objects.InstanceList.get_by_host( diff --git a/nova/compute/utils.py b/nova/compute/utils.py index 813cc2e609c..24bdd34afb8 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -54,6 +54,11 @@ CONF = nova.conf.CONF LOG = log.getLogger(__name__) +# This semaphore is used to enforce a limit on disk-IO-intensive operations +# (image downloads, image conversions) at any given time. +# It is initialized at ComputeManager.init_host() +disk_ops_semaphore = None + def exception_to_dict(fault, message=None): """Converts exceptions to a dict for use in notifications.""" diff --git a/nova/conf/compute.py b/nova/conf/compute.py index 9a404a6f21b..23b124fc03e 100644 --- a/nova/conf/compute.py +++ b/nova/conf/compute.py @@ -727,6 +727,15 @@ failing if ``vif_plugging_is_fatal`` is True, or simply continuing with the live migration """), + cfg.IntOpt('max_concurrent_disk_ops', + default=0, + min=0, + help=""" +Number of concurrent disk-IO-intensive operations (glance image downloads, +image format conversions, etc.) that we will do in parallel. If this is set +too high then response time suffers. +The default value of 0 means no limit. + """), ] interval_opts = [ diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 9e543465664..efdbe2211af 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -62,6 +62,7 @@ from nova.compute import power_state from nova.compute import provider_tree from nova.compute import task_states +from nova.compute import utils as compute_utils from nova.compute import vm_states import nova.conf from nova import context @@ -18575,13 +18576,16 @@ def fake_get_info(self, instance): self.drvr._wait_for_running({'name': 'else', 'uuid': 'other_uuid'}) + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.utils.execute') @mock.patch('os.rename') - def test_disk_raw_to_qcow2(self, mock_rename, mock_execute): + def test_disk_raw_to_qcow2(self, mock_rename, mock_execute, + mock_disk_op_sema): path = '/test/disk' _path_qcow = path + '_qcow' self.drvr._disk_raw_to_qcow2(path) + mock_disk_op_sema.__enter__.assert_called_once() mock_execute.assert_has_calls([ mock.call('qemu-img', 'convert', '-f', 'raw', '-O', 'qcow2', path, _path_qcow)]) @@ -21499,6 +21503,7 @@ def _setup_block_rebase_domain_and_guest_mocks(self, dom_xml): return mock_domain, guest + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch.object(host.Host, "has_min_version", mock.Mock(return_value=True)) @mock.patch("nova.virt.libvirt.guest.Guest.is_active", @@ -21507,7 +21512,8 @@ def _setup_block_rebase_domain_and_guest_mocks(self, dom_xml): return_value=mock.Mock(file_format="fake_fmt")) @mock.patch('nova.utils.execute') def test_volume_snapshot_delete_when_dom_not_running(self, mock_execute, - mock_qemu_img_info): + mock_qemu_img_info, + mock_disk_op_sema): """Deleting newest snapshot of a file-based image when the domain is not running should trigger a blockRebase using qemu-img not libvirt. In this test, we rebase the image with another image as backing file. @@ -21523,11 +21529,13 @@ def test_volume_snapshot_delete_when_dom_not_running(self, mock_execute, self.volume_uuid, snapshot_id, self.delete_info_1) + mock_disk_op_sema.__enter__.assert_called_once() mock_qemu_img_info.assert_called_once_with("snap.img") mock_execute.assert_called_once_with('qemu-img', 'rebase', '-b', 'snap.img', '-F', 'fake_fmt', 'disk1_file') + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch.object(host.Host, "has_min_version", mock.Mock(return_value=True)) @mock.patch("nova.virt.libvirt.guest.Guest.is_active", @@ -21536,7 +21544,7 @@ def test_volume_snapshot_delete_when_dom_not_running(self, mock_execute, return_value=mock.Mock(file_format="fake_fmt")) @mock.patch('nova.utils.execute') def test_volume_snapshot_delete_when_dom_not_running_and_no_rebase_base( - self, mock_execute, mock_qemu_img_info): + self, mock_execute, mock_qemu_img_info, mock_disk_op_sema): """Deleting newest snapshot of a file-based image when the domain is not running should trigger a blockRebase using qemu-img not libvirt. In this test, the image is rebased onto no backing file (i.e. @@ -21553,6 +21561,7 @@ def test_volume_snapshot_delete_when_dom_not_running_and_no_rebase_base( self.volume_uuid, snapshot_id, self.delete_info_3) + mock_disk_op_sema.__enter__.assert_called_once() self.assertEqual(0, mock_qemu_img_info.call_count) mock_execute.assert_called_once_with('qemu-img', 'rebase', '-b', '', 'disk1_file') @@ -21944,10 +21953,13 @@ def _create_image(self, extra_properties=None): recv_meta = self.image_service.create(self.context, sent_meta) return recv_meta + @mock.patch.object(compute_utils, 'disk_ops_semaphore', + new_callable=compute_utils.UnlimitedSemaphore) @mock.patch.object(host.Host, 'has_min_version') @mock.patch.object(imagebackend.Image, 'resolve_driver_format') @mock.patch.object(host.Host, '_get_domain') - def _snapshot(self, image_id, mock_get_domain, mock_resolve, mock_version): + def _snapshot(self, image_id, mock_get_domain, mock_resolve, mock_version, + mock_disk_op_sema): mock_get_domain.return_value = FakeVirtDomain() driver = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) driver.snapshot(self.context, self.instance_ref, image_id, diff --git a/nova/tests/unit/virt/libvirt/test_imagebackend.py b/nova/tests/unit/virt/libvirt/test_imagebackend.py index 89223357f81..109a1e30428 100644 --- a/nova/tests/unit/virt/libvirt/test_imagebackend.py +++ b/nova/tests/unit/virt/libvirt/test_imagebackend.py @@ -27,6 +27,7 @@ from oslo_utils import units from oslo_utils import uuidutils +from nova.compute import utils as compute_utils import nova.conf from nova import context from nova import exception @@ -673,13 +674,14 @@ def setUp(self): self.LV = '%s_%s' % (self.INSTANCE['uuid'], self.NAME) self.PATH = os.path.join('/dev', self.VG, self.LV) + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch.object(imagebackend.lvm, 'create_volume') @mock.patch.object(imagebackend.disk, 'get_disk_size', return_value=TEMPLATE_SIZE) @mock.patch('nova.privsep.qemu.convert_image') def _create_image(self, sparse, mock_convert_image, mock_get, mock_create, - mock_ignored): + mock_ignored, mock_disk_op_sema): fn = mock.MagicMock() image = self.image_class(self.INSTANCE, self.NAME) @@ -695,6 +697,7 @@ def _create_image(self, sparse, mock_convert_image, mock_get, mock_create, path = '/dev/%s/%s_%s' % (self.VG, self.INSTANCE.uuid, self.NAME) mock_convert_image.assert_called_once_with( self.TEMPLATE_PATH, path, None, 'raw', CONF.instances_path) + mock_disk_op_sema.__enter__.assert_called_once() @mock.patch.object(imagebackend.lvm, 'create_volume') def _create_image_generated(self, sparse, mock_create): @@ -708,6 +711,7 @@ def _create_image_generated(self, sparse, mock_create): self.SIZE, sparse=sparse) fn.assert_called_once_with(target=self.PATH, ephemeral_size=None) + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch.object(imagebackend.disk, 'resize2fs') @mock.patch.object(imagebackend.lvm, 'create_volume') @@ -715,7 +719,8 @@ def _create_image_generated(self, sparse, mock_create): return_value=TEMPLATE_SIZE) @mock.patch('nova.privsep.qemu.convert_image') def _create_image_resize(self, sparse, mock_convert_image, mock_get, - mock_create, mock_resize, mock_ignored): + mock_create, mock_resize, mock_ignored, + mock_disk_op_sema): fn = mock.MagicMock() fn(target=self.TEMPLATE_PATH) image = self.image_class(self.INSTANCE, self.NAME) @@ -727,6 +732,7 @@ def _create_image_resize(self, sparse, mock_convert_image, mock_get, mock_convert_image.assert_called_once_with( self.TEMPLATE_PATH, self.PATH, None, 'raw', CONF.instances_path) + mock_disk_op_sema.__enter__.assert_called_once() mock_resize.assert_called_once_with(self.PATH, run_as_root=True) @mock.patch.object(imagebackend.fileutils, 'ensure_tree') @@ -945,13 +951,14 @@ def _create_image(self, sparse): mock.Mock()), mock.patch.object(self.libvirt_utils, 'remove_logical_volumes', mock.Mock()), - mock.patch('nova.privsep.qemu.convert_image')): + mock.patch('nova.privsep.qemu.convert_image'), + mock.patch.object(compute_utils, 'disk_ops_semaphore')): fn = mock.Mock() image = self.image_class(self.INSTANCE, self.NAME) image.create_image(fn, self.TEMPLATE_PATH, self.TEMPLATE_SIZE, context=self.CONTEXT) - + compute_utils.disk_ops_semaphore.__enter__.assert_called_once() fn.assert_called_with(context=self.CONTEXT, target=self.TEMPLATE_PATH) self.lvm.create_volume.assert_called_with(self.VG, @@ -1021,13 +1028,14 @@ def _create_image_resize(self, sparse): mock.Mock()), mock.patch.object(self.libvirt_utils, 'remove_logical_volumes', mock.Mock()), - mock.patch('nova.privsep.qemu.convert_image')): + mock.patch('nova.privsep.qemu.convert_image'), + mock.patch.object(compute_utils, 'disk_ops_semaphore')): fn = mock.Mock() image = self.image_class(self.INSTANCE, self.NAME) image.create_image(fn, self.TEMPLATE_PATH, self.SIZE, context=self.CONTEXT) - + compute_utils.disk_ops_semaphore.__enter__.assert_called_once() fn.assert_called_with(context=self.CONTEXT, target=self.TEMPLATE_PATH) self.disk.get_disk_size.assert_called_with(self.TEMPLATE_PATH) diff --git a/nova/tests/unit/virt/libvirt/test_utils.py b/nova/tests/unit/virt/libvirt/test_utils.py index c31332f3b17..f3802f4dd8f 100644 --- a/nova/tests/unit/virt/libvirt/test_utils.py +++ b/nova/tests/unit/virt/libvirt/test_utils.py @@ -25,6 +25,7 @@ from oslo_utils.fixture import uuidsentinel as uuids import six +from nova.compute import utils as compute_utils from nova import context from nova import exception from nova import objects @@ -518,7 +519,9 @@ def test_write_to_file_with_umask(self): finally: os.unlink(dst_path) - def _do_test_extract_snapshot(self, mock_execute, src_format='qcow2', + @mock.patch.object(compute_utils, 'disk_ops_semaphore') + def _do_test_extract_snapshot(self, mock_execute, mock_disk_op_sema, + src_format='qcow2', dest_format='raw', out_format='raw'): libvirt_utils.extract_snapshot('/path/to/disk/image', src_format, '/extracted/snap', dest_format) @@ -528,6 +531,7 @@ def _do_test_extract_snapshot(self, mock_execute, src_format='qcow2', qemu_img_cmd += ('-c',) qemu_img_cmd += ('/path/to/disk/image', '/extracted/snap') mock_execute.assert_called_once_with(*qemu_img_cmd) + mock_disk_op_sema.__enter__.assert_called_once() @mock.patch.object(utils, 'execute') def test_extract_snapshot_raw(self, mock_execute): @@ -636,9 +640,11 @@ def test_fetch_initrd_image(self, mock_images): mock_images.assert_called_once_with( _context, image_id, target, trusted_certs) + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch('nova.privsep.qemu.unprivileged_convert_image') - def test_fetch_raw_image(self, mock_convert_image, mock_direct_io): + def test_fetch_raw_image(self, mock_convert_image, mock_direct_io, + mock_disk_op_sema): def fake_rename(old, new): self.executes.append(('mv', old, new)) @@ -693,6 +699,7 @@ class FakeImgInfo(object): ('mv', 't.qcow2.converted', 't.qcow2')] images.fetch_to_raw(context, image_id, target) self.assertEqual(self.executes, expected_commands) + mock_disk_op_sema.__enter__.assert_called_once() mock_convert_image.assert_called_with( 't.qcow2.part', 't.qcow2.converted', 'qcow2', 'raw', CONF.instances_path) diff --git a/nova/tests/unit/virt/test_images.py b/nova/tests/unit/virt/test_images.py index 2baf445e2cf..9b3ef7d002c 100644 --- a/nova/tests/unit/virt/test_images.py +++ b/nova/tests/unit/virt/test_images.py @@ -18,6 +18,7 @@ from oslo_concurrency import processutils import six +from nova.compute import utils as compute_utils from nova import exception from nova import test from nova import utils @@ -49,16 +50,19 @@ def test_qemu_info_with_no_errors(self, path_exists, self.assertTrue(image_info) self.assertTrue(str(image_info)) + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch.object(processutils, 'execute', side_effect=processutils.ProcessExecutionError) - def test_convert_image_with_errors(self, mocked_execute, mock_direct_io): + def test_convert_image_with_errors(self, mocked_execute, mock_direct_io, + mock_disk_op_sema): self.assertRaises(exception.ImageUnacceptable, images.convert_image, '/path/that/does/not/exist', '/other/path/that/does/not/exist', 'qcow2', 'raw') + mock_disk_op_sema.__enter__.assert_called_once() @mock.patch.object(utils, 'execute') @mock.patch.object(os.path, 'exists', return_value=True) @@ -101,22 +105,28 @@ def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch): images.fetch_to_raw, None, 'href123', '/no/path') + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch('oslo_concurrency.processutils.execute') def test_convert_image_with_direct_io_support(self, mock_execute, - mock_direct_io): + mock_direct_io, + mock_disk_op_sema): images._convert_image('source', 'dest', 'in_format', 'out_format', run_as_root=False) expected = ('qemu-img', 'convert', '-t', 'none', '-O', 'out_format', '-f', 'in_format', 'source', 'dest') + mock_disk_op_sema.__enter__.assert_called_once() self.assertTupleEqual(expected, mock_execute.call_args[0]) + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=False) @mock.patch('oslo_concurrency.processutils.execute') def test_convert_image_without_direct_io_support(self, mock_execute, - mock_direct_io): + mock_direct_io, + mock_disk_op_sema): images._convert_image('source', 'dest', 'in_format', 'out_format', run_as_root=False) expected = ('qemu-img', 'convert', '-t', 'writethrough', '-O', 'out_format', '-f', 'in_format', 'source', 'dest') + mock_disk_op_sema.__enter__.assert_called_once() self.assertTupleEqual(expected, mock_execute.call_args[0]) diff --git a/nova/tests/unit/virt/xenapi/test_vm_utils.py b/nova/tests/unit/virt/xenapi/test_vm_utils.py index 0815d508c06..435d6da514a 100644 --- a/nova/tests/unit/virt/xenapi/test_vm_utils.py +++ b/nova/tests/unit/virt/xenapi/test_vm_utils.py @@ -27,6 +27,7 @@ from nova.compute import flavors from nova.compute import power_state +from nova.compute import utils as compute_utils import nova.conf from nova import context from nova import exception @@ -132,6 +133,7 @@ def test_rescue_too_many(self): class GenerateConfigDriveTestCase(VMUtilsTestBase): + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch.object(vm_utils, 'safe_find_sr') @mock.patch.object(vm_utils, "create_vdi", return_value='vdi_ref') @mock.patch.object(vm_utils.instance_metadata, "InstanceMetadata") @@ -144,7 +146,7 @@ class GenerateConfigDriveTestCase(VMUtilsTestBase): def test_no_admin_pass(self, mock_tmpdir, mock_create_vbd, mock_size, mock_stream, mock_execute, mock_builder, mock_instance_metadata, mock_create_vdi, - mock_find_sr): + mock_find_sr, mock_disk_op_sema): mock_tmpdir.return_value.__enter__.return_value = '/mock' @@ -153,7 +155,7 @@ def test_no_admin_pass(self, mock_tmpdir, mock_create_vbd, mock_size, vm_utils.generate_configdrive('session', 'context', 'instance', 'vm_ref', 'userdevice', 'network_info') - + mock_disk_op_sema.__enter__.assert_called_once() mock_size.assert_called_with('/mock/configdrive.vhd') mock_open.assert_called_with('/mock/configdrive.vhd') mock_execute.assert_called_with('qemu-img', 'convert', '-Ovpc', diff --git a/nova/virt/images.py b/nova/virt/images.py index dbaa4e7b302..2f04f86eefe 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -28,6 +28,7 @@ from oslo_utils import imageutils from oslo_utils import units +from nova.compute import utils as compute_utils import nova.conf from nova import exception from nova.i18n import _ @@ -118,12 +119,13 @@ def convert_image_unsafe(source, dest, out_format, run_as_root=False): def _convert_image(source, dest, in_format, out_format, run_as_root): try: - if not run_as_root: - nova.privsep.qemu.unprivileged_convert_image( - source, dest, in_format, out_format, CONF.instances_path) - else: - nova.privsep.qemu.convert_image( - source, dest, in_format, out_format, CONF.instances_path) + with compute_utils.disk_ops_semaphore: + if not run_as_root: + nova.privsep.qemu.unprivileged_convert_image( + source, dest, in_format, out_format, CONF.instances_path) + else: + nova.privsep.qemu.convert_image( + source, dest, in_format, out_format, CONF.instances_path) except processutils.ProcessExecutionError as exp: msg = (_("Unable to convert image to %(format)s: %(exp)s") % @@ -133,8 +135,9 @@ def _convert_image(source, dest, in_format, out_format, run_as_root): def fetch(context, image_href, path, trusted_certs=None): with fileutils.remove_path_on_error(path): - IMAGE_API.download(context, image_href, dest_path=path, - trusted_certs=trusted_certs) + with compute_utils.disk_ops_semaphore: + IMAGE_API.download(context, image_href, dest_path=path, + trusted_certs=trusted_certs) def get_info(context, image_href): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 194b5619528..37ad612fa1c 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -1967,10 +1967,12 @@ def snapshot(self, context, instance, image_id, update_task_state): update_task_state(task_state=task_states.IMAGE_UPLOADING, expected_state=task_states.IMAGE_PENDING_UPLOAD) with libvirt_utils.file_open(out_path, 'rb') as image_file: - self._image_api.update(context, - image_id, - metadata, - image_file) + # execute operation with disk concurrency semaphore + with compute_utils.disk_ops_semaphore: + self._image_api.update(context, + image_id, + metadata, + image_file) except Exception: with excutils.save_and_reraise_exception(): LOG.exception(_("Failed to snapshot image")) @@ -2425,8 +2427,10 @@ def _rebase_with_qemu_img(guest, device, active_disk_object, qemu_img_extra_arg = ['-F', b_file_fmt] qemu_img_extra_arg.append(active_disk_object.source_path) - utils.execute("qemu-img", "rebase", "-b", backing_file, - *qemu_img_extra_arg) + # execute operation with disk concurrency semaphore + with compute_utils.disk_ops_semaphore: + utils.execute("qemu-img", "rebase", "-b", backing_file, + *qemu_img_extra_arg) def _volume_snapshot_delete(self, context, instance, volume_id, snapshot_id, delete_info=None): @@ -8361,8 +8365,10 @@ def _wait_for_running(self, instance): def _disk_raw_to_qcow2(path): """Converts a raw disk to qcow2.""" path_qcow = path + '_qcow' - utils.execute('qemu-img', 'convert', '-f', 'raw', - '-O', 'qcow2', path, path_qcow) + # execute operation with disk concurrency semaphore + with compute_utils.disk_ops_semaphore: + utils.execute('qemu-img', 'convert', '-f', 'raw', + '-O', 'qcow2', path, path_qcow) os.rename(path_qcow, path) def finish_migration(self, context, migration, instance, disk_info, diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index dce771ecddb..4250ac93b47 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -26,6 +26,7 @@ from oslo_log import log as logging from oslo_utils import fileutils +from nova.compute import utils as compute_utils import nova.conf from nova.i18n import _ from nova.objects import fields as obj_fields @@ -315,7 +316,9 @@ def extract_snapshot(disk_path, source_fmt, out_path, dest_fmt): qemu_img_cmd += ('-c',) qemu_img_cmd += (disk_path, out_path) - utils.execute(*qemu_img_cmd) + # execute operation with disk concurrency semaphore + with compute_utils.disk_ops_semaphore: + utils.execute(*qemu_img_cmd) def load_file(path): diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 0c930ec7052..0dadb95287f 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -46,6 +46,7 @@ from nova.api.metadata import base as instance_metadata from nova.compute import power_state from nova.compute import task_states +from nova.compute import utils as compute_utils import nova.conf from nova import exception from nova.i18n import _ @@ -1153,8 +1154,9 @@ def generate_configdrive(session, context, instance, vm_ref, userdevice, cdb.make_drive(tmp_file) # XAPI can only import a VHD file, so convert to vhd format vhd_file = '%s.vhd' % tmp_file - utils.execute('qemu-img', 'convert', '-Ovpc', tmp_file, - vhd_file) + with compute_utils.disk_ops_semaphore: + utils.execute('qemu-img', 'convert', '-Ovpc', tmp_file, + vhd_file) vhd_file_size = os.path.getsize(vhd_file) with open(vhd_file) as file_obj: volume_utils.stream_to_vdi( diff --git a/releasenotes/notes/bp-io-semaphore-for-concurrent-disk-ops-690890c9f01fa18c.yaml b/releasenotes/notes/bp-io-semaphore-for-concurrent-disk-ops-690890c9f01fa18c.yaml new file mode 100644 index 00000000000..66b203463f7 --- /dev/null +++ b/releasenotes/notes/bp-io-semaphore-for-concurrent-disk-ops-690890c9f01fa18c.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Introduced a new config option ``[compute]/max_concurrent_disk_ops`` to + reduce disk contention by specifying the maximum number of concurrent + disk-IO-intensive operations per compute service. This would include + operations such as image download, image format conversion, snapshot + extraction, etc. + The default value is 0, which means that there is no limit.