OpenStack 动态迁移流程分析



Live Migration RPC Calls

Let's consider the two hosts called:

  • src (where inst runs)
  • dest

The user call maps to:

  • scheduler rpc: live_migration(block_migration, disk_over_commit, instance_id, dest)

Scheduler driver does the following:

  • check instance exists
  • check source alive
  • check destination alive, and has enough memory
  • check source + destination hypervisor type match and dest is same or newer version than src
  • compute: check_can_live_migrate_on_dest (on dest) *TODO*
  • updates instance db entry to "migrating"
  • compute: live_migration (on src)

Compute:check_can_live_migrate_on_destination does the following (on dest): *TODO*

  • calls compute_driver check_can_live_migration_on_dest
  • which can call the delegate that calls check_can_live_migrate_on_src

Compute:check_can_live_migrate_on_src does the following (on src): *TODO*

  • takes a dictionary from dest
  • possibly throws an exception if there is an issue

Compute: live_migration does the following (on src):

  • check_for_export with volume
  • calls pre_live_migration on dest
  • does rollback on exception
  • calls driver

Compute_driver: live_migration (on src)

  • on success calls manager's post_live_migration
  • on failure calls manager's rollback_live_migration

Compute: post_live_migration (on src):

  • updates floating ips
  • deletes old traces of image
  • calls post_live_migration_at_destination on dest

Compute: rollback_live_migration (on src)

  • updates DB
  • sorts out volumes and networks
  • on block migration, calls rollback_live_migration_at_destination on dest

Compute: post_live_migration_at_destination (on dest)

  • sets up networking
  • sets task as complete

Compute: rollback_live_migration_at_destination (on dest)

  • does some clean up


A. nova/api/openstack/compute/contrib/admin_actions.py:定义了若干管理员权限运行的管理虚拟机的操作

_migrate_live()函数:虚拟机动态迁移在Opentack Nova中的

    @wsgi.action('os-migrateLive')
    def _migrate_live(self, req, id, body):
        """Permit admins to (live) migrate a server to a new host."""
        context = req.environ["nova.context"]
        authorize(context, 'migrateLive')

        try:
            block_migration = body["os-migrateLive"]["block_migration"]
            disk_over_commit = body["os-migrateLive"]["disk_over_commit"]
            host = body["os-migrateLive"]["host"]
        except (TypeError, KeyError):
            msg = _("host, block_migration and disk_over_commit must "
                    "be specified for live migration.")
            raise exc.HTTPBadRequest(explanation=msg)

        try:
            instance = self.compute_api.get(context, id, want_objects=True)
            self.compute_api.live_migrate(context, instance, block_migration,
                                          disk_over_commit, host) #转B
        except (exception.ComputeServiceUnavailable,
                exception.InvalidHypervisorType,
                exception.UnableToMigrateToSelf,
                exception.DestinationHypervisorTooOld) as ex:
            raise exc.HTTPBadRequest(explanation=ex.format_message())
        except Exception:
            if host is None:
                msg = _("Live migration of instance %s to another host "
                        "failed") % id
            else:
                msg = _("Live migration of instance %(id)s to host %(host)s "
                        "failed") % {'id': id, 'host': host}
            LOG.exception(msg)
            # Return messages from scheduler
            raise exc.HTTPBadRequest(explanation=msg)

        return webob.Response(status_int=202)

B. nova/compute/api.py: 注意,此处在live_migrate返回之后,还调用check_instance_state对迁移后的vm进行状态检查

    @check_instance_cell
    @check_instance_state(vm_state=[vm_states.ACTIVE])
    def live_migrate(self, context, instance, block_migration,
                     disk_over_commit, host_name):
        """Migrate a server lively to a new host."""
        LOG.debug(_("Going to try to live migrate instance to %s"),
                  host_name or "another host", instance=instance)

        instance.task_state = task_states.MIGRATING
        instance.save(expected_task_state=None)

        self.compute_task_api.live_migrate_instance(context, instance, #转C
                host_name, block_migration=block_migration,
                disk_over_commit=disk_over_commit)

