虛擬機冷遷移和resize代碼分析(二)

  上一節咱們分析了冷遷移和resize上層各自的邏輯,本節主要講解二者底層相同的代碼邏輯。其中compute_api.resize()方法會進一步調用nova/compute/api.py.resize()的方法。部分代碼和註釋以下所示:前端

@wrap_check_policy
@check_instance_lock
@check_instance_cell
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED])
def resize(self, context, instance, flavor_id=None, clean_shutdown=True,
           **extra_instance_updates):
    """Resize (ie, migrate) a running instance.

    If flavor_id is None, the process is considered a migration, keeping
    the original flavor_id. If flavor_id is not None, the instance should
    be migrated to a new host and resized to the new flavor_id.
    """
    self._check_auto_disk_config(instance, **extra_instance_updates)

    current_instance_type = instance.get_flavor()

    #若是flavor_id爲空,instance_type不變;不然使用前端傳入的flavor_id
    if not flavor_id:
        LOG.debug("flavor_id is None. Assuming migration.",
                  instance=instance)
        new_instance_type = current_instance_type
    else:
        new_instance_type = flavors.get_flavor_by_flavor_id(
                flavor_id, read_deleted="no")
        if (new_instance_type.get('root_gb') == 0 and
            current_instance_type.get('root_gb') != 0 and
            not self.is_volume_backed_instance(context, instance)):
            reason = _('Resize to zero disk flavor is not allowed.')
            raise exception.CannotResizeDisk(reason=reason)

    if not new_instance_type:
        raise exception.FlavorNotFound(flavor_id=flavor_id)

    current_instance_type_name = current_instance_type['name']
    new_instance_type_name = new_instance_type['name']
    LOG.debug("Old instance type %(current_instance_type_name)s, "
              "new instance type %(new_instance_type_name)s",
              {'current_instance_type_name': current_instance_type_name,
               'new_instance_type_name': new_instance_type_name},
              instance=instance)

    same_instance_type = (current_instance_type['id'] ==
                          new_instance_type['id'])

    # NOTE(sirp): We don't want to force a customer to change their flavor
    # when Ops is migrating off of a failed host.
    if not same_instance_type and new_instance_type.get('disabled'):
        raise exception.FlavorNotFound(flavor_id=flavor_id)

    if same_instance_type and flavor_id and self.cell_type != 'compute':
        raise exception.CannotResizeToSameFlavor()

    # ensure there is sufficient headroom for upsizes
    if flavor_id:
		#計算resize所需的資源配額,主要統計vcpu和內存
        deltas = compute_utils.upsize_quota_delta(context,
                                                  new_instance_type,
                                                  current_instance_type)
        try:
			#檢查更新項目配額
            quotas = compute_utils.reserve_quota_delta(context, deltas,
                                                       instance)
        except exception.OverQuota as exc:
            quotas = exc.kwargs['quotas']
            overs = exc.kwargs['overs']
            usages = exc.kwargs['usages']
            headroom = self._get_headroom(quotas, usages, deltas)
            (overs, reqs, total_alloweds,
             useds) = self._get_over_quota_detail(headroom, overs, quotas,
                                                  deltas)
            LOG.warning(_LW("%(overs)s quota exceeded for %(pid)s,"
                            " tried to resize instance."),
                        {'overs': overs, 'pid': context.project_id})
            raise exception.TooManyInstances(overs=overs,
                                             req=reqs,
                                             used=useds,
                                             allowed=total_alloweds)
    else:
        quotas = objects.Quotas(context=context)
	#將instance task_state狀態設置爲RESIZE_PREP
    instance.task_state = task_states.RESIZE_PREP
    instance.progress = 0
	#更新實例狀態到數據庫
    instance.update(extra_instance_updates)
    instance.save(expected_task_state=[None])

    filter_properties = {'ignore_hosts': []}
	#判斷是否能夠resize至本機
	#若是nova.conf中allow_resize_to_same_host==false,則將自身host添加到
	#ignore_hosts列表中,後續調度則不會將實例resize到本機
    if not CONF.allow_resize_to_same_host:
        filter_properties['ignore_hosts'].append(instance.host)

    if self.cell_type == 'api':
        # Commit reservations early and create migration record.
        self._resize_cells_support(context, quotas, instance,
                                   current_instance_type,
                                   new_instance_type)

    if not flavor_id:
		#更新數據庫信息,若是flavor_id爲空,表示冷遷移,不然表示resize
        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}
	#調用conductor api,經過conductor rpc將請求轉發給conductor manager 
    self.compute_task_api.resize_instance(context, instance,
            extra_instance_updates, scheduler_hint=scheduler_hint,
            flavor=new_instance_type,
            reservations=quotas.reservations or [],
            clean_shutdown=clean_shutdown)

  接着調用nova/conductor/api.py.ComputeTaskAPI.resize_instance()方法,其代碼以下:node

