Skip to content

Commit

Permalink
support preallocated VM images
Browse files Browse the repository at this point in the history
Use fallocate where available to allocate file system blocks efficiently
when the VM is initially provisioned. This will give immediate feedback
if enough space isn't available. Also it may significantly improve
performance on writes to new blocks, and may even improve I/O
performance to prewritten blocks due to reduced fragmentation.

Also noted is the potential to subsume the less general
libvirt_sparse_logical_volumes option into the new more general
preallocate_images option.

Implements bp: preallocated-images
Change-Id: I4bbb8616416c9dca3a3a8894ebf764a0b6b8dbc9
  • Loading branch information
Pádraig Brady committed Feb 20, 2013
1 parent 4e54c59 commit 24f6c62
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 0 deletions.
38 changes: 38 additions & 0 deletions nova/tests/test_imagebackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from nova.openstack.common import uuidutils
from nova import test
from nova.tests import fake_libvirt_utils
from nova.tests import fake_utils
from nova.virt.libvirt import imagebackend

CONF = cfg.CONF
Expand Down Expand Up @@ -125,6 +126,27 @@ def test_cache_template_exists(self):

self.mox.VerifyAll()

def test_prealloc_image(self):
CONF.set_override('preallocate_images', 'space')

fake_utils.fake_execute_clear_log()
fake_utils.stub_out_utils_execute(self.stubs)
image = self.image_class(self.INSTANCE, self.NAME)

def fake_fetch(target, *args, **kwargs):
return

self.stubs.Set(os.path, 'exists', lambda _: True)

# Call twice to verify testing fallocate is only called once.
image.cache(fake_fetch, self.TEMPLATE_PATH, self.SIZE)
image.cache(fake_fetch, self.TEMPLATE_PATH, self.SIZE)

self.assertEqual(fake_utils.fake_execute_get_log(),
['fallocate -n -l 1 %s.fallocate_test' % self.PATH,
'fallocate -n -l %s %s' % (self.SIZE, self.PATH),
'fallocate -n -l %s %s' % (self.SIZE, self.PATH)])


class RawTestCase(_ImageTestCase, test.TestCase):

Expand Down Expand Up @@ -361,6 +383,22 @@ def test_create_image_generated_negative(self):
ephemeral_size=None)
self.mox.VerifyAll()

def test_prealloc_image(self):
CONF.set_override('preallocate_images', 'space')

fake_utils.fake_execute_clear_log()
fake_utils.stub_out_utils_execute(self.stubs)
image = self.image_class(self.INSTANCE, self.NAME)

def fake_fetch(target, *args, **kwargs):
return

self.stubs.Set(os.path, 'exists', lambda _: True)

image.cache(fake_fetch, self.TEMPLATE_PATH, self.SIZE)

self.assertEqual(fake_utils.fake_execute_get_log(), [])


class BackendTestCase(test.TestCase):
INSTANCE = {'name': 'fake-instance',
Expand Down
5 changes: 5 additions & 0 deletions nova/virt/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
default=None,
help='The default format an ephemeral_volume will be '
'formatted with on creation.'),
cfg.StrOpt('preallocate_images',
default='none',
help='VM image preallocation mode: '
'"none" => no storage provisioning is done up front, '
'"space" => storage is fully allocated at instance start'),
cfg.BoolOpt('use_cow_images',
default=True,
help='Whether to use cow images'),
Expand Down
35 changes: 35 additions & 0 deletions nova/virt/libvirt/imagebackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from nova.openstack.common import excutils
from nova.openstack.common import fileutils
from nova.openstack.common import lockutils
from nova.openstack.common import log as logging
from nova import utils
from nova.virt.disk import api as disk
from nova.virt import images
Expand Down Expand Up @@ -52,6 +53,9 @@
CONF = cfg.CONF
CONF.register_opts(__imagebackend_opts)
CONF.import_opt('base_dir_name', 'nova.virt.libvirt.imagecache')
CONF.import_opt('preallocate_images', 'nova.virt.driver')

LOG = logging.getLogger(__name__)


class Image(object):
Expand All @@ -67,6 +71,7 @@ def __init__(self, source_type, driver_format, is_block_dev=False):
self.source_type = source_type
self.driver_format = driver_format
self.is_block_dev = is_block_dev
self.preallocate = False

# NOTE(mikal): We need a lock directory which is shared along with
# instance files, to cover the scenario where multiple compute nodes
Expand Down Expand Up @@ -133,6 +138,25 @@ def call_if_not_exists(target, *args, **kwargs):
self.create_image(call_if_not_exists, base, size,
*args, **kwargs)

if size and self.preallocate and self._can_fallocate():
utils.execute('fallocate', '-n', '-l', size, self.path)

def _can_fallocate(self):
"""Check once per class, whether fallocate(1) is available,
and that the instances directory supports fallocate(2).
"""
can_fallocate = getattr(self.__class__, 'can_fallocate', None)
if can_fallocate is None:
_out, err = utils.trycmd('fallocate', '-n', '-l', '1',
self.path + '.fallocate_test')
utils.delete_if_exists(self.path + '.fallocate_test')
can_fallocate = not err
self.__class__.can_fallocate = can_fallocate
if not can_fallocate:
LOG.error('Unable to preallocate_images=%s at path: %s' %
(CONF.preallocate_images, self.path))
return can_fallocate

def snapshot_create(self):
raise NotImplementedError

Expand All @@ -152,6 +176,7 @@ def __init__(self, instance=None, disk_name=None, path=None,
os.path.join(libvirt_utils.get_instance_path(instance),
disk_name))
self.snapshot_name = snapshot_name
self.preallocate = CONF.preallocate_images != 'none'

def create_image(self, prepare_template, base, size, *args, **kwargs):
@lockutils.synchronized(base, 'nova-', external=True,
Expand Down Expand Up @@ -190,11 +215,15 @@ def __init__(self, instance=None, disk_name=None, path=None,
os.path.join(libvirt_utils.get_instance_path(instance),
disk_name))
self.snapshot_name = snapshot_name
self.preallocate = CONF.preallocate_images != 'none'

def create_image(self, prepare_template, base, size, *args, **kwargs):
@lockutils.synchronized(base, 'nova-', external=True,
lock_path=self.lock_path)
def copy_qcow2_image(base, target, size):
# TODO(pbrady): Consider copying the cow image here
# with preallocation=metadata set for performance reasons.
# This would be keyed on a 'preallocate_images' setting.
libvirt_utils.create_cow_image(base, target)
if size:
disk.extend(target, size)
Expand Down Expand Up @@ -241,13 +270,19 @@ def __init__(self, instance=None, disk_name=None, path=None,
self.escape(disk_name))
self.path = os.path.join('/dev', self.vg, self.lv)

# TODO(pbrady): possibly deprecate libvirt_sparse_logical_volumes
# for the more general preallocate_images
self.sparse = CONF.libvirt_sparse_logical_volumes
self.preallocate = not self.sparse

if snapshot_name:
self.snapshot_name = snapshot_name
self.snapshot_path = os.path.join('/dev', self.vg,
self.snapshot_name)

def _can_fallocate(self):
return False

def create_image(self, prepare_template, base, size, *args, **kwargs):
@lockutils.synchronized(base, 'nova-', external=True,
lock_path=self.lock_path)
Expand Down

0 comments on commit 24f6c62

Please sign in to comment.