这里简单对nova resize的代码调用简单记录,写得不好的地方还请见谅!


测试环境:OpenStack Liberty(rdo)


# compute-1计算节点下一台test-cirros-1的虚拟机

[root@controller1 ~(keystone_admin)]# nova show test-cirros-1

+--------------------------------------+------------------------------------------------------------+

| Property                             | Value                                                      |

+--------------------------------------+------------------------------------------------------------+

| NetworkA network                     | 192.168.0.26                                               |

| OS-DCF:diskConfig                    | MANUAL                                                     |

| OS-EXT-AZ:availability_zone          | nova                                                       |

| OS-EXT-SRV-ATTR:host                 | compute-1                                                  |

| OS-EXT-SRV-ATTR:hostname             | test-cirros-1                                              |

| OS-EXT-SRV-ATTR:hypervisor_hostname  | compute-1                                                  |

| OS-EXT-SRV-ATTR:instance_name        | instance-00000018                                          |

| OS-EXT-SRV-ATTR:kernel_id            |                                                            |

| OS-EXT-SRV-ATTR:launch_index         | 0                                                          |

| OS-EXT-SRV-ATTR:ramdisk_id           |                                                            |

| OS-EXT-SRV-ATTR:reservation_id       | r-a30b0e7o                                                 |

| OS-EXT-SRV-ATTR:root_device_name     | /dev/vda                                                   |

| OS-EXT-SRV-ATTR:user_data            | -                                                          |

| OS-EXT-STS:power_state               | 1                                                          |

| OS-EXT-STS:task_state                | -                                                          |

| OS-EXT-STS:vm_state                  | active                                                     |

| OS-SRV-USG:launched_at               | 2016-04-17T13:21:54.000000                                 |

| OS-SRV-USG:terminated_at             | -                                                          |

| accessIPv4                           |                                                            |

| accessIPv6                           |                                                            |

| config_drive                         |                                                            |

| created                              | 2016-04-17T12:36:38Z                                       |

| flavor                               | m1.tiny (1)                                                |

| hostId                               | f6f5e4c5991491de79426865084507060287154e125bc2657a21fd23   |

| id                                   | 21015376-8c4f-4fad-a834-245b57f23dcc                       |

| image                                | cirros-0.3.4-x86_64 (1acb04d0-a910-45e4-aad3-5e6686fd1a91) |

| key_name                             | -                                                          |

| locked                               | False                                                      |

| metadata                             | {}                                                         |

| name                                 | test-cirros-1                                              |

| os-extended-volumes:volumes_attached | []                                                         |

| progress                             | 0                                                          |

| security_groups                      | default                                                    |

| status                               | ACTIVE                                                     |

| tenant_id                            | 52b09594cf604d45ae3a12e44da8eac9                           |

| updated                              | 2016-04-17T13:21:55Z                                       |

| user_id                              | 082ef596bad842d180cc5f9326b1fd35                           |

+--------------------------------------+------------------------------------------------------------+


/usr/lib/python2.7/site-packages/nova/compute/api.py(2621)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):
        。。。
        self._check_auto_disk_config(instance, **extra_instance_updates)

        current_instance_type = instance.get_flavor()

        # If flavor_id is not provided, only migrate the instance.
        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 (new_instance_type.get('root_gb') < current_instance_type.get('root_gb')):  # 这个是我加的,防止resize到disk小的flavor。
                reason = _('Resize to smaller disk flavor is not allowed.')
                raise exception.CannotResizeDisk(reason=reason)
        。。。。。。  
        
        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': []}
        if not CONF.allow_resize_to_same_host:     # 是否enable allow_resize_to_same_host,还是走调度
            filter_properties['ignore_hosts'].append(instance.host)       
       。。。
        scheduler_hint = {'filter_properties': filter_properties}
        self.compute_task_api.resize_instance(context, instance,      # self.compute_task_api=conductor.ComputeTaskAPI()
                extra_instance_updates, scheduler_hint=scheduler_hint,
                flavor=new_instance_type,
                reservations=quotas.reservations or [],
                clean_shutdown=clean_shutdown)


虚拟机状态发生改变

[root@controller1 ~(keystone_admin)]# nova list
+--------------------------------------+---------------+--------+-------------+-------------+-----------------------+
| ID                                   | Name          | Status | Task State  | Power State | Networks              |
+--------------------------------------+---------------+--------+-------------+-------------+-----------------------+
| 21015376-8c4f-4fad-a834-245b57f23dcc | test-cirros-1 | RESIZE | resize_prep | Running     | NetworkA=192.168.0.26 |
+--------------------------------------+---------------+--------+-------------+-------------+-----------------------+