def resize_instance(self, context, instance, extra_instance_updates,
                    scheduler_hint, flavor, reservations,
                    clean_shutdown=True):
    # NOTE(comstud): 'extra_instance_updates' is not used here but is
    # needed for compatibility with the cells_rpcapi version of this
    # method.
    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)

  接着繼續調用nova/conductor/rpcapi.py.ComputeTaskAPI.migrate_server()方法,其代碼以下:數據庫

def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
              flavor, block_migration, disk_over_commit,
              reservations=None, clean_shutdown=True):
	#此處傳入live==false,即爲冷遷移
    kw = {'instance': instance, 'scheduler_hint': scheduler_hint,
          'live': live, 'rebuild': rebuild, 'flavor': flavor,
          'block_migration': block_migration,
          'disk_over_commit': disk_over_commit,
          'reservations': reservations,
          'clean_shutdown': clean_shutdown}
    version = '1.11'
    if not self.client.can_send_version(version):
        del kw['clean_shutdown']
        version = '1.10'
    if not self.client.can_send_version(version):
        kw['flavor'] = objects_base.obj_to_primitive(flavor)
        version = '1.6'
    if not self.client.can_send_version(version):
        kw['instance'] = jsonutils.to_primitive(
                objects_base.obj_to_primitive(instance))
        version = '1.4'
    cctxt = self.client.prepare(version=version)
    return cctxt.call(context, 'migrate_server', **kw)

  nova-conductor會收到該請求,根據路由映射,該請求會遞交給nova/couductor/manager.py.ComputeTaskManager.migrate_server()去處理。其代碼和註釋以下:json

@messaging.expected_exceptions(exception.NoValidHost,
                               exception.ComputeServiceUnavailable,
                               exception.InvalidHypervisorType,
                               exception.InvalidCPUInfo,
                               exception.UnableToMigrateToSelf,
                               exception.DestinationHypervisorTooOld,
                               exception.InvalidLocalStorage,
                               exception.InvalidSharedStorage,
                               exception.HypervisorUnavailable,
                               exception.InstanceInvalidState,
                               exception.MigrationPreCheckError,
                               exception.LiveMigrationWithOldNovaNotSafe,
                               exception.UnsupportedPolicyException)
def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
        flavor, block_migration, disk_over_commit, reservations=None,
        clean_shutdown=True):
    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)
	#非熱遷移、非重建且flavor,也即resize或者是冷遷移,下面二者開始執行相同的邏輯代碼
    elif not live and not rebuild and flavor:
        instance_uuid = instance.uuid
        with compute_utils.EventReporter(context, 'cold_migrate',
                                         instance_uuid):
            self._cold_migrate(context, instance, flavor,
                               scheduler_hint['filter_properties'],
                               reservations, clean_shutdown)
    else:
        raise NotImplementedError()

     接着調用_cold_migrate()方法,其代碼和註釋以下:api