C.nova/conductor/api.py: class ComputeTaskAPI()

    def live_migrate_instance(self, context, instance, host_name,
                              block_migration, disk_over_commit):
        scheduler_hint = {'host': host_name}
        self.conductor_compute_rpcapi.migrate_server(  #转D
            context, instance, scheduler_hint, True, False, None,
            block_migration, disk_over_commit, None)

D. nova/conductor/rpcapi.py: migrate_server()

    def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
                  flavor, block_migration, disk_over_commit,
                  reservations=None):
        if self.client.can_send_version('1.6'):
            version = '1.6'
        else:
            instance = jsonutils.to_primitive(
                    objects_base.obj_to_primitive(instance))
            version = '1.4'
        flavor_p = jsonutils.to_primitive(flavor)
        cctxt = self.client.prepare(version=version)
        return cctxt.call(context, 'migrate_server',  #转E
                          instance=instance, scheduler_hint=scheduler_hint,
                          live=live, rebuild=rebuild, flavor=flavor_p,
                          block_migration=block_migration,
                          disk_over_commit=disk_over_commit,
                          reservations=reservations)

E. nova/conductor/manager.py: migrate_server()

    def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
            flavor, block_migration, disk_over_commit, reservations=None):
        if instance and not isinstance(instance, instance_obj.Instance):
            # 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 = instance_obj.Instance._from_db_object(
                context, instance_obj.Instance(), instance,
                expected_attrs=attrs)
        if live and not rebuild and not flavor:
            self._live_migrate(context, instance, scheduler_hint, #转F
                               block_migration, disk_over_commit)
        elif not live and not rebuild and flavor:
            instance_uuid = instance['uuid']
            with compute_utils.EventReporter(context, ConductorManager(),
                                         'cold_migrate', instance_uuid):
                self._cold_migrate(context, instance, flavor,
                                   scheduler_hint['filter_properties'],
                                   reservations)
        else:
            raise NotImplementedError()

F.nova/conductor/manager.py : _live_migrate()

    def _live_migrate(self, context, instance, scheduler_hint,
                      block_migration, disk_over_commit):
        destination = scheduler_hint.get("host")
        try:
            live_migrate.execute(context, instance, destination, #转3
                             block_migration, disk_over_commit)
        except (exception.NoValidHost,
                exception.ComputeServiceUnavailable,
                exception.InvalidHypervisorType,
                exception.UnableToMigrateToSelf,
                exception.DestinationHypervisorTooOld,
                exception.InvalidLocalStorage,
                exception.InvalidSharedStorage,
                exception.MigrationPreCheckError) as ex:
            with excutils.save_and_reraise_exception():
                #TODO(johngarbutt) - eventually need instance actions here
                request_spec = {'instance_properties': {
                    'uuid': instance['uuid'], },
                }
                scheduler_utils.set_vm_state_and_notify(context,
                        'compute_task', 'migrate_server',
                        dict(vm_state=instance['vm_state'],
                             task_state=None,
                             expected_task_state=task_states.MIGRATING,),
                        ex, request_spec, self.db)
        except Exception as ex:
            with excutils.save_and_reraise_exception():
                request_spec = {'instance_properties': {
                    'uuid': instance['uuid'], },
                }
                scheduler_utils.set_vm_state_and_notify(context,
                        'compute_task', 'migrate_server',
                        {'vm_state': vm_states.ERROR},
                        ex, request_spec, self.db)



1.动态迁移调用

nova/scheduler/manager.py:SchedulerManager()

    def live_migration(self, context, instance, dest,
                       block_migration, disk_over_commit):
        try:
            self._schedule_live_migration(context, instance, dest,
                    block_migration, disk_over_commit) #调用2
        except (exception.NoValidHost,
                exception.ComputeServiceUnavailable,
                exception.InvalidHypervisorType,
                exception.UnableToMigrateToSelf,
                exception.DestinationHypervisorTooOld,
                exception.InvalidLocalStorage,
                exception.InvalidSharedStorage,
                exception.MigrationPreCheckError) as ex:
            request_spec = {'instance_properties': {
                'uuid': instance['uuid'], },
            }
            with excutils.save_and_reraise_exception():
                self._set_vm_state_and_notify('live_migration',
                            dict(vm_state=instance['vm_state'],
                                 task_state=None,
                                 expected_task_state=task_states.MIGRATING,),
                                              context, ex, request_spec)
        except Exception as ex:
            request_spec = {'instance_properties': {
                'uuid': instance['uuid'], },
            }
            with excutils.save_and_reraise_exception():
                self._set_vm_state_and_notify('live_migration',
                                             {'vm_state': vm_states.ERROR},
                                             context, ex, request_spec)