进入conductor的resize_instance function

/usr/lib/python2.7/site-packages/nova/conductor/api.py
189 class ComputeTaskAPI(object):
190     """ComputeTask API that queues up compute tasks for nova-conductor."""
191
192     def __init__(self):
193         self.conductor_compute_rpcapi = rpcapi.ComputeTaskAPI()
194
195     def resize_instance(self, context, instance, extra_instance_updates,
196                         scheduler_hint, flavor, reservations,
197                         clean_shutdown=True):
198         # NOTE(comstud): 'extra_instance_updates' is not used here but is
199         # needed for compatibility with the cells_rpcapi version of this
200         # method.
201         self.conductor_compute_rpcapi.migrate_server(              # rpc操作,走消息队列
202             context, instance, scheduler_hint, live=False, rebuild=False,      # live是False,冷迁移
203             flavor=flavor, block_migration=None, disk_over_commit=None,
204             reservations=reservations, clean_shutdown=clean_shutdown)


/usr/lib/python2.7/site-packages/nova/conductor/rpcapi.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):
        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,
              'request_spec': request_spec,
              }
        version = '1.13'
        if not self.client.can_send_version(version):
            del kw['request_spec']
            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 resize实际上是冷迁移的一种

/usr/lib/python2.7/site-packages/nova/conductor/manager.py(551)migrate_server()
def migrate_server(self, context, instance, scheduler_hint, live, rebuild,
            flavor, block_migration, disk_over_commit, reservations=None,
            clean_shutdown=True, request_spec=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',
                                             instance_uuid):
                self._cold_migrate(context, instance, flavor,
                                   scheduler_hint['filter_properties'],
                                   reservations, clean_shutdown)
        else:
            raise NotImplementedError()
            

# 冷迁移function
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()


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)


resize的prepare

/usr/lib/python2.7/site-packages/nova/conductor/tasks/migrate.py
class MigrationTask(base.TaskBase):
    def __init__(self, context, instance, flavor, filter_properties,
                 request_spec, reservations, clean_shutdown, compute_rpcapi,
                 scheduler_client):
        super(MigrationTask, self).__init__(context, instance)
        self.clean_shutdown = clean_shutdown
        self.request_spec = request_spec
        self.reservations = reservations
        self.filter_properties = filter_properties
        self.flavor = flavor
        self.quotas = None

        self.compute_rpcapi = compute_rpcapi
        self.scheduler_client = scheduler_client

    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)
        # TODO(sbauza): Hydrate here the object until we modify the
        # scheduler.utils methods to directly use the RequestSpec object
        spec_obj = objects.RequestSpec.from_primitives(
            self.context, self.request_spec, self.filter_properties)
        hosts = self.scheduler_client.select_destinations(
            self.context, spec_obj)
        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)

# 又是rpc
/usr/lib/python2.7/site-packages/nova/compute/rpcapi.py
 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)
        
        
# prep_resize function
/usr/lib/python2.7/site-packages/nova/compute/manager.py
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 = 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)
                                  
# _prep_resize function
def _prep_resize(self, context, image, instance, instance_type,
            quotas, request_spec, filter_properties, node,
            clean_shutdown=True):

        if not filter_properties:
            filter_properties = {}

        if not instance.host:
            self._set_instance_obj_error_state(context, instance)
            msg = _('Instance has no source host')
            raise exception.MigrationError(reason=msg)

        same_host = instance.host == self.host
        # if the flavor IDs match, it's migrate; otherwise resize
        if same_host and instance_type.id == instance['instance_type_id']:
            # check driver whether support migrate to same host
            if not self.driver.capabilities['supports_migrate_to_same_host']:
                raise exception.UnableToMigrateToSelf(
                    instance_id=instance.uuid, host=self.host)

        # NOTE(danms): Stash the new instance_type to avoid having to
        # look it up in the database later
        instance.new_flavor = instance_type
        # NOTE(mriedem): Stash the old vm_state so we can set the
        # resized/reverted instance back to the same state later.
        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', {})
        rt = self._get_resource_tracker(node)
        with rt.resize_claim(context, instance, instance_type,
                             image_meta=image, limits=limits) as claim:
            LOG.info(_LI('Migrating'), context=context, instance=instance)
            self.compute_rpcapi.resize_instance(                           # 
                    context, instance, claim.migration, image,
                    instance_type, quotas.reservations,
                    clean_shutdown)


