Adds a Cache for Volumes Created from Snapshots with Quobyte

This is not related to the Cinder image cache.

In order to speed up the creation of multiple volumes from a single
snapshot this change adds a cache of volumes created by merging a
snapshots backing chain to the Quobyte driver.
This behaviour can be activated via a new config option
'quobyte_volume_from_snapshot_cache'.
Instead of merging a snapshots backing chain into a new volume each time
a volume is created from this snapshot, the new implementation merges the
backing chain into a volume in the new volume cache. New volumes to be
created from that snapshot are then copied from the cached volume and
no longer require the merging process.
Merging happens only for the first time when the cached volume copy is
created from a specific snapshot. Subsequent creations of volumes from
this snapshot are simply copied from the cache which requires no costly
backing chain merge.

Partial-Bug: #1715078

Change-Id: I2142b1c0a0cc2c4f85794416e702a326d3406b9d
This commit is contained in:
Silvan Kaiser 2017-09-12 09:31:07 +02:00
parent 4f778ee01e
commit 8c72fcadae
3 changed files with 365 additions and 30 deletions

View File

@ -18,6 +18,7 @@
import errno
import os
import psutil
import shutil
import six
import traceback
@ -62,6 +63,18 @@ class QuobyteDriverTestCase(test.TestCase):
SNAP_UUID = 'bacadaca-baca-daca-baca-dacadacadaca'
SNAP_UUID_2 = 'bebedede-bebe-dede-bebe-dedebebedede'
def _get_fake_snapshot(self, src_volume):
snapshot = fake_snapshot.fake_snapshot_obj(
self.context,
volume_name=src_volume.name,
display_name='clone-snap-%s' % src_volume.id,
size=src_volume.size,
volume_size=src_volume.size,
volume_id=src_volume.id,
id=self.SNAP_UUID)
snapshot.volume = src_volume
return snapshot
def setUp(self):
super(QuobyteDriverTestCase, self).setUp()
@ -76,6 +89,7 @@ class QuobyteDriverTestCase(test.TestCase):
self.TEST_MNT_POINT_BASE
self._configuration.nas_secure_file_operations = "auto"
self._configuration.nas_secure_file_permissions = "auto"
self._configuration.quobyte_volume_from_snapshot_cache = False
self._driver =\
quobyte.QuobyteDriver(configuration=self._configuration,
@ -118,6 +132,62 @@ class QuobyteDriverTestCase(test.TestCase):
'fallocate', '-l', '%sG' % test_size, tmp_path,
run_as_root=self._driver._execute_as_root)
@mock.patch.object(os, "makedirs")
@mock.patch.object(os.path, "join", return_value="dummy_path")
@mock.patch.object(os, "access", return_value=True)
def test__ensure_volume_cache_ok(self, os_access_mock, os_join_mock,
os_makedirs_mock):
tmp_path = "/some/random/path"
self._driver._ensure_volume_from_snap_cache(tmp_path)
calls = [mock.call("dummy_path", os.F_OK),
mock.call("dummy_path", os.R_OK),
mock.call("dummy_path", os.W_OK),
mock.call("dummy_path", os.X_OK)]
os_access_mock.assert_has_calls(calls)
os_join_mock.assert_called_once_with(
tmp_path, self._driver.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME)
self.assertFalse(os_makedirs_mock.called)
@mock.patch.object(os, "makedirs")
@mock.patch.object(os.path, "join", return_value="dummy_path")
@mock.patch.object(os, "access", return_value=True)
def test__ensure_volume_cache_create(self, os_access_mock, os_join_mock,
os_makedirs_mock):
tmp_path = "/some/random/path"
os_access_mock.side_effect = [False, True, True, True]
self._driver._ensure_volume_from_snap_cache(tmp_path)
calls = [mock.call("dummy_path", os.F_OK),
mock.call("dummy_path", os.R_OK),
mock.call("dummy_path", os.W_OK),
mock.call("dummy_path", os.X_OK)]
os_access_mock.assert_has_calls(calls)
os_join_mock.assert_called_once_with(
tmp_path, self._driver.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME)
os_makedirs_mock.assert_called_once_with("dummy_path")
@mock.patch.object(os, "makedirs")
@mock.patch.object(os.path, "join", return_value="dummy_path")
@mock.patch.object(os, "access", return_value=True)
def test__ensure_volume_cache_error(self, os_access_mock, os_join_mock,
os_makedirs_mock):
tmp_path = "/some/random/path"
os_access_mock.side_effect = [True, False, False, False]
self.assertRaises(
exception.VolumeDriverException,
self._driver._ensure_volume_from_snap_cache, tmp_path)
calls = [mock.call("dummy_path", os.F_OK),
mock.call("dummy_path", os.R_OK)]
os_access_mock.assert_has_calls(calls)
os_join_mock.assert_called_once_with(
tmp_path, self._driver.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME)
self.assertFalse(os_makedirs_mock.called)
def test_local_path(self):
"""local_path common use case."""
drv = self._driver
@ -166,8 +236,8 @@ class QuobyteDriverTestCase(test.TestCase):
self.TEST_MNT_POINT)
mock_validate.assert_called_once_with(self.TEST_MNT_POINT)
def test_mount_quobyte_should_suppress_and_log_already_mounted_error(self):
"""test_mount_quobyte_should_suppress_and_log_already_mounted_error
def test_mount_quobyte_should_suppress_already_mounted_error(self):
"""test_mount_quobyte_should_suppress_already_mounted_error
Based on /proc/mount, the file system is not mounted yet. However,
mount.quobyte returns with an 'already mounted' error. This is
@ -175,12 +245,13 @@ class QuobyteDriverTestCase(test.TestCase):
successful.
Because _mount_quobyte gets called with ensure=True, the error will
be suppressed and logged instead.
be suppressed instead.
"""
with mock.patch.object(self._driver, '_execute') as mock_execute, \
mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver'
'.read_proc_mount') as mock_open, \
mock.patch('cinder.volume.drivers.quobyte.LOG') as mock_LOG:
mock.patch('cinder.volume.drivers.quobyte.QuobyteDriver'
'._validate_volume') as mock_validate:
# Content of /proc/mount (empty).
mock_open.return_value = six.StringIO()
mock_execute.side_effect = [None, putils.ProcessExecutionError(
@ -196,14 +267,12 @@ class QuobyteDriverTestCase(test.TestCase):
self.TEST_MNT_POINT, run_as_root=False)
mock_execute.assert_has_calls([mkdir_call, mount_call],
any_order=False)
mock_LOG.warning.assert_called_once_with('%s is already mounted',
self.TEST_QUOBYTE_VOLUME)
mock_validate.assert_called_once_with(self.TEST_MNT_POINT)
def test_mount_quobyte_should_reraise_already_mounted_error(self):
"""test_mount_quobyte_should_reraise_already_mounted_error
Like test_mount_quobyte_should_suppress_and_log_already_mounted_error
Like test_mount_quobyte_should_suppress_already_mounted_error
but with ensure=False.
"""
with mock.patch.object(self._driver, '_execute') as mock_execute, \
@ -228,6 +297,68 @@ class QuobyteDriverTestCase(test.TestCase):
mock_execute.assert_has_calls([mkdir_call, mount_call],
any_order=False)
@mock.patch.object(image_utils, "qemu_img_info")
def test_optimize_volume_not(self, iu_qii_mock):
drv = self._driver
vol = self._simple_volume()
vol.size = 3
img_data = mock.Mock()
img_data.disk_size = 3 * units.Gi
iu_qii_mock.return_value = img_data
drv._execute = mock.Mock()
drv._create_regular_file = mock.Mock()
drv.local_path = mock.Mock(return_value="/some/path")
drv.optimize_volume(vol)
iu_qii_mock.assert_called_once_with("/some/path",
run_as_root=drv._execute_as_root)
self.assertFalse(drv._execute.called)
self.assertFalse(drv._create_regular_file.called)
@mock.patch.object(image_utils, "qemu_img_info")
def test_optimize_volume_sparse(self, iu_qii_mock):
drv = self._driver
vol = self._simple_volume()
vol.size = 3
img_data = mock.Mock()
img_data.disk_size = 2 * units.Gi
iu_qii_mock.return_value = img_data
drv._execute = mock.Mock()
drv._create_regular_file = mock.Mock()
drv.local_path = mock.Mock(return_value="/some/path")
drv.optimize_volume(vol)
iu_qii_mock.assert_called_once_with(drv.local_path(),
run_as_root=drv._execute_as_root)
drv._execute.assert_called_once_with(
'truncate', '-s', '%sG' % vol.size, drv.local_path(),
run_as_root=drv._execute_as_root)
self.assertFalse(drv._create_regular_file.called)
@mock.patch.object(image_utils, "qemu_img_info")
def test_optimize_volume_regular(self, iu_qii_mock):
drv = self._driver
drv.configuration.quobyte_qcow2_volumes = False
drv.configuration.quobyte_sparsed_volumes = False
vol = self._simple_volume()
vol.size = 3
img_data = mock.Mock()
img_data.disk_size = 2 * units.Gi
iu_qii_mock.return_value = img_data
drv._execute = mock.Mock()
drv._create_regular_file = mock.Mock()
drv.local_path = mock.Mock(return_value="/some/path")
drv.optimize_volume(vol)
iu_qii_mock.assert_called_once_with(drv.local_path(),
run_as_root=drv._execute_as_root)
self.assertFalse(drv._execute.called)
drv._create_regular_file.assert_called_once_with(drv.local_path(),
vol.size)
def test_get_hash_str(self):
"""_get_hash_str should calculation correct value."""
drv = self._driver
@ -643,15 +774,7 @@ class QuobyteDriverTestCase(test.TestCase):
dest_vol_path = os.path.join(vol_dir, dest_volume['name'])
info_path = os.path.join(vol_dir, src_volume['name']) + '.info'
snapshot = fake_snapshot.fake_snapshot_obj(
self.context,
volume_name=src_volume.name,
display_name='clone-snap-%s' % src_volume.id,
size=src_volume.size,
volume_size=src_volume.size,
volume_id=src_volume.id,
id=self.SNAP_UUID)
snapshot.volume = src_volume
snapshot = self._get_fake_snapshot(src_volume)
snap_file = dest_volume['name'] + '.' + snapshot['id']
snap_path = os.path.join(vol_dir, snap_file)
@ -672,9 +795,8 @@ class QuobyteDriverTestCase(test.TestCase):
{'active': snap_file,
snapshot['id']: snap_file})
image_utils.qemu_img_info = mock.Mock(return_value=img_info)
drv._set_rw_permissions_for_all = mock.Mock()
drv._find_share = mock.Mock()
drv._find_share.return_value = "/some/arbitrary/path"
drv._set_rw_permissions = mock.Mock()
drv.optimize_volume = mock.Mock()
drv._copy_volume_from_snapshot(snapshot, dest_volume, size)
@ -687,7 +809,124 @@ class QuobyteDriverTestCase(test.TestCase):
dest_vol_path,
'raw',
run_as_root=self._driver._execute_as_root))
drv._set_rw_permissions_for_all.assert_called_once_with(dest_vol_path)
drv._set_rw_permissions.assert_called_once_with(dest_vol_path)
drv.optimize_volume.assert_called_once_with(dest_volume)
@mock.patch.object(os, "access", return_value=True)
def test_copy_volume_from_snapshot_cached(self, os_ac_mock):
drv = self._driver
drv.configuration.quobyte_volume_from_snapshot_cache = True
# lots of test vars to be prepared at first
dest_volume = self._simple_volume(
id='c1073000-0000-0000-0000-0000000c1073')
src_volume = self._simple_volume()
vol_dir = os.path.join(self.TEST_MNT_POINT_BASE,
drv._get_hash_str(self.TEST_QUOBYTE_VOLUME))
dest_vol_path = os.path.join(vol_dir, dest_volume['name'])
info_path = os.path.join(vol_dir, src_volume['name']) + '.info'
snapshot = self._get_fake_snapshot(src_volume)
snap_file = dest_volume['name'] + '.' + snapshot['id']
snap_path = os.path.join(vol_dir, snap_file)
cache_path = os.path.join(vol_dir,
drv.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME,
snapshot['id'])
size = dest_volume['size']
qemu_img_output = """image: %s
file format: raw
virtual size: 1.0G (1073741824 bytes)
disk size: 173K
backing file: %s
""" % (snap_file, src_volume['name'])
img_info = imageutils.QemuImgInfo(qemu_img_output)
# mocking and testing starts here
image_utils.convert_image = mock.Mock()
drv._read_info_file = mock.Mock(return_value=
{'active': snap_file,
snapshot['id']: snap_file})
image_utils.qemu_img_info = mock.Mock(return_value=img_info)
drv._set_rw_permissions = mock.Mock()
shutil.copyfile = mock.Mock()
drv.optimize_volume = mock.Mock()
drv._copy_volume_from_snapshot(snapshot, dest_volume, size)
drv._read_info_file.assert_called_once_with(info_path)
image_utils.qemu_img_info.assert_called_once_with(snap_path,
force_share=False,
run_as_root=False)
self.assertFalse(image_utils.convert_image.called,
("_convert_image was called but should not have been")
)
os_ac_mock.assert_called_once_with(
drv._local_volume_from_snap_cache_path(snapshot), os.F_OK)
shutil.copyfile.assert_called_once_with(cache_path, dest_vol_path)
drv._set_rw_permissions.assert_called_once_with(dest_vol_path)
drv.optimize_volume.assert_called_once_with(dest_volume)
def test_copy_volume_from_snapshot_not_cached(self):
drv = self._driver
drv.configuration.quobyte_volume_from_snapshot_cache = True
# lots of test vars to be prepared at first
dest_volume = self._simple_volume(
id='c1073000-0000-0000-0000-0000000c1073')
src_volume = self._simple_volume()
vol_dir = os.path.join(self.TEST_MNT_POINT_BASE,
drv._get_hash_str(self.TEST_QUOBYTE_VOLUME))
src_vol_path = os.path.join(vol_dir, src_volume['name'])
dest_vol_path = os.path.join(vol_dir, dest_volume['name'])
info_path = os.path.join(vol_dir, src_volume['name']) + '.info'
snapshot = self._get_fake_snapshot(src_volume)
snap_file = dest_volume['name'] + '.' + snapshot['id']
snap_path = os.path.join(vol_dir, snap_file)
cache_path = os.path.join(vol_dir,
drv.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME,
snapshot['id'])
size = dest_volume['size']
qemu_img_output = """image: %s
file format: raw
virtual size: 1.0G (1073741824 bytes)
disk size: 173K
backing file: %s
""" % (snap_file, src_volume['name'])
img_info = imageutils.QemuImgInfo(qemu_img_output)
# mocking and testing starts here
image_utils.convert_image = mock.Mock()
drv._read_info_file = mock.Mock(return_value=
{'active': snap_file,
snapshot['id']: snap_file})
image_utils.qemu_img_info = mock.Mock(return_value=img_info)
drv._set_rw_permissions = mock.Mock()
shutil.copyfile = mock.Mock()
drv.optimize_volume = mock.Mock()
drv._copy_volume_from_snapshot(snapshot, dest_volume, size)
drv._read_info_file.assert_called_once_with(info_path)
image_utils.qemu_img_info.assert_called_once_with(snap_path,
force_share=False,
run_as_root=False)
(image_utils.convert_image.
assert_called_once_with(
src_vol_path,
drv._local_volume_from_snap_cache_path(snapshot), 'raw',
run_as_root=self._driver._execute_as_root))
shutil.copyfile.assert_called_once_with(cache_path, dest_vol_path)
drv._set_rw_permissions.assert_called_once_with(dest_vol_path)
drv.optimize_volume.assert_called_once_with(dest_volume)
def test_create_volume_from_snapshot_status_not_available(self):
"""Expect an error when the snapshot's status is not 'available'."""