2.用户调用scheduler rpc准备动态迁移

nova/scheduler/manager.py

    def _schedule_live_migration(self, context, instance, dest,
            block_migration, disk_over_commit): 
        task = live_migrate.LiveMigrationTask(context, instance,
                    dest, block_migration, disk_over_commit) #调用3中的__init__()函数进行初始化
        return task.execute() #返回3中的execute()函数

3.nova/conductor/tasks/live_migration.py

execute()函数中,主要进行了三部分的内容,第一,如果目前主机不存在,则由调度算法选取一个目标主机,并且进行相关的检测,确保能够进行实时迁移操作;第二,如果目标主机存在,则直接进行相关的检测操作,确保能够进行实时迁移操作;第三,执行迁移操作。

class LiveMigrationTask(object):
    def __init__(self, context, instance, destination,
                 block_migration, disk_over_commit):
        self.context = context
        self.instance = instance
        self.destination = destination
        self.block_migration = block_migration
        self.disk_over_commit = disk_over_commit
        self.source = instance.host
        self.migrate_data = None
        self.compute_rpcapi = compute_rpcapi.ComputeAPI()
        self.servicegroup_api = servicegroup.API()
        self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
        self.image_service = glance.get_default_image_service()

    def execute(self):
        self._check_instance_is_running() #检查实例是否存在(check instance exists)
        self._check_host_is_up(self.source) #检测源虚机状态(check source alive)

        if not self.destination:
            self.destination = self._find_destination() #若目的虚机不存在,则找到并转3.1
        else:
            self._check_requested_destination() #转到3.2检测目的虚机状态

        #TODO(johngarbutt) need to move complexity out of compute manager
        #跳向nova/compute/rpcapi.py:live_migration()函数然后使用cast形式调用4
        return self.compute_rpcapi.live_migration(self.context, 
                host=self.source,
                instance=self.instance,
                dest=self.destination,
                block_migration=self.block_migration,
                migrate_data=self.migrate_data)
                #TODO(johngarbutt) disk_over_commit?
            

     

nova/conductor/tasks/live_migration.py

    def _find_destination(self):
        #TODO(johngarbutt) this retry loop should be shared
        attempted_hosts = [self.source]
        image = None
        if self.instance.image_ref:
            image = compute_utils.get_image_metadata(self.context,
                                                     self.image_service,
                                                     self.instance.image_ref,
                                                     self.instance)
        instance_type = flavors.extract_flavor(self.instance)

        host = None
        while host is None:
            self._check_not_over_max_retries(attempted_hosts)

            host = self._get_candidate_destination(image,
                    instance_type, attempted_hosts)
            try:
                self._check_compatible_with_source_hypervisor(host) #检测源和目的虚机hypervisor版本是否匹配
                self._call_livem_checks_on_host(host) #检测能否在目的机上进行迁移 转3.1.1
            except exception.Invalid as e:
                LOG.debug(_("Skipping host: %(host)s because: %(e)s") %
                    {"host": host, "e": e})
                attempted_hosts.append(host)
                host = None
        return host


3.1.1 nova/computer/rpcapi.py:check_can_live_migrate_destination() 这个方法实现的是在目标主机上检测是否可以进行实时迁移,并将检测结果返回给源主机

    def check_can_live_migrate_destination(self, ctxt, instance, destination,
                                           block_migration, disk_over_commit):
        if self.client.can_send_version('2.38'):
            version = '2.38'
        else:
            version = '2.0'
            instance = jsonutils.to_primitive(
                    objects_base.obj_to_primitive(instance))
        cctxt = self.client.prepare(server=destination, version=version)
        #这里调用call方法实现在一个主题topic上发送一条远程消息,实现在目标主机上进行检测是否可以进行实时迁移,并等待响应。调用nova/compute/manager.py:check_can_live_migrate_destination()执行具体的检测
        return cctxt.call(ctxt, 'check_can_live_migrate_destination',
                          instance=instance,
                          block_migration=block_migration,
                          disk_over_commit=disk_over_commit)