又是rpc

/usr/lib/python2.7/site-packages/nova/compute/rpcapi.py
def resize_instance(self, ctxt, instance, migration, image, instance_type,
                        reservations=None, clean_shutdown=True):
        msg_args = {'instance': instance, 'migration': migration,
                    'image': image, 'reservations': reservations,
                    'instance_type': instance_type,
                    'clean_shutdown': clean_shutdown,
        }
        version = '4.1'
        if not self.client.can_send_version(version):
            msg_args['instance_type'] = objects_base.obj_to_primitive(
                                            instance_type)
            version = '4.0'
        cctxt = self.client.prepare(server=_compute_host(None, instance),
                version=version)
        cctxt.cast(ctxt, 'resize_instance', **msg_args)


 

/usr/lib/python2.7/site-packages/nova/compute/manager.py(3765)resize_instance()
def resize_instance(self, context, instance, image,
                        reservations, migration, instance_type,
                        clean_shutdown):
        """Starts the migration of a running instance to another host."""
        quotas = objects.Quotas.from_reservations(context,
                                                  reservations,
                                                  instance=instance)
        with self._error_out_instance_on_exception(context, instance,
                                                   quotas=quotas):
            # TODO(chaochin) Remove this until v5 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 instance_type or
                not isinstance(instance_type, objects.Flavor)):
                instance_type = objects.Flavor.get_by_id(
                    context, migration['new_instance_type_id'])
            network_info = self.network_api.get_instance_nw_info(context,
                                                                 instance)
            migration.status = 'migrating'
            with migration.obj_as_admin():
                migration.save()
            instance.task_state = task_states.RESIZE_MIGRATING
            instance.save(expected_task_state=task_states.RESIZE_PREP)
            self._notify_about_instance_usage(
                context, instance, "resize.start", network_info=network_info)
            bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
                    context, instance.uuid)
            block_device_info = self._get_instance_block_device_info(
                                context, instance, bdms=bdms)
            timeout, retry_interval = self._get_power_off_values(context,
                                            instance, clean_shutdown)
            disk_info = self.driver.migrate_disk_and_power_off(        # virt/driver.py
                    context, instance, migration.dest_host,
                    instance_type, network_info,
                    block_device_info,
                    timeout, retry_interval)
            self._terminate_volume_connections(context, instance, bdms)  # 断开volume的连接
            migration_p = obj_base.obj_to_primitive(migration)
            self.network_api.migrate_instance_start(context,
                                                    instance,
                                                    migration_p)
            migration.status = 'post-migrating'
            with migration.obj_as_admin():
                migration.save()
            instance.host = migration.dest_compute
            instance.node = migration.dest_node
            instance.task_state = task_states.RESIZE_MIGRATED
            instance.save(expected_task_state=task_states.RESIZE_MIGRATING)
            self.compute_rpcapi.finish_resize(context, instance,             # rpc操作
                    migration, image, disk_info,
                    migration.dest_compute, reservations=quotas.reservations)
            self._notify_about_instance_usage(context, instance, "resize.end",
                                              network_info=network_info)
            self.instance_events.clear_events_for_instance(instance)


虚拟机状态也发生了改变

[root@controller1 ~(keystone_admin)]# nova list

+--------------------------------------+---------------+--------+------------------+-------------+-----------------------+

| ID                                   | Name          | Status | Task State       | Power State | Networks              |

+--------------------------------------+---------------+--------+------------------+-------------+-----------------------+

| 21015376-8c4f-4fad-a834-245b57f23dcc | test-cirros-1 | RESIZE | resize_migrating | Running     | NetworkA=192.168.0.26 |

+--------------------------------------+---------------+--------+------------------+-------------+-----------------------+


[root@controller1 ~(keystone_admin)]# nova list

+--------------------------------------+---------------+--------+-----------------+-------------+-----------------------+

| ID                                   | Name          | Status | Task State      | Power State | Networks              |

+--------------------------------------+---------------+--------+-----------------+-------------+-----------------------+

| 21015376-8c4f-4fad-a834-245b57f23dcc | test-cirros-1 | RESIZE | resize_migrated | Running     | NetworkA=192.168.0.26 |

+--------------------------------------+---------------+--------+-----------------+-------------+-----------------------+


instance.save(expected_task_state=task_states.RESIZE_MIGRATING)  # save写库操作


rpc操作