def _cold_migrate(self, context, instance, flavor, filter_properties,
                  reservations, clean_shutdown):
    image = utils.get_image_from_system_metadata(
        instance.system_metadata)

    request_spec = scheduler_utils.build_request_spec(
        context, image, [instance], instance_type=flavor)
    task = self._build_cold_migrate_task(context, instance, flavor,
                                         filter_properties, request_spec,
                                         reservations, clean_shutdown)
    try:
        task.execute()
    except exception.NoValidHost as ex:
        vm_state = instance.vm_state
        if not vm_state:
            vm_state = vm_states.ACTIVE
        updates = {'vm_state': vm_state, 'task_state': None}
        self._set_vm_state_and_notify(context, instance.uuid,
                                      'migrate_server',
                                      updates, ex, request_spec)

        # if the flavor IDs match, it's migrate; otherwise resize
        if flavor.id == instance.instance_type_id:
            msg = _("No valid host found for cold migrate")
        else:
            msg = _("No valid host found for resize")
        raise exception.NoValidHost(reason=msg)
    except exception.UnsupportedPolicyException as ex:
        with excutils.save_and_reraise_exception():
            vm_state = instance.vm_state
            if not vm_state:
                vm_state = vm_states.ACTIVE
            updates = {'vm_state': vm_state, 'task_state': None}
            self._set_vm_state_and_notify(context, instance.uuid,
                                          'migrate_server',
                                          updates, ex, request_spec)
    except Exception as ex:
        with excutils.save_and_reraise_exception():
            updates = {'vm_state': instance.vm_state,
                       'task_state': None}
            self._set_vm_state_and_notify(context, instance.uuid,
                                          'migrate_server',
                                          updates, ex, request_spec)

  接着調用_build_cold_migrate_task()方法,其代碼和註釋以下:app

def _build_cold_migrate_task(self, context, instance, flavor,
                             filter_properties, request_spec, reservations,
                             clean_shutdown):
    return migrate.MigrationTask(context, instance, flavor,
                                 filter_properties, request_spec,
                                 reservations, clean_shutdown,
                                 self.compute_rpcapi,
                                 self.scheduler_client)

  後面會進一步調用nova/couductor/tasks/migrate.py.MigrationTask._excute()方法。其代碼和註釋以下:dom

def _execute(self):
    image = self.request_spec.get('image')
    self.quotas = objects.Quotas.from_reservations(self.context,
                                                   self.reservations,
                                                   instance=self.instance)
    scheduler_utils.setup_instance_group(self.context, self.request_spec,
                                         self.filter_properties)
    scheduler_utils.populate_retry(self.filter_properties,
                                   self.instance.uuid)
	
		'''發送同步消息給nova-scheduler,選取用於遷移雲主機的主機
        接口調用以下:SchedulerClient -> SchedulerQueryClient -
        > SchedulerAPI'''
    hosts = self.scheduler_client.select_destinations(
        self.context, self.request_spec, self.filter_properties)
	#從hosts中選擇第一個host進行冷遷移(第一個即爲權重最大者)
    host_state = hosts[0]
	scheduler_utils.populate_filter_properties(self.filter_properties,
                                               host_state)
    # context is not serializable
    self.filter_properties.pop('context', None)

    (host, node) = (host_state['host'], host_state['nodename'])
    self.compute_rpcapi.prep_resize(
        self.context, image, self.instance, self.flavor, host,
        self.reservations, request_spec=self.request_spec,
        filter_properties=self.filter_properties, node=node,
        clean_shutdown=self.clean_shutdown)

  接下來繼續調用nova\scheduler\rpcapi.py.select_destinations()方法,其代碼和註釋以下:ide

def select_destinations(self, ctxt, request_spec, filter_properties):
    cctxt = self.client.prepare(version='4.0')
    return cctxt.call(ctxt, 'select_destinations',
        request_spec=request_spec, filter_properties=filter_properties)

  接下來進一步調用nova/scheduler/manager.py.SchedulerManager.select_destinations()方法。其代碼和註釋以下:ui

def select_destinations(self, context, request_spec, filter_properties):
    """Returns destinations(s) best suited for this request_spec and
    filter_properties.

    The result should be a list of dicts with 'host', 'nodename' and
    'limits' as keys.
    """
    dests = self.driver.select_destinations(context, request_spec,
        filter_properties)
    return jsonutils.to_primitive(dests)

  此時要注意,scheduler_driver的類型,該參數是在nova.conf配置的,默認採用nova.scheduler.filter_scheduler.FilterScheduler。故應該調用nova/scheduler/filter_scheduler.py.select_destinations()方法,其代碼和註釋以下:this