3.2 nova/conductor/tasks/live_migration.py :_check_requested_destination()函数进行目的虚机的状态检测

    def _check_requested_destination(self):
        self._check_destination_is_not_source() #目的虚机与源虚机是否一致
        self._check_host_is_up(self.destination) #目的虚机是否已启动
        self._check_destination_has_enough_memory() #是否有足够内存
        self._check_compatible_with_source_hypervisor(self.destination) #源与目的虚机hypervisor是否匹配
        self._call_livem_checks_on_host(self.destination) #目的虚机能否进行迁移


4.nova/compute/manager.py:live_migration()函数执行动态迁移

目的主机上发起migrate过程,调用了两个方法pre_live_migration和live_migration


    def live_migration(self, context, dest, instance,
                       block_migration=False, migrate_data=None):
        """Executing live migration.

        :param context: security context
        :param instance: instance dict
        :param dest: destination host
        :param block_migration: if true, prepare for block migration
        :param migrate_data: implementation specific params

        """
        # Create a local copy since we'll be modifying the dictionary
        migrate_data = dict(migrate_data or {})
        try:
            if block_migration:
                disk = self.driver.get_instance_disk_info(instance['name']) 
            else:
                disk = None

            pre_migration_data = self.compute_rpcapi.pre_live_migration(
                context, instance,
                block_migration, disk, dest, migrate_data) #通过compute_rpc发送一个pre_live_migration的请求给源主机的nova-compute进行动态迁移的预处理
            migrate_data['pre_live_migration_result'] = pre_migration_data

        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.exception(_('Pre live migration failed at %s'),
                              dest, instance=instance)
                self._rollback_live_migration(context, instance, dest,
                                              block_migration, migrate_data) #如遇异常,则rollback

        # Executing live migration
        # live_migration might raises exceptions, but
        # nothing must be recovered in this version.
        #根据本地的配置(compute_driver=libvirt.LibvirtDriver),所以这里的driver是nova.virt.libvirt.driver.LibvirtDriver
        self.driver.live_migration(context, instance, dest, #调用目的主机上的libvirt驱动进行实际的迁移过程
                                   self._post_live_migration,  #迁移成功,转4.1 调用nova/compute/manager.py _post_live_migration()方法
                                   self._rollback_live_migration, #迁移异常,转4.2调用nova/compute/manager.py _rollback_live_migration()
                                   block_migration, migrate_data) 

