冷迁移与Resize
1、迁移是指将虚拟机从一个计算节点迁移到另外一个节点上。冷迁移过程中虚拟机是关机或是处于不可用的状态,而热迁移则需要保证虚拟机时刻运行。
2、Resize则是指根据需求调整虚拟机的计算能力和资源。Resize和冷迁移的工作流程相同,区别只是Resize时必须保持新的Flavor配置大于旧的配置,而冷迁移则要求两者相同。
3、resize和冷迁移的本质区别:
同一个接口,根据传参不同,有flavor就执行升配,无flavor就走的是冷迁移。
1、 入口API:nova/api/openstack/compute/migrate_server.py
class MigrateServerController(wsgi.Controller):
................
def _migrate(self, req, id, body):
"""允许管理员将服务器迁移到新主机。"""
context = req.environ['nova.context']
instance = common.get_instance(self.compute_api, context, id,
expected_attrs=['flavor', 'services'])
context.can(ms_policies.POLICY_ROOT % 'migrate',
target={'project_id': instance.project_id})
host_name = None
if (api_version_request.is_supported(req, min_version='2.56') and
body['migrate'] is not None):
host_name = body['migrate'].get('host')
try:
self.compute_api.resize(req.environ['nova.context'], instance,
host_name=host_name)
................
instance = common.get_instance(self.compute_api, context, id,expected_attrs=['flavor', 'services'])依据instance id 获取instance id 并对instance的存在性做了校验
实际调用函数self.compute_api.resize(req.environ['nova.context'], instance,host_name=host_name)。compute_api.resize定义在 nova/compute/api.py
2、nova/compute/api.py
class API:
"""用于与计算管理器交互的API."""
@check_instance_lock
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED])
@check_instance_host(check_is_up=True)
def resize(self, context, instance, flavor_id=None, clean_shutdown=True,
host_name=None, auto_disk_config=None):
"""调整(即,迁移)一个正在运行的实例的大小。
如果flavor_id为None,则认为该进程是迁移,保持
原flavor_id。如果flavor_id不是None,实例应该
迁移到新的主机并调整大小为新的flavor_id。
在调整大小的情况下,host_name总是None。
Host_name仅在冷迁移情况下可以设置。
"""
allow_cross_cell_resize = self._allow_cross_cell_resize(
context, instance)
if host_name is not None:
node = self._validate_host_for_cold_migrate(
context, instance, host_name, allow_cross_cell_resize)
# 校验磁盘信息
self._check_auto_disk_config(
instance, auto_disk_config=auto_disk_config)
# 获取当前flavor 并校验flavor
current_flavor = instance.get_flavor()
’’’
注意(aarents):确保image_base_image_ref存在,因为在finish_resize/cross_cell_resize期间需要它。实例升级
从一个较老的新星释放可能没有这个属性,因为
a rebuild bug bug /1893618。
‘’’
instance.system_metadata.update(
{'image_base_image_ref': instance.image_ref}
)
volume_backed = None
# 如果没有提供flavor_id,则只迁移实例。
if not flavor_id:
LOG.debug("flavor_id is None. Assuming migration.",
instance=instance)
# 保证迁移前后虚拟机 Flavor 不会发生改变
new_flavor = current_flavor
else:
................
filter_properties = {'ignore_hosts': []}
“””通过配置项 allow_resize_to_same_host 来决定是否会 resize 到同一个计算节点
实际上,当 Migrate 到同一个计算节点时,nova-compute 会触发 UnableToMigrateToSelf 异常,
再继续 Retry Scheduler,直至调度到合适的计算节点或异常退出,前提是 nova-scheduler 启用了 RetryFilter
“””
if not self._allow_resize_to_same_host(same_flavor, instance):
filter_properties['ignore_hosts'].append(instance.host)
# 获取request_spec,该参数为新的调度参数,将ignore host加入调度
request_spec = objects.RequestSpec.get_by_instance_uuid(
context, instance.uuid)
request_spec.ignore_hosts = filter_properties['ignore_hosts']
................
# 更新虚拟机状态为RESIZE_PREP, 虚拟机设置为该状态开始,将会锁定,不允许进行其他任务
instance.task_state = task_states.RESIZE_PREP
instance.progress = 0
instance.auto_disk_config = auto_disk_config or False
instance.save(expected_task_state=[None])
# 发送迁移状态
if not flavor_id:
self._record_action_start(context, instance, instance_actions.MIGRATE)
else:
self._record_action_start(context, instance,instance_actions.RESIZE)
scheduler_hint = {'filter_properties': filter_properties}
................
# resize_instance 任务迁移
self.compute_task_api.resize_instance(
context, instance,
scheduler_hint=scheduler_hint,
flavor=new_flavor,
clean_shutdown=clean_shutdown,
request_spec=request_spec,
do_cast=True)
该步骤主要是进行参数校验,并获取request_spec 调度参数。 调用nova/conductor/api的resize_instance函数
3、nova/conductor/api
def resize_instance(self, context, instance, scheduler_hint, flavor,
reservations=None, clean_shutdown=True,
request_spec=None, host_list=None, do_cast=False):
self.conductor_compute_rpcapi.migrate_server(
context, instance, scheduler_hint, live=False, rebuild=False,
flavor=flavor, block_migration=None, disk_over_commit=None,
reservations=reservations, clean_shutdown=clean_shutdown,
request_spec=request_spec, host_list=host_list,
do_cast=do_cast)
调用的conductor 层的 migrate_server, 通过rpc远程调用方式进行。
4、nova/conductor/rcpapi.py
def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
flavor, block_migration, disk_over_commit,
reservations=None, clean_shutdown=True, request_spec=None,
host_list=None, do_cast=False):
cctxt = self.client.prepare(
version=version,
call_monitor_timeout=CONF.rpc_response_timeout,
timeout=CONF.long_rpc_timeout)
if do_cast:
return cctxt.cast(context, 'migrate_server', **kw)
return cctxt.call(context, 'migrate_server', **kw)
实际调用的conductor的migrate_server该过程实际通过Manager进行处理。
5、nova/conductor/manager.py
@targets_cell
@wrap_instance_event(prefix='conductor')
def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
flavor, block_migration, disk_over_commit, reservations=None,
clean_shutdown=True, request_spec=None, host_list=None):
if instance and not isinstance(instance, nova_object.NovaObject):
# NOTE(danms): Until v2 of the RPC API, we need to tolerate
# old-world instance objects here
attrs = ['metadata', 'system_metadata', 'info_cache',
'security_groups']
instance = objects.Instance._from_db_object(
context, objects.Instance(), instance,
expected_attrs=attrs)
# NOTE: Remove this when we drop support for v1 of the RPC API
if flavor and not isinstance(flavor, objects.Flavor):
# Code downstream may expect extra_specs to be populated since it
# is receiving an object, so lookup the flavor to ensure this.
flavor = objects.Flavor.get_by_id(context, flavor['id'])
if live and not rebuild and not flavor:
self._live_migrate(context, instance, scheduler_hint,
block_migration, disk_over_commit, request_spec)
elif not live and not rebuild and flavor:
instance_uuid = instance.uuid
with compute_utils.EventReporter(context, 'cold_migrate',
self.host, instance_uuid):
self._cold_migrate(context, instance, flavor,
scheduler_hint['filter_properties'],
clean_shutdown, request_spec,
host_list)
else:
raise NotImplementedError()
migrate_server 是一个公用的函数, 冷迁移, resiez, 以及热迁移都经过该函数。这里冷迁移实际使用_cold_migrate 进行处理。
def _cold_migrate(self, context, instance, flavor, filter_properties,
clean_shutdown, request_spec, host_list):
# 更新request_spec
request_spec = self._get_request_spec_for_cold_migrate(
context, instance, flavor, filter_properties, request_spec)
# 创建冷迁移任务 并执行
task = self._build_cold_migrate_task(context, instance, flavor,
request_spec, clean_shutdown, host_list)
try:
task.execute()
except exception.NoValidHost as ex:
..................
# 保存request_spec 调度参数
if request_spec.obj_what_changed():
request_spec.save()
该过程实际就是做异常处理,并保存请求参数。task.execute() 实际处理由_execute进行。定义在nova/conductor/tasks/migrate.py
6、nova/conductor/task/migrate.py
def _execute(self):
# 将forced host 重置掉,以免影响调度
self.request_spec.reset_forced_destinations()
legacy_props = self.request_spec.to_legacy_filter_properties_dict()
scheduler_utils.setup_instance_group(self.context, self.request_spec)
if not ('requested_destination' in self.request_spec and self.request_spec.requested_destination and 'host' in self.request_spec.requested_destination):
scheduler_utils.populate_retry(legacy_props,self.instance.uuid)
port_res_req, req_lvl_params = (self.network_api.get_requested_resource_for_instance(
self.context, self.instance.uuid))
# 重新调度生成request_spec
self.request_spec.requested_resources = port_res_req
self.request_spec.request_level_params = req_lvl_params
self._set_requested_destination_cell(legacy_props)
.................
if self.host_list is None:
# 调度, 获取可用宿主机,并选用一台
selection = self._schedule()
.................
else:
# 这是一个重新调度,将使用提供的备用主机,在host_list中作为目的主机。
selection = self._reschedule()
scheduler_utils.populate_filter_properties(legacy_props, selection)
(host, node) = (selection.service_host, selection.nodename)
# rpc 调用目的节点prep_resize
self.compute_rpcapi.prep_resize(
self.context, self.instance, self.request_spec.image,
self.flavor, host, migration,
request_spec=self.request_spec, filter_properties=legacy_props,
node=node, clean_shutdown=self.clean_shutdown,
host_list=self.host_list)
该步骤主要进行调度, 选取目标宿主机, 发送任务, 进行迁移。Nova-compute发起RPC请求后,Nova-compute的manager响应对应的方法
7、nova/compute/manager.py
def prep_resize(self, context, image, instance, flavor,
request_spec, filter_properties, node,
clean_shutdown, migration, host_list):
# 校验参数
if node is None:
node = self._get_nodename(instance, refresh=True)
instance_state = instance.vm_state
with self._error_out_instance_on_exception(
context, instance, instance_state=instance_state),\
errors_out_migration_ctxt(migration):
# 通知状态
self._send_prep_resize_notifications(
context, instance, fields.NotificationPhase.START,
flavor)
try:
scheduler_hints = self._get_scheduler_hints(filter_properties,
request_spec)
try:
self._validate_instance_group_policy(context, instance,
scheduler_hints)
except exception.RescheduledException as e:
raise exception.InstanceFaultRollback(inner_exception=e)
self._prep_resize(context, image, instance,
flavor, filter_properties,
node, migration, request_spec,
clean_shutdown)
..............
finally:
self._send_prep_resize_notifications(
context, instance, fields.NotificationPhase.END,
flavor)
由_prep_resize 进行处理
def _prep_resize(
self, context, image, instance, flavor, filter_properties, node,
migration, request_spec, clean_shutdown=True,
):
if not filter_properties:
filter_properties = {}
if not instance.host:
self._set_instance_obj_error_state(instance)
msg = _('Instance has no source host')
raise exception.MigrationError(reason=msg)
“””
判断是否为同一台主机,
并检查 supports_migrate_to_same_host 是否支持迁移到同一台主机上
“””
same_host = instance.host == self.host
# 如果flavor id匹配,它就是迁移;否则调整
if same_host and flavor.id == instance['instance_type_id']:
if not self.driver.capabilities.get(
'supports_migrate_to_same_host', False):
raise exception.InstanceFaultRollback(
inner_exception=exception.UnableToMigrateToSelf(
instance_id=instance.uuid, host=self.host))
# 更新instance 信息 并保存
instance.new_flavor = flavor
vm_state = instance.vm_state
LOG.debug('Stashing vm_state: %s', vm_state, instance=instance)
instance.system_metadata['old_vm_state'] = vm_state
instance.save()
............
limits = filter_properties.get('limits', {})
allocs = self.reportclient.get_allocations_for_consumer(
context, instance.uuid)
# 开始迁移
with self.rt.resize_claim(
context, instance, flavor, node, migration, allocs,
image_meta=image, limits=limits,
) as claim:
LOG.info('Migrating', instance=instance)
# RPC转换到源主机以启动实际的大小调整/迁移。
self.compute_rpcapi.resize_instance(
context, instance, claim.migration, image,
flavor, request_spec, clean_shutdown)
通过RPC回到源主机上由源主机的nova-compute服务完成迁移。
8、nova/compure/rpcapi.py
def resize_instance(self, ctxt, instance, migration, image, flavor,
request_spec, clean_shutdown=True):
version = '6.0'
msg_args = {'instance': instance, 'migration': migration,
'image': image,
'flavor': flavor,
'clean_shutdown': clean_shutdown,
'request_spec': request_spec,
}
client = self.router.client(ctxt)
if not client.can_send_version(version):
version = self._ver(ctxt, '5.2')
del msg_args['flavor']
msg_args['instance_type'] = flavor
if not client.can_send_version(version):
msg_args.pop('request_spec')
version = '5.0'
cctxt = client.prepare(server=_compute_host(None, instance),
version=version)
cctxt.cast(ctxt, 'resize_instance', **msg_args)
通过源码分析,得到 compute_rpcapi.resize_instance 其实是调用instance原来主机上的过程,
进行正式的迁移。
9、nova/compute/manager.py
def resize_instance(self, context, instance, image,
migration, flavor, clean_shutdown,
request_spec):
"""启动一个正在运行的实例迁移到另一个主机。
这是从目标主机的' ' prep_resize ' '例程启动的并在源主机上运行。
"""
try:
self._resize_instance(context, instance, image, migration,
flavor, clean_shutdown, request_spec)
except Exception:
with excutils.save_and_reraise_exception():
self._revert_allocation(context, instance, migration)
主要调用_resize_instance函数。
def _resize_instance(
self, context, instance, image, migration, flavor,
clean_shutdown, request_spec,
):
instance_state = instance.vm_state
with self._error_out_instance_on_exception(
context, instance, instance_state=instance_state), \
errors_out_migration_ctxt(migration):
# 获取instance 网络信息 该部分现在由NEUTRON 处理
network_info = self.network_api.get_instance_nw_info(context, instance)
# 更新状态
migration.status = 'migrating'
migration.save()
instance.task_state = task_states.RESIZE_MIGRATING
instance.save(expected_task_state=task_states.RESIZE_PREP)
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
# 通知instance状态
self._send_resize_instance_notifications(
context, instance, bdms, network_info,
fields.NotificationPhase.START)
# 获取虚拟机的块设备信息
block_device_info = self._get_instance_block_device_info(
context, instance, bdms=bdms)
timeout, retry_interval = self._get_power_off_values(
instance, clean_shutdown)
# 关闭并进行C盘迁移
disk_info = self.driver.migrate_disk_and_power_off(
context, instance, migration.dest_host,
flavor, network_info,
block_device_info,
timeout, retry_interval)
# 中断磁盘映射关系
self._terminate_volume_connections(context, instance, bdms)
# 迁移虚拟机网络
self.network_api.migrate_instance_start(context,
instance,
migration)
# 保存虚拟机状态
migration.status = 'post-migrating'
migration.save()
# 迁移完成
instance.host = migration.dest_compute
instance.node = migration.dest_node
instance.old_flavor = instance.flavor
instance.task_state = task_states.RESIZE_MIGRATED
instance.save(expected_task_state=task_states.RESIZE_MIGRATING)
# 完成迁移, 完成迁移的收尾工作, 实例创建, 启动等
self.compute_rpcapi.finish_resize(context, instance,
migration, image, disk_info, migration.dest_compute,
request_spec)
self._send_resize_instance_notifications(
context, instance, bdms, network_info,
fields.NotificationPhase.END)
self.instance_events.clear_events_for_instance(instance)
关键步骤:
1、关闭并进行磁盘迁移
2、网络迁移
3、迁移收尾工作, 创建实例, 启动实例等
核心步骤:
disk_info = self.driver.migrate_disk_and_power_off(
context, instance, migration.dest_host,
flavor, network_info,
block_device_info,
timeout, retry_interval)
driver.migrate_disk_and_power_off过程·定义在nova/virt/libvirt/driver.py
def migrate_disk_and_power_off(self, context, instance, dest,
flavor, network_info,
block_device_info=None,
timeout=0, retry_interval=0):
LOG.debug("Starting migrate_disk_and_power_off",
instance=instance)
ephemerals = driver.block_device_info_get_ephemerals(block_device_info)
eph_size = (block_device.get_bdm_ephemeral_disk_size(ephemerals) or
instance.flavor.ephemeral_gb)
# 磁盘参数校验
root_down = flavor.root_gb < instance.flavor.root_gb
ephemeral_down = flavor.ephemeral_gb < eph_size
booted_from_volume = self._is_booted_from_volume(block_device_info)
if (root_down and not booted_from_volume) or ephemeral_down:
reason = _("Unable to resize disk down.")
raise exception.InstanceFaultRollback(
exception.ResizeError(reason=reason))
.................
# 是否为共享存储, 如果不是则在目标宿主机上创建对应的目录
if not shared_instance_path:
try:
self._remotefs.create_dir(dest, inst_base)
except processutils.ProcessExecutionError as e:
reason = _("not able to execute ssh command: %s") % e
raise exception.InstanceFaultRollback(
exception.ResizeError(reason=reason))
# 关闭虚拟机
self.power_off(instance, timeout, retry_interval)
self.unplug_vifs(instance, network_info)
# 获取磁盘映射关系
block_device_mapping = driver.block_device_info_get_mapping(
block_device_info)
for vol in block_device_mapping:
connection_info = vol['connection_info']
self._disconnect_volume(context, connection_info, instance)
disk_info = self._get_instance_disk_info(instance, block_device_info)
# 进行目录操作, 磁盘设置等
try:
self._cleanup_failed_instance_base(inst_base_resize)
os.rename(inst_base, inst_base_resize)
if shared_instance_path:
dest = None
fileutils.ensure_tree(inst_base)
on_execute = lambda process: \
self.job_tracker.add_job(instance, process.pid)
on_completion = lambda process: \
self.job_tracker.remove_job(instance, process.pid)
...............
# 将磁盘复制到目标节点
compression = info['type'] not in NO_COMPRESSION_TYPES
libvirt_utils.copy_image(from_path, img_path, host=dest,
on_execute=on_execute,
on_completion=on_completion,
compression=compression)
# 迁移disk.info 磁盘信息, 例如注入信息等
src_disk_info_path = os.path.join(inst_base_resize, 'disk.info')
if os.path.exists(src_disk_info_path):
dst_disk_info_path = os.path.join(inst_base, 'disk.info')
libvirt_utils.copy_image(src_disk_info_path,
dst_disk_info_path,
host=dest, on_execute=on_execute,
on_completion=on_completion)
libvirt_utils.save_and_migrate_vtpm_dir(
instance.uuid, inst_base_resize, inst_base, dest,
on_execute, on_completion)
except Exception:
with excutils.save_and_reraise_exception():
self._cleanup_remote_migration(dest, inst_base,
inst_base_resize,
shared_instance_path)
return jsonutils.dumps(disk_info)
10、nova/compute/rcpapi.py
def finish_resize(self, ctxt, instance, migration, image, disk_info, host,
request_spec):
.............
cctxt = client.prepare(
server=host, version=version)
cctxt.cast(ctxt, 'finish_resize', **msg_args)
def finish_resize(self, context, disk_info, image, instance,
migration, request_spec):
“””
完成迁移过程。
设置新传输的磁盘,并在其新的主机上打开实例。
“””
try:
self._finish_resize_helper(context, disk_info, image, instance,migration, request_spec)
except Exception:
with excutils.save_and_reraise_exception():
LOG.info('Deleting allocations for old flavor on source node '
'%s after finish_resize failure. You may be able to '
'recover the instance by hard rebooting it.',
migration.source_compute, instance=instance)
self._delete_allocation_after_move(
context, instance, migration)
通过调用._finish_resize_helper函数调用_finish_resize 。
def _finish_resize(self, context, instance, migration, disk_info,
image_meta, bdms, request_spec):
resize_instance = False
old_instance_type_id = migration.old_instance_type_id
new_instance_type_id = migration.new_instance_type_id
old_flavor = instance.flavor
old_vm_state = instance.system_metadata.get('old_vm_state',vm_states.ACTIVE)
if old_instance_type_id != new_instance_type_id:
new_flavor = instance.new_flavor # this is set in _prep_resize
self._set_instance_info(instance, new_flavor)
for key in ('root_gb', 'swap', 'ephemeral_gb'):
if old_flavor[key] != new_flavor[key]:
resize_instance = True
break
instance.apply_migration_context()
# 创建设置网络, 并完成网络迁移
self.network_api.setup_networks_on_host(context, instance,
migration.dest_compute)
provider_mappings = self._get_request_group_mapping(request_spec)
self.network_api.migrate_instance_finish(
context, instance, migration, provider_mappings)
network_info = self.network_api.get_instance_nw_info(context, instance)
instance.task_state = task_states.RESIZE_FINISH
instance.save(expected_task_state=task_states.RESIZE_MIGRATED)
self._send_finish_resize_notifications(
context, instance, bdms, network_info,
fields.NotificationPhase.START)
self._update_volume_attachments(context, instance, bdms)
# 获取磁盘信息
block_device_info = self._get_instance_block_device_info(
context, instance, refresh_conn_info=True, bdms=bdms)
power_on = old_vm_state != vm_states.STOPPED
allocations = self.reportclient.get_allocs_for_consumer(
context, instance.uuid)['allocations']
try:
# 完成迁移
self.driver.finish_migration(context, migration, instance,disk_info,
network_info,image_meta, resize_instance,allocations, block_device_info, power_on)
except Exception:
with excutils.save_and_reraise_exception():
if old_instance_type_id != new_instance_type_id:
self._set_instance_info(instance, old_flavor)
self._complete_volume_attachments(context, bdms)
migration.status = 'finished'
migration.save()
# 更新状态
instance.vm_state = vm_states.RESIZED
instance.task_state = None
instance.launched_at = timeutils.utcnow()
instance.save(expected_task_state=task_states.RESIZE_FINISH)
return network_info
finish_migration 定义在 virt/libvirt/driver.py
12、nova/virt/libvirt/driver.py
def finish_migration(
self,
context: nova_context.RequestContext,
migration: 'objects.Migration',
instance: 'objects.Instance',
disk_info: str,
network_info: network_model.NetworkInfo,
image_meta: 'objects.ImageMeta',
resize_instance: bool,
allocations: ty.Dict[str, ty.Any],
block_device_info: ty.Optional[ty.Dict[str, ty.Any]] = None,
power_on: bool = True,
) -> None:
# 在目标主机上完成迁移过程
# 获取磁盘信息,并创建磁盘镜像
block_disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type,
instance,
image_meta,
block_device_info)
self._create_image(context, instance, block_disk_info['mapping'],
block_device_info=block_device_info,
ignore_bdi_for_swap=True,
fallback_from_host=migration.source_compute)
self._ensure_console_log_for_instance(instance)
gen_confdrive = functools.partial(
self._create_configdrive, context, instance,
InjectionInfo(admin_pass=None, network_info=network_info,
files=None))
for info in jsonutils.loads(disk_info):
path = info['path']
disk_name = os.path.basename(path)
if (disk_name != 'disk.config' and
info['type'] == 'raw' and CONF.use_cow_images):
self._disk_raw_to_qcow2(info['path'])
mdevs = self._allocate_mdevs(allocations)
self._finish_migration_vtpm(context, instance)
# 依据instance 信息生成 xml 文件
xml = self._get_guest_xml(context, instance, network_info,
block_disk_info, image_meta,
block_device_info=block_device_info,
mdevs=mdevs)
# 创建虚拟机
guest = self._create_guest_with_network(
context, xml, instance, network_info, block_device_info,
power_on=power_on, vifs_already_plugged=True,
post_xml_callback=gen_confdrive)
if power_on:
timer = loopingcall.FixedIntervalLoopingCall(
self._wait_for_running,
instance)
timer.start(interval=0.5).wait()
guest.sync_guest_time()
LOG.debug("finish_migration finished successfully.", instance=instance)
13、_create_guest_with_network创建虚拟机
_create_guest_and_network 在目标宿主机创建虚拟机, 如果原始状态为active则启动
该步骤在虚拟机创建中分析。