def select_destinations(self, context, request_spec, filter_properties):
    """Selects a filtered set of hosts and nodes."""
    # TODO(sbauza): Change the select_destinations method to accept a
    # RequestSpec object directly (and add a new RPC API method for passing
    # a RequestSpec object over the wire)
    spec_obj = objects.RequestSpec.from_primitives(context,
                                                   request_spec,
                                                   filter_properties)
    self.notifier.info(
        context, 'scheduler.select_destinations.start',
        dict(request_spec=spec_obj.to_legacy_request_spec_dict()))

    num_instances = spec_obj.num_instances
    selected_hosts = self._schedule(context, spec_obj)

    # Couldn't fulfill the request_spec
    if len(selected_hosts) < num_instances:
        # NOTE(Rui Chen): If multiple creates failed, set the updated time
        # of selected HostState to None so that these HostStates are
        # refreshed according to database in next schedule, and release
        # the resource consumed by instance in the process of selecting
        # host.
        for host in selected_hosts:
            host.obj.updated = None

        # Log the details but don't put those into the reason since
        # we don't want to give away too much information about our
        # actual environment.
        LOG.debug('There are %(hosts)d hosts available but '
                  '%(num_instances)d instances requested to build.',
                  {'hosts': len(selected_hosts),
                   'num_instances': num_instances})

        reason = _('There are not enough hosts available.')
        raise exception.NoValidHost(reason=reason)

    dests = [dict(host=host.obj.host, nodename=host.obj.nodename,
                  limits=host.obj.limits) for host in selected_hosts]

    self.notifier.info(
        context, 'scheduler.select_destinations.end',
        dict(request_spec=spec_obj.to_legacy_request_spec_dict()))
    return dests

其中_schedule()方法以下:

def _schedule(self, context, spec_obj):
    """Returns a list of hosts that meet the required specs,
    ordered by their fitness.
    """
    elevated = context.elevated()
    #加載nova.conf文件中的過濾選項
	#用戶能夠經過nova.conf中的scheduler_json_config_location
	#參數指定一個包含過濾參數的json格式的過濾文件
    config_options = self._get_configuration_options()

    # Find our local list of acceptable hosts by repeatedly
    # filtering and weighing our options. Each time we choose a
    # host, we virtually consume resources on it so subsequent
    # selections can adjust accordingly.

    # Note: remember, we are using an iterator here. So only
    # traverse this list once. This can bite you if the hosts
    # are being scanned in a filter or weighing function.
	
    #獲取全部的活動主機
	hosts = self._get_all_host_states(elevated)

    selected_hosts = []
    num_instances = spec_obj.num_instances
    # TODO(sbauza): Modify the interfaces for HostManager and filters to
    # accept the RequestSpec object directly (in a later patch hopefully)
    filter_properties = spec_obj.to_legacy_filter_properties_dict()
    # NOTE(sbauza): Adding temporarly some keys since filters are
    # directly using it - until we provide directly RequestSpec
    filter_properties.update(
        {'request_spec': spec_obj.to_legacy_request_spec_dict(),
         'instance_type': spec_obj.flavor})
    # TODO(sbauza): Adding two keys not used in-tree but which will be
    # provided as non-fields for the RequestSpec once we provide it to the
    # filters
	#更新過濾參數
    filter_properties.update({'context': context,
                              'config_options': config_options})
    for num in range(num_instances):
        # Filter local hosts based on requirements ...
		# 返回知足過濾條件的host,所使用的過濾器能夠經過nova.conf文件中
        #的scheduler_default_filters選項指定,相關的過濾器代碼位於
        #nova/scheduler/filters
        hosts = self.host_manager.get_filtered_hosts(hosts,
                filter_properties, index=num)
        if not hosts:
            # Can't get any more locally.
            break

        LOG.debug("Filtered %(hosts)s", {'hosts': hosts})
        #經過權重過濾器進一步過濾host,返回一個按照權重降序排列的host列
        #表,權重過濾器能夠經過nova.conf文件中
        #的scheduler_weight_classes選項指定,相關的過濾器代碼位於nova/scheduler/weights
        weighed_hosts = self.host_manager.get_weighed_hosts(hosts,
                filter_properties)

        LOG.debug("Weighed %(hosts)s", {'hosts': weighed_hosts})

        scheduler_host_subset_size = CONF.scheduler_host_subset_size
        if scheduler_host_subset_size > len(weighed_hosts):
            scheduler_host_subset_size = len(weighed_hosts)
        if scheduler_host_subset_size < 1:
            scheduler_host_subset_size = 1
        #設置host隨機選擇範圍,默認選擇第一個
        chosen_host = random.choice(
            weighed_hosts[0:scheduler_host_subset_size])
        LOG.debug("Selected host: %(host)s", {'host': chosen_host})
        selected_hosts.append(chosen_host)

        # Now consume the resources so the filter/weights
        # will change for the next instance.
        chosen_host.obj.consume_from_request(spec_obj)
        if filter_properties.get('group_updated') is True:
            filter_properties['group_hosts'].add(chosen_host.obj.host)
    return selected_hosts

  接着回到_excute()方法,查看compute_rpcapi.prep_resize()方法,調用nova/compute/rpcapi.py.ComputeAPI.prep_resize()方法其代碼以下:

def prep_resize(self, ctxt, image, instance, instance_type, host,
                reservations=None, request_spec=None,
                filter_properties=None, node=None,
                clean_shutdown=True):
    image_p = jsonutils.to_primitive(image)
    msg_args = {'instance': instance,
                'instance_type': instance_type,
                'image': image_p,
                'reservations': reservations,
                'request_spec': request_spec,
                'filter_properties': filter_properties,
                'node': node,
                'clean_shutdown': clean_shutdown}
    version = '4.1'
    if not self.client.can_send_version(version):
        version = '4.0'
        msg_args['instance_type'] = objects_base.obj_to_primitive(
                                        instance_type)
    cctxt = self.client.prepare(server=host, version=version)
    cctxt.cast(ctxt, 'prep_resize', **msg_args)

  接着,將消息傳給nova/compute/manager.py.ComputeManager.prep_resize()方法,該節點爲目的計算節點,其代碼和註釋以下:

@wrap_exception()
@reverts_task_state
@wrap_instance_event
@wrap_instance_fault
def prep_resize(self, context, image, instance, instance_type,
                reservations, request_spec, filter_properties, node,
                clean_shutdown):
    """Initiates the process of moving a running instance to another host.

    Possibly changes the RAM and disk size in the process.

    """
    if node is None:
		#假如上述調度中沒有選擇傳遞node,則會再次選擇一個node
        node = self.driver.get_available_nodes(refresh=True)[0]
        LOG.debug("No node specified, defaulting to %s", node,
                  instance=instance)

    # NOTE(melwitt): Remove this in version 5.0 of the RPC API
    # Code downstream may expect extra_specs to be populated since it
    # is receiving an object, so lookup the flavor to ensure this.
    if not isinstance(instance_type, objects.Flavor):
        instance_type = objects.Flavor.get_by_id(context,
                                                 instance_type['id'])

    quotas = objects.Quotas.from_reservations(context,
                                              reservations,
                                              instance=instance)
    with self._error_out_instance_on_exception(context, instance,
                                               quotas=quotas):
        compute_utils.notify_usage_exists(self.notifier, context, instance,
                                          current_period=True)
        self._notify_about_instance_usage(
                context, instance, "resize.prep.start")
        try:
            self._prep_resize(context, image, instance,
                              instance_type, quotas,
                              request_spec, filter_properties,
                              node, clean_shutdown)
        # NOTE(dgenin): This is thrown in LibvirtDriver when the
        #               instance to be migrated is backed by LVM.
        #               Remove when LVM migration is implemented.
        except exception.MigrationPreCheckError:
            raise
        except Exception:
            # try to re-schedule the resize elsewhere:
            exc_info = sys.exc_info()
            self._reschedule_resize_or_reraise(context, image, instance,
                    exc_info, instance_type, quotas, request_spec,
                    filter_properties)
        finally:
            extra_usage_info = dict(
                    new_instance_type=instance_type.name,
                    new_instance_type_id=instance_type.id)

            self._notify_about_instance_usage(
                context, instance, "resize.prep.end",
                extra_usage_info=extra_usage_info)

  後面的內容下次繼續講解。

相關文章
相關標籤/搜索