Merge "Allow to perform parallel live migrations"
This commit is contained in:
@@ -370,6 +370,30 @@ Related options:
|
|||||||
* live_migration_downtime
|
* live_migration_downtime
|
||||||
* live_migration_downtime_steps
|
* live_migration_downtime_steps
|
||||||
* live_migration_downtime_delay
|
* live_migration_downtime_delay
|
||||||
|
"""),
|
||||||
|
cfg.IntOpt('live_migration_parallel_connections',
|
||||||
|
default=1,
|
||||||
|
help="""
|
||||||
|
Number of parallel connections to QEMU during live migrations.
|
||||||
|
|
||||||
|
Values above 1 will instruct hypervisor explicitly on amount of connections
|
||||||
|
to use.
|
||||||
|
Please note, that each connection can utilize up to 1 CPU core, especially when
|
||||||
|
``live_migration_with_native_tls`` is used. Therefore it is recommended to
|
||||||
|
reserve CPUs using ``cpu_shared_set``/``cpu_dedicated_set`` or
|
||||||
|
``reserved_host_cpus`` multiplied by ``cpu_allocation_ratio``.
|
||||||
|
|
||||||
|
Usage of ``live_migration_parallel_connections`` in
|
||||||
|
combination with ``live_migration_permit_post_copy`` is supported only with
|
||||||
|
`QEMU>=10.1.0 <https://www.qemu.org/2025/08/26/qemu-10-1-0/>`_.
|
||||||
|
|
||||||
|
Related options:
|
||||||
|
|
||||||
|
* ``[compute] cpu_shared_set``
|
||||||
|
* ``[compute] cpu_dedicated_set``
|
||||||
|
* ``[DEFAULT] reserved_host_cpus``
|
||||||
|
* ``[libvirt] live_migration_permit_post_copy``
|
||||||
|
|
||||||
"""),
|
"""),
|
||||||
cfg.StrOpt('live_migration_timeout_action',
|
cfg.StrOpt('live_migration_timeout_action',
|
||||||
default='abort',
|
default='abort',
|
||||||
@@ -411,10 +435,15 @@ When using post-copy mode, if the source and destination hosts lose network
|
|||||||
connectivity, the VM being live-migrated will need to be rebooted. For more
|
connectivity, the VM being live-migrated will need to be rebooted. For more
|
||||||
details, please see the Administration guide.
|
details, please see the Administration guide.
|
||||||
|
|
||||||
|
Usage of the option together with ``live_migration_parallel_connections``
|
||||||
|
is supported only with QEMU>=10.1.0. Otherwise VM will end up in SHUTOFF
|
||||||
|
state on the destination host.
|
||||||
|
|
||||||
Related options:
|
Related options:
|
||||||
|
|
||||||
* live_migration_permit_auto_converge
|
* live_migration_permit_auto_converge
|
||||||
* live_migration_timeout_action
|
* live_migration_timeout_action
|
||||||
|
* live_migration_parallel_connections
|
||||||
"""),
|
"""),
|
||||||
cfg.BoolOpt('live_migration_permit_auto_converge',
|
cfg.BoolOpt('live_migration_permit_auto_converge',
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
2
nova/tests/fixtures/libvirt.py
vendored
2
nova/tests/fixtures/libvirt.py
vendored
@@ -127,6 +127,8 @@ VIR_MIGRATE_PERSIST_DEST = 8
|
|||||||
VIR_MIGRATE_UNDEFINE_SOURCE = 16
|
VIR_MIGRATE_UNDEFINE_SOURCE = 16
|
||||||
VIR_MIGRATE_NON_SHARED_INC = 128
|
VIR_MIGRATE_NON_SHARED_INC = 128
|
||||||
VIR_MIGRATE_AUTO_CONVERGE = 8192
|
VIR_MIGRATE_AUTO_CONVERGE = 8192
|
||||||
|
VIR_MIGRATE_PARALLEL = 131072
|
||||||
|
VIR_MIGRATE_PARAM_PARALLEL_CONNECTIONS = 'parallel.connections'
|
||||||
VIR_MIGRATE_POSTCOPY = 32768
|
VIR_MIGRATE_POSTCOPY = 32768
|
||||||
VIR_MIGRATE_TLS = 65536
|
VIR_MIGRATE_TLS = 65536
|
||||||
|
|
||||||
|
|||||||
@@ -1223,6 +1223,19 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
|||||||
break
|
break
|
||||||
self.assertTrue(version_arg_found)
|
self.assertTrue(version_arg_found)
|
||||||
|
|
||||||
|
@mock.patch.object(libvirt_driver.LibvirtDriver,
|
||||||
|
'_register_all_undefined_instance_details',
|
||||||
|
new=mock.Mock())
|
||||||
|
@mock.patch.object(fakelibvirt.Connection, 'getVersion',
|
||||||
|
return_value=versionutils.convert_version_to_int(
|
||||||
|
(9, 1, 0)))
|
||||||
|
def test_qemu_multifd_with_postcopy_version_ok(self, mock_gv):
|
||||||
|
self.flags(live_migration_parallel_connections=2,
|
||||||
|
live_migration_permit_post_copy=True,
|
||||||
|
group='libvirt')
|
||||||
|
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
|
||||||
|
self.assertRaises(exception.InternalError, drvr.init_host, "dummyhost")
|
||||||
|
|
||||||
@mock.patch.object(libvirt_driver.LibvirtDriver,
|
@mock.patch.object(libvirt_driver.LibvirtDriver,
|
||||||
'_register_all_undefined_instance_details',
|
'_register_all_undefined_instance_details',
|
||||||
new=mock.Mock())
|
new=mock.Mock())
|
||||||
@@ -2002,6 +2015,21 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
|||||||
libvirt_driver.libvirt.VIR_MIGRATE_NON_SHARED_INC |
|
libvirt_driver.libvirt.VIR_MIGRATE_NON_SHARED_INC |
|
||||||
libvirt_driver.libvirt.VIR_MIGRATE_AUTO_CONVERGE))
|
libvirt_driver.libvirt.VIR_MIGRATE_AUTO_CONVERGE))
|
||||||
|
|
||||||
|
def test_live_migration_parallel_connections_enabled(self):
|
||||||
|
self.flags(live_migration_parallel_connections=2, group='libvirt')
|
||||||
|
self._do_test_parse_migration_flags(
|
||||||
|
lm_expected=(libvirt_driver.libvirt.VIR_MIGRATE_UNDEFINE_SOURCE |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_PERSIST_DEST |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_PEER2PEER |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_LIVE |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_PARALLEL),
|
||||||
|
bm_expected=(libvirt_driver.libvirt.VIR_MIGRATE_UNDEFINE_SOURCE |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_PERSIST_DEST |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_PEER2PEER |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_LIVE |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_NON_SHARED_INC |
|
||||||
|
libvirt_driver.libvirt.VIR_MIGRATE_PARALLEL))
|
||||||
|
|
||||||
def test_live_migration_permit_auto_converge_and_post_copy_true(self):
|
def test_live_migration_permit_auto_converge_and_post_copy_true(self):
|
||||||
self.flags(live_migration_permit_auto_converge=True, group='libvirt')
|
self.flags(live_migration_permit_auto_converge=True, group='libvirt')
|
||||||
self.flags(live_migration_permit_post_copy=True, group='libvirt')
|
self.flags(live_migration_permit_post_copy=True, group='libvirt')
|
||||||
@@ -14133,6 +14161,54 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
|||||||
drvr._live_migration_uri(target_connection),
|
drvr._live_migration_uri(target_connection),
|
||||||
params=params, flags=expected_flags)
|
params=params, flags=expected_flags)
|
||||||
|
|
||||||
|
@mock.patch.object(host.Host, 'has_min_version', return_value=True)
|
||||||
|
@mock.patch.object(fakelibvirt.virDomain, "migrateToURI3")
|
||||||
|
@mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml',
|
||||||
|
return_value='')
|
||||||
|
@mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc', return_value='')
|
||||||
|
def test_block_live_parallel_connections(
|
||||||
|
self, mock_old_xml, mock_new_xml,
|
||||||
|
mock_migrateToURI3, mock_min_version):
|
||||||
|
self.flags(live_migration_parallel_connections=5, group='libvirt')
|
||||||
|
target_connection = None
|
||||||
|
disk_paths = ['vda', 'vdb']
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'bandwidth': CONF.libvirt.live_migration_bandwidth,
|
||||||
|
'migrate_disks': disk_paths,
|
||||||
|
'parallel.connections':
|
||||||
|
CONF.libvirt.live_migration_parallel_connections
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start test
|
||||||
|
migrate_data = objects.LibvirtLiveMigrateData(
|
||||||
|
graphics_listen_addr_vnc='0.0.0.0',
|
||||||
|
graphics_listen_addr_spice='0.0.0.0',
|
||||||
|
serial_listen_addr='127.0.0.1',
|
||||||
|
serial_listen_ports=[1234],
|
||||||
|
target_connect_addr=target_connection,
|
||||||
|
bdms=[],
|
||||||
|
block_migration=True)
|
||||||
|
|
||||||
|
dom = fakelibvirt.virDomain
|
||||||
|
guest = libvirt_guest.Guest(dom)
|
||||||
|
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||||
|
drvr._parse_migration_flags()
|
||||||
|
instance = objects.Instance(**self.test_instance)
|
||||||
|
drvr._live_migration_operation(self.context, instance,
|
||||||
|
target_connection, True, migrate_data,
|
||||||
|
guest, disk_paths)
|
||||||
|
|
||||||
|
expected_flags = (fakelibvirt.VIR_MIGRATE_UNDEFINE_SOURCE |
|
||||||
|
fakelibvirt.VIR_MIGRATE_PERSIST_DEST |
|
||||||
|
fakelibvirt.VIR_MIGRATE_PEER2PEER |
|
||||||
|
fakelibvirt.VIR_MIGRATE_NON_SHARED_INC |
|
||||||
|
fakelibvirt.VIR_MIGRATE_LIVE |
|
||||||
|
fakelibvirt.VIR_MIGRATE_PARALLEL)
|
||||||
|
mock_migrateToURI3.assert_called_once_with(
|
||||||
|
drvr._live_migration_uri(target_connection),
|
||||||
|
params=params, flags=expected_flags)
|
||||||
|
|
||||||
@mock.patch.object(host.Host, 'has_min_version', return_value=True)
|
@mock.patch.object(host.Host, 'has_min_version', return_value=True)
|
||||||
@mock.patch.object(fakelibvirt.virDomain, "migrateToURI3")
|
@mock.patch.object(fakelibvirt.virDomain, "migrateToURI3")
|
||||||
@mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml',
|
@mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml',
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ MIN_VFIO_PCI_VARIANT_QEMU_VERSION = (8, 2, 2)
|
|||||||
MIN_VIRTIO_SOUND_LIBVIRT_VERSION = (10, 4, 0)
|
MIN_VIRTIO_SOUND_LIBVIRT_VERSION = (10, 4, 0)
|
||||||
MIN_VIRTIO_SOUND_QEMU_VERSION = (8, 2, 0)
|
MIN_VIRTIO_SOUND_QEMU_VERSION = (8, 2, 0)
|
||||||
|
|
||||||
|
# Minimum version of Qemu that supports multifd migration with post-copy
|
||||||
|
MIN_MULTIFD_WITH_POSTCOPY_QEMU_VERSION = (10, 1, 0)
|
||||||
|
|
||||||
REGISTER_IMAGE_PROPERTY_DEFAULTS = [
|
REGISTER_IMAGE_PROPERTY_DEFAULTS = [
|
||||||
'hw_machine_type',
|
'hw_machine_type',
|
||||||
'hw_cdrom_bus',
|
'hw_cdrom_bus',
|
||||||
@@ -814,6 +817,16 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||||||
raise exception.InternalError(
|
raise exception.InternalError(
|
||||||
_('Nova requires QEMU version %s or greater.') %
|
_('Nova requires QEMU version %s or greater.') %
|
||||||
libvirt_utils.version_to_string(MIN_QEMU_VERSION))
|
libvirt_utils.version_to_string(MIN_QEMU_VERSION))
|
||||||
|
if (CONF.libvirt.virt_type in ("qemu", "kvm") and
|
||||||
|
CONF.libvirt.live_migration_parallel_connections > 1 and
|
||||||
|
CONF.libvirt.live_migration_permit_post_copy is True):
|
||||||
|
if not self._host.has_min_version(
|
||||||
|
hv_ver=MIN_MULTIFD_WITH_POSTCOPY_QEMU_VERSION):
|
||||||
|
raise exception.InternalError(
|
||||||
|
_('Nova requires QEMU version %s or greater to use '
|
||||||
|
'live migration parallel connections with post-copy.') %
|
||||||
|
libvirt_utils.version_to_string(
|
||||||
|
MIN_MULTIFD_WITH_POSTCOPY_QEMU_VERSION))
|
||||||
|
|
||||||
if CONF.libvirt.virt_type == 'parallels':
|
if CONF.libvirt.virt_type == 'parallels':
|
||||||
if not self._host.has_min_version(hv_ver=MIN_VIRTUOZZO_VERSION):
|
if not self._host.has_min_version(hv_ver=MIN_VIRTUOZZO_VERSION):
|
||||||
@@ -1354,6 +1367,11 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||||||
migration_flags |= libvirt.VIR_MIGRATE_AUTO_CONVERGE
|
migration_flags |= libvirt.VIR_MIGRATE_AUTO_CONVERGE
|
||||||
return migration_flags
|
return migration_flags
|
||||||
|
|
||||||
|
def _handle_live_migration_parallel(self, migration_flags):
|
||||||
|
if CONF.libvirt.live_migration_parallel_connections > 1:
|
||||||
|
migration_flags |= libvirt.VIR_MIGRATE_PARALLEL
|
||||||
|
return migration_flags
|
||||||
|
|
||||||
def _parse_migration_flags(self):
|
def _parse_migration_flags(self):
|
||||||
(live_migration_flags,
|
(live_migration_flags,
|
||||||
block_migration_flags) = self._prepare_migration_flags()
|
block_migration_flags) = self._prepare_migration_flags()
|
||||||
@@ -1378,6 +1396,11 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||||||
block_migration_flags = self._handle_live_migration_auto_converge(
|
block_migration_flags = self._handle_live_migration_auto_converge(
|
||||||
block_migration_flags)
|
block_migration_flags)
|
||||||
|
|
||||||
|
live_migration_flags = self._handle_live_migration_parallel(
|
||||||
|
live_migration_flags)
|
||||||
|
block_migration_flags = self._handle_live_migration_parallel(
|
||||||
|
block_migration_flags)
|
||||||
|
|
||||||
self._live_migration_flags = live_migration_flags
|
self._live_migration_flags = live_migration_flags
|
||||||
self._block_migration_flags = block_migration_flags
|
self._block_migration_flags = block_migration_flags
|
||||||
|
|
||||||
@@ -11173,12 +11196,14 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||||||
serial_ports = list(self._get_serial_ports_from_guest(guest))
|
serial_ports = list(self._get_serial_ports_from_guest(guest))
|
||||||
|
|
||||||
LOG.debug("About to invoke the migrate API", instance=instance)
|
LOG.debug("About to invoke the migrate API", instance=instance)
|
||||||
guest.migrate(self._live_migration_uri(dest),
|
guest.migrate(
|
||||||
|
self._live_migration_uri(dest),
|
||||||
migrate_uri=migrate_uri,
|
migrate_uri=migrate_uri,
|
||||||
flags=migration_flags,
|
flags=migration_flags,
|
||||||
migrate_disks=device_names,
|
migrate_disks=device_names,
|
||||||
destination_xml=new_xml_str,
|
destination_xml=new_xml_str,
|
||||||
bandwidth=CONF.libvirt.live_migration_bandwidth)
|
bandwidth=CONF.libvirt.live_migration_bandwidth,
|
||||||
|
parallel=CONF.libvirt.live_migration_parallel_connections)
|
||||||
LOG.debug("Migrate API has completed", instance=instance)
|
LOG.debug("Migrate API has completed", instance=instance)
|
||||||
|
|
||||||
for hostname, port in serial_ports:
|
for hostname, port in serial_ports:
|
||||||
|
|||||||
@@ -583,7 +583,8 @@ class Guest(object):
|
|||||||
self._domain.suspend()
|
self._domain.suspend()
|
||||||
|
|
||||||
def migrate(self, destination, migrate_uri=None, migrate_disks=None,
|
def migrate(self, destination, migrate_uri=None, migrate_disks=None,
|
||||||
destination_xml=None, flags=0, bandwidth=0):
|
destination_xml=None, flags=0, bandwidth=0,
|
||||||
|
parallel=0):
|
||||||
"""Migrate guest object from its current host to the destination
|
"""Migrate guest object from its current host to the destination
|
||||||
|
|
||||||
:param destination: URI of host destination where guest will be migrate
|
:param destination: URI of host destination where guest will be migrate
|
||||||
@@ -609,9 +610,12 @@ class Guest(object):
|
|||||||
not change its memory faster than a
|
not change its memory faster than a
|
||||||
hypervisor can transfer the changed
|
hypervisor can transfer the changed
|
||||||
memory to the destination host
|
memory to the destination host
|
||||||
|
VIR_MIGRATE_PARALLEL Send memory pages to the destination host
|
||||||
|
through several network connections.
|
||||||
VIR_MIGRATE_POSTCOPY Tell libvirt to enable post-copy migration
|
VIR_MIGRATE_POSTCOPY Tell libvirt to enable post-copy migration
|
||||||
VIR_MIGRATE_TLS Use QEMU-native TLS
|
VIR_MIGRATE_TLS Use QEMU-native TLS
|
||||||
:param bandwidth: The maximum bandwidth in MiB/s
|
:param bandwidth: The maximum bandwidth in MiB/s
|
||||||
|
:param parallel: Number of connections used during live migration
|
||||||
"""
|
"""
|
||||||
params = {}
|
params = {}
|
||||||
# In migrateToURI3 these parameters are extracted from the
|
# In migrateToURI3 these parameters are extracted from the
|
||||||
@@ -625,6 +629,8 @@ class Guest(object):
|
|||||||
params['migrate_disks'] = migrate_disks
|
params['migrate_disks'] = migrate_disks
|
||||||
if migrate_uri:
|
if migrate_uri:
|
||||||
params['migrate_uri'] = migrate_uri
|
params['migrate_uri'] = migrate_uri
|
||||||
|
if parallel > 1:
|
||||||
|
params[libvirt.VIR_MIGRATE_PARAM_PARALLEL_CONNECTIONS] = parallel
|
||||||
|
|
||||||
# Due to a quirk in the libvirt python bindings,
|
# Due to a quirk in the libvirt python bindings,
|
||||||
# VIR_MIGRATE_NON_SHARED_INC with an empty migrate_disks is
|
# VIR_MIGRATE_NON_SHARED_INC with an empty migrate_disks is
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Implemented parallel live migrations for libvirt driver, which can be
|
||||||
|
enabled by defining ``[libvirt] live_migration_parallel_connections``
|
||||||
|
to a value higher than 1. By default parallel migrations are not used to
|
||||||
|
preserve existing behavior. Also note that QEMU prior to 10.1.0 was not
|
||||||
|
able to support multifd with postcopy migrations, so enabling these two
|
||||||
|
features together on older QEMU versions is disallowed.
|
||||||
Reference in New Issue
Block a user