4.1  nova/compute/manager.py :post_live_migration()函数 更新数据库记录(updates floating ips,deletes old traces of image)

    @wrap_exception()
    def _post_live_migration(self, ctxt, instance_ref,
                            dest, block_migration=False, migrate_data=None):
        """Post operations for live migration.

        This method is called from live_migration
        and mainly updating database record.

        :param ctxt: security context
        :param instance_ref: nova.db.sqlalchemy.models.Instance
        :param dest: destination host
        :param block_migration: if true, prepare for block migration
        :param migrate_data: if not None, it is a dict which has data
        required for live migration without shared storage

        """
        LOG.info(_('_post_live_migration() is started..'),
                 instance=instance_ref)

        # Cleanup source host post live-migration
        block_device_info = self._get_instance_volume_block_device_info(
                            ctxt, instance_ref)
        self.driver.post_live_migration(ctxt, instance_ref, block_device_info)

        # Detaching volumes.
        connector = self.driver.get_volume_connector(instance_ref)
        for bdm in self._get_instance_volume_bdms(ctxt, instance_ref):
            # NOTE(vish): We don't want to actually mark the volume
            #             detached, or delete the bdm, just remove the
            #             connection from this host.

            # remove the volume connection without detaching from hypervisor
            # because the instance is not running anymore on the current host
            self.volume_api.terminate_connection(ctxt, bdm['volume_id'],
                                                 connector)

        # Releasing vlan.
        # (not necessary in current implementation?)

        network_info = self._get_instance_nw_info(ctxt, instance_ref)

        self._notify_about_instance_usage(ctxt, instance_ref,
                                          "live_migration._post.start",
                                          network_info=network_info)
        # Releasing security group ingress rule.
        self.driver.unfilter_instance(instance_ref,
                                      network_info)

        migration = {'source_compute': self.host,
                     'dest_compute': dest, }
        self.conductor_api.network_migrate_instance_start(ctxt,
                                                          instance_ref,
                                                          migration)

        # Define domain at destination host, without doing it,
        # pause/suspend/terminate do not work.
        self.compute_rpcapi.post_live_migration_at_destination(ctxt,
                instance_ref, block_migration, dest) #转到4.1.1

        # No instance booting at source host, but instance dir
        # must be deleted for preparing next block migration
        # must be deleted for preparing next live migration w/o shared storage
        is_shared_storage = True
        if migrate_data:
            is_shared_storage = migrate_data.get('is_shared_storage', True)
        if block_migration or not is_shared_storage:
            self.driver.destroy(instance_ref, network_info)
        else:
            # self.driver.destroy() usually performs  vif unplugging
            # but we must do it explicitly here when block_migration
            # is false, as the network devices at the source must be
            # torn down
            self.driver.unplug_vifs(instance_ref, network_info)

        # NOTE(tr3buchet): tear down networks on source host
        self.network_api.setup_networks_on_host(ctxt, instance_ref,
                                                self.host, teardown=True)

        self._notify_about_instance_usage(ctxt, instance_ref,
                                          "live_migration._post.end",
                                          network_info=network_info)
        LOG.info(_('Migrating instance to %s finished successfully.'),
                 dest, instance=instance_ref)
        LOG.info(_("You may see the error \"libvirt: QEMU error: "
                   "Domain not found: no domain with matching name.\" "
                   "This error can be safely ignored."),
                 instance=instance_ref)

        if CONF.vnc_enabled or CONF.spice.enabled:
            if CONF.cells.enable:
                self.cells_rpcapi.consoleauth_delete_tokens(ctxt,
                        instance_ref['uuid'])
            else:
                self.consoleauth_rpcapi.delete_tokens_for_instance(ctxt,
                        instance_ref['uuid'])

4.1.1 nova/compute/manager.py  or  nova/compute/rpcapi.py  : post_live_migration_at_destination()动态迁移成功的善后处理

    def post_live_migration_at_destination(self, context, instance,
                                           block_migration=False):
        """Post operations for live migration .

        :param context: security context
        :param instance: Instance dict
        :param block_migration: if true, prepare for block migration

        """
        LOG.info(_('Post operation of migration started'),
                 instance=instance)

        # NOTE(tr3buchet): setup networks on destination host
        #                  this is called a second time because
        #                  multi_host does not create the bridge in
        #                  plug_vifs
        self.network_api.setup_networks_on_host(context, instance,
                                                         self.host) #sets up networking
        migration = {'source_compute': instance['host'],
                     'dest_compute': self.host, }
        self.conductor_api.network_migrate_instance_finish(context,
                                                           instance,
                                                           migration) #sets task as complete

        network_info = self._get_instance_nw_info(context, instance)
        self._notify_about_instance_usage(
                     context, instance, "live_migration.post.dest.start",
                     network_info=network_info)
        block_device_info = self._get_instance_volume_block_device_info(
                            context, instance)

        self.driver.post_live_migration_at_destination(context, instance,
                                            network_info,
                                            block_migration, block_device_info)
        # Restore instance state
        current_power_state = self._get_power_state(context, instance)
        node_name = None
        try:
            compute_node = self._get_compute_info(context, self.host)
            node_name = compute_node['hypervisor_hostname']
        except exception.NotFound:
            LOG.exception(_('Failed to get compute_info for %s') % self.host)
        finally:
            instance = self._instance_update(context, instance['uuid'],
                    host=self.host, power_state=current_power_state,
                    vm_state=vm_states.ACTIVE, task_state=None,
                    expected_task_state=task_states.MIGRATING,
                    node=node_name)

        # NOTE(vish): this is necessary to update dhcp
        self.network_api.setup_networks_on_host(context, instance, self.host)
        self._notify_about_instance_usage(
                     context, instance, "live_migration.post.dest.end",
                     network_info=network_info)