View File

@ -17,13 +17,16 @@
import errno
import os
import psutil
import shutil
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import fileutils
from oslo_utils import units
from cinder import compute
from cinder import coordination
from cinder import exception
from cinder.i18n import _
from cinder.image import image_utils
@ -32,7 +35,7 @@ from cinder import utils
from cinder.volume import configuration
from cinder.volume.drivers import remotefs as remotefs_drv
VERSION = '1.1.7'
VERSION = '1.1.8'
LOG = logging.getLogger(__name__)
@ -54,6 +57,11 @@ volume_opts = [
default='$state_path/mnt',
help=('Base dir containing the mount point'
' for the Quobyte volume.')),
cfg.BoolOpt('quobyte_volume_from_snapshot_cache',
default=False,
help=('Create a cache of volumes from merged snapshots to '
'speed up creation of multiple volumes from a single '
'snapshot.'))
]
CONF = cfg.CONF
@ -88,6 +96,7 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
1.1.5 - Enables extension of volumes with snapshots
1.1.6 - Optimizes volume creation
1.1.7 - Support fuse subtype based Quobyte mount validation
1.1.8 - Adds optional snapshot merge caching
"""
@ -99,6 +108,8 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Quobyte_CI"
QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME = "volume_from_snapshot_cache"
def __init__(self, execute=processutils.execute, *args, **kwargs):
super(QuobyteDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(volume_opts)
@ -111,6 +122,32 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
self._execute('fallocate', '-l', '%sG' % size,
path, run_as_root=self._execute_as_root)
def _ensure_volume_from_snap_cache(self, mount_path):
"""This expects the Quobyte volume to be mounted & available"""
cache_path = os.path.join(mount_path,
self.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME)
if not os.access(cache_path, os.F_OK):
LOG.info("Volume from snapshot cache directory does not exist, "
"creating the directory %(volcache)s",
{'volcache': cache_path})
os.makedirs(cache_path)
if not (os.access(cache_path, os.R_OK)
and os.access(cache_path, os.W_OK)
and os.access(cache_path, os.X_OK)):
msg = _("Insufficient permissions for Quobyte volume from "
"snapshot cache directory at %(cpath)s. Please update "
"permissions.") % {'cpath': cache_path}
raise exception.VolumeDriverException(msg)
LOG.debug("Quobyte volume from snapshot cache directory validated ok")
def _local_volume_from_snap_cache_path(self, snapshot):
path_to_disk = os.path.join(
self._local_volume_dir(snapshot.volume),
self.QUOBYTE_VOLUME_SNAP_CACHE_DIR_NAME,
snapshot.id)
return path_to_disk
def do_setup(self, context):
"""Any initialization the volume driver does while starting."""
super(QuobyteDriver, self).do_setup(context)
@ -138,6 +175,32 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
else:
raise
def optimize_volume(self, volume):
"""Optimizes a volume for Quobyte
This optimization is normally done during creation but volumes created
from e.g. snapshots require additional grooming.
:param volume: volume reference
"""
volume_path = self.local_path(volume)
volume_size = volume.size
data = image_utils.qemu_img_info(self.local_path(volume),
run_as_root=self._execute_as_root)
if data.disk_size >= (volume_size * units.Gi):
LOG.debug("Optimization of volume %(volpath)s is not required, "
"skipping this step.", {'volpath': volume_path})
return
LOG.debug("Optimizing volume %(optpath)s", {'optpath': volume_path})
if (self.configuration.quobyte_qcow2_volumes or
self.configuration.quobyte_sparsed_volumes):
self._execute('truncate', '-s', '%sG' % volume_size,
volume_path, run_as_root=self._execute_as_root)
else:
self._create_regular_file(volume_path, volume_size)
def set_nas_security_options(self, is_new_cinder_install):
self._execute_as_root = False
@ -222,18 +285,20 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
def create_volume_from_snapshot(self, volume, snapshot):
return self._create_volume_from_snapshot(volume, snapshot)
@coordination.synchronized('{self.driver_prefix}-{snapshot.volume.id}')
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
"""Copy data from snapshot to destination volume.
This is done with a qemu-img convert to raw/qcow2 from the snapshot
qcow2.
qcow2. If the quobyte_volume_from_snapshot_cache is active the result
is copied into the cache and all volumes created from this
snapshot id are directly copied from the cache.
"""
LOG.debug("snapshot: %(snap)s, volume: %(vol)s, ",
{'snap': snapshot.id,
'vol': volume.id,
'size': volume_size})
info_path = self._local_path_volume_info(snapshot.volume)
snap_info = self._read_info_file(info_path)
vol_path = self._local_volume_dir(snapshot.volume)
@ -248,6 +313,7 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
path_to_snap_img = os.path.join(vol_path, img_info.backing_file)
path_to_new_vol = self._local_path_volume(volume)
path_to_cached_vol = self._local_volume_from_snap_cache_path(snapshot)
LOG.debug("will copy from snapshot at %s", path_to_snap_img)
@ -256,12 +322,27 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
else:
out_format = 'raw'
image_utils.convert_image(path_to_snap_img,
path_to_new_vol,
out_format,
run_as_root=self._execute_as_root)
self._set_rw_permissions_for_all(path_to_new_vol)
if not self.configuration.quobyte_volume_from_snapshot_cache:
LOG.debug("Creating direct copy from snapshot")
image_utils.convert_image(path_to_snap_img,
path_to_new_vol,
out_format,
run_as_root=self._execute_as_root)
else:
# create the volume via volume cache
if not os.access(path_to_cached_vol, os.F_OK):
LOG.debug("Caching volume %(volpath)s from snapshot.",
{'volpath': path_to_cached_vol})
image_utils.convert_image(path_to_snap_img,
path_to_cached_vol,
out_format,
run_as_root=self._execute_as_root)
# Copy volume from cache
LOG.debug("Copying volume %(volpath)s from cache",
{'volpath': path_to_new_vol})
shutil.copyfile(path_to_cached_vol, path_to_new_vol)
self._set_rw_permissions(path_to_new_vol)
self.optimize_volume(volume)
@utils.synchronized('quobyte', external=False)
def delete_volume(self, volume):
@ -299,6 +380,9 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
def delete_snapshot(self, snapshot):
"""Apply locking to the delete snapshot operation."""
self._delete_snapshot(snapshot)
if self.configuration.quobyte_volume_from_snapshot_cache:
fileutils.delete_if_exists(
self._local_volume_from_snap_cache_path(snapshot))
@utils.synchronized('quobyte', external=False)
def initialize_connection(self, volume, connector):
@ -495,11 +579,14 @@ class QuobyteDriver(remotefs_drv.RemoteFSSnapDriverDistributed):
except processutils.ProcessExecutionError as exc:
if ensure and 'already mounted' in exc.stderr:
LOG.warning("%s is already mounted", quobyte_volume)
mounted = True
else:
raise
if mounted:
self._validate_volume(mount_path)
if self.configuration.quobyte_volume_from_snapshot_cache:
self._ensure_volume_from_snap_cache(mount_path)
def _validate_volume(self, mount_path):
"""Runs a number of tests on the expect Quobyte mount"""

View File

@ -0,0 +1,9 @@
---
fixes:
- |
Added a new optional cache of volumes generated from snapshots for the
Quobyte backend. Enabling this cache speeds up creation of multiple
volumes from a single snapshot at the cost of a slight increase in
creation time for the first volume generated for this given snapshot.
The ``quobyte_volume_from_snapshot_cache`` option is off by default.