/usr/lib/python2.7/site-packages/nova/compute/rpcapi.py
def finish_resize(self, ctxt, instance, migration, image, disk_info,
            host, reservations=None):
        version = '4.0'
        cctxt = self.client.prepare(server=host, version=version)
        cctxt.cast(ctxt, 'finish_resize',
                   instance=instance, migration=migration,
                   image=image, disk_info=disk_info, reservations=reservations)


# resize的finish过程

/usr/lib/python2.7/site-packages/nova/compute/manager.py
def finish_resize(self, context, disk_info, image, instance,
                      reservations, migration):
        """Completes the migration process.
        Sets up the newly transferred disk and turns on the instance at its
        new host machine.
        """
        quotas = objects.Quotas.from_reservations(context,
                                                  reservations,
                                                  instance=instance)
        try:
            image_meta = objects.ImageMeta.from_dict(image)
            self._finish_resize(context, instance, migration,
                                disk_info, image_meta)
            quotas.commit()
        except Exception:
            LOG.exception(_LE('Setting instance vm_state to ERROR'),
                          instance=instance)
            with excutils.save_and_reraise_exception():
                try:
                    quotas.rollback()
                except Exception:
                    LOG.exception(_LE("Failed to rollback quota for failed "
                                      "finish_resize"),
                                  instance=instance)
                self._set_instance_obj_error_state(context, instance)
        
        
# _finish_resize function
def _finish_resize(self, context, instance, migration, disk_info,
                       image_meta):
        resize_instance = False
        old_instance_type_id = migration['old_instance_type_id']
        new_instance_type_id = migration['new_instance_type_id']
        old_instance_type = instance.get_flavor()
        # NOTE(mriedem): Get the old_vm_state so we know if we should
        # power on the instance. If old_vm_state is not set we need to default
        # to ACTIVE for backwards compatibility
        old_vm_state = instance.system_metadata.get('old_vm_state',
                                                    vm_states.ACTIVE)
        instance.old_flavor = old_instance_type

        if old_instance_type_id != new_instance_type_id:
            instance_type = instance.get_flavor('new')
            self._set_instance_info(instance, instance_type)
            for key in ('root_gb', 'swap', 'ephemeral_gb'):
                if old_instance_type[key] != instance_type[key]:
                    resize_instance = True
                    break
        instance.apply_migration_context()

        # NOTE(tr3buchet): setup networks on destination host
        self.network_api.setup_networks_on_host(context, instance,
                                                migration['dest_compute'])

        migration_p = obj_base.obj_to_primitive(migration)
        self.network_api.migrate_instance_finish(context,
                                                 instance,
                                                 migration_p)

        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._notify_about_instance_usage(
            context, instance, "finish_resize.start",
            network_info=network_info)

        block_device_info = self._get_instance_block_device_info(
                            context, instance, refresh_conn_info=True)

        # NOTE(mriedem): If the original vm_state was STOPPED, we don't
        # automatically power on the instance after it's migrated
        power_on = old_vm_state != vm_states.STOPPED

        try:
            self.driver.finish_migration(context, migration, instance,
                                         disk_info,
                                         network_info,
                                         image_meta, resize_instance,
                                         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_instance_type)

        migration.status = 'finished'
        with migration.obj_as_admin():
            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)

        self._update_scheduler_instance_info(context, instance)
        self._notify_about_instance_usage(
            context, instance, "finish_resize.end",
            network_info=network_info)


虚拟机状态又发生改变

[root@controller1 ~(keystone_admin)]# nova list

+--------------------------------------+---------------+--------+---------------+-------------+-----------------------+

| ID                                   | Name          | Status | Task State    | Power State | Networks              |

+--------------------------------------+---------------+--------+---------------+-------------+-----------------------+

| 21015376-8c4f-4fad-a834-245b57f23dcc | test-cirros-1 | RESIZE | resize_finish | Running     | NetworkA=192.168.0.26 |

+--------------------------------------+---------------+--------+---------------+-------------+-----------------------+


[root@controller1 ~(keystone_admin)]# nova list

+--------------------------------------+---------------+---------------+------------+-------------+-----------------------+

| ID                                   | Name          | Status        | Task State | Power State | Networks              |

+--------------------------------------+---------------+---------------+------------+-------------+-----------------------+

| 21015376-8c4f-4fad-a834-245b57f23dcc | test-cirros-1 | VERIFY_RESIZE | -          | Running     | NetworkA=192.168.0.26 |

+--------------------------------------+---------------+---------------+------------+-------------+-----------------------+



revert_resize、confirm_resize调用后续补充。。。