4.2 nova/compute/manager.py:_rollback_live_migration()函数处理动态迁移失败的动作

    @wrap_exception()
    def _rollback_live_migration(self, context, instance,
                                 dest, block_migration, migrate_data=None):
        """Recovers Instance/volume state from migrating -> running.

        :param context: security context
        :param instance: nova.db.sqlalchemy.models.Instance
        :param dest:
            This method is called from live migration src host.
            This param specifies destination host.
        :param block_migration: if true, prepare for block migration
        :param migrate_data:
            if not none, contains implementation specific data.

        """
        host = instance['host']
        instance = self._instance_update(context, instance['uuid'],
                host=host, vm_state=vm_states.ACTIVE,
                task_state=None, expected_task_state=task_states.MIGRATING) #updates DB

        # NOTE(tr3buchet): setup networks on source host (really it's re-setup)
        self.network_api.setup_networks_on_host(context, instance, self.host) #sorts out volumes

        for bdm in self._get_instance_volume_bdms(context, instance):  #sorts out  networks
            volume_id = bdm['volume_id']
            self.compute_rpcapi.remove_volume_connection(context, instance,
                    volume_id, dest)

        self._notify_about_instance_usage(context, instance,
                                          "live_migration._rollback.start")

        # Block migration needs empty image at destination host
        # before migration starts, so if any failure occurs,
        # any empty images has to be deleted.
        # Also Volume backed live migration w/o shared storage needs to delete
        # newly created instance-xxx dir on the destination as a part of its
        # rollback process
        is_volume_backed = False
        is_shared_storage = True
        if migrate_data:
            is_volume_backed = migrate_data.get('is_volume_backed', False)
            is_shared_storage = migrate_data.get('is_shared_storage', True)
        if block_migration or (is_volume_backed and not is_shared_storage):
            self.compute_rpcapi.rollback_live_migration_at_destination(context,
                    instance, dest)  #调用nova/compute/rpcapi.py:rollback_live_migration_at_destination()做一些清理工作

        self._notify_about_instance_usage(context, instance,
                                          "live_migration._rollback.end")


5.nova/compute/manager.py:pre_live_migration() 目的主机上的预处理

    @wrap_exception()
    def pre_live_migration(self, context, instance,
                           block_migration=False, disk=None,
                           migrate_data=None):
        """Preparations for live migration at dest host.

        :param context: security context
        :param instance: dict of instance data
        :param block_migration: if true, prepare for block migration
        :param migrate_data : if not None, it is a dict which holds data
        required for live migration without shared storage.

        """
        #取出虚拟机实例的块设备信息
        block_device_info = self._get_instance_volume_block_device_info(
                            context, instance, refresh_conn_info=True)
        # 取出虚拟机实例的网络配置信息(实际调用的是network_api向源主机请求数据)
        network_info = self._get_instance_nw_info(context, instance)
        self._notify_about_instance_usage(
                     context, instance, "live_migration.pre.start",
                     network_info=network_info)

        # TODO(tr3buchet): figure out how on the earth this is necessary
        fixed_ips = network_info.fixed_ips()
        if not fixed_ips:
            raise exception.FixedIpNotFoundForInstance(
                                       instance_uuid=instance['uuid'])

        pre_live_migration_data = self.driver.pre_live_migration(context,
                                       instance,
                                       block_device_info,
                                       network_info,
                                       disk,
                                       migrate_data)
        # NOTE(tr3buchet): setup networks on destination host
        self.network_api.setup_networks_on_host(context, instance,
                                                         self.host)

        # Creating filters to hypervisors and firewalls.
        # An example is that nova-instance-instance-xxx,
        # which is written to libvirt.xml(Check "virsh nwfilter-list")
        # This nwfilter is necessary on the destination host.
        # In addition, this method is creating filtering rule
        # onto destination host.
        # 创建hypervisors和firewalls相关的过滤器
        self.driver.ensure_filtering_rules_for_instance(instance,
                                            network_info)

        self._notify_about_instance_usage(
                     context, instance, "live_migration.pre.end",
                     network_info=network_info)

        return pre_live_migration_data





相关内容

    暂无相关文章