• 基于镜像卷启动的虚机快照代码分析


    基于ocata版本进行跟踪分析

    1、nova-api接受并处理请求的入口函数nova/api/openstack/compute/servers.py/ServersController._action_create_image

    nova/api/openstack/compute/servers.py
    class ServersController(wsgi.Controller):
       @wsgi.action('createImage')
        @common.check_snapshots_enabled
        @validation.schema(schema_servers.create_image, '2.0', '2.0')
        @validation.schema(schema_servers.create_image, '2.1')
        def _action_create_image(self, req, id, body):
            """Snapshot a server instance."""
            # 从req中获取请求的上下文,并验证执行权限
            context = req.environ['nova.context']
            context.can(server_policies.SERVERS % 'create_image')
            
            # 从body中解析出传递的参数,快照名称及属性信息
            entity = body["createImage"]
            image_name = common.normalize_name(entity["name"])
            metadata = entity.get('metadata', {})
    
            # Starting from microversion 2.39 we don't check quotas on createImage
            if api_version_request.is_supported(
                    req, max_version=
                    api_version_request.MAX_IMAGE_META_PROXY_API_VERSION):
                # 检查快照属性的相关配额信息
                common.check_img_metadata_properties_quota(context, metadata)
            #通过虚机uuid,从数据库中获取虚机实例的信息,返回的是一个实例对象
            instance = self._get_server(context, req, id)
            #从数据库bloack_device_mapping表里面获取该虚机所有的块设备映射信息
            bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context, instance.uuid)
    
            try:
                # 判断虚机是基于镜像启动还是基于磁盘启动
                if compute_utils.is_volume_backed_instance(context, instance,bdms):
                    context.can(server_policies.SERVERS %'create_image:allow_volume_backed')
                    #基于磁盘启动的虚机快照入口
                    image = self.compute_api.snapshot_volume_backed(
                                                           context,
                                                           instance,
                                                           image_name,
                                                           extra_properties=metadata)
                else:
                    #基于镜像启动的虚机快照入口
                    image = self.compute_api.snapshot(context,
                                                      instance,
                                                      image_name,
                                                      extra_properties=metadata)
            .........
    
            # build location of newly-created image entity
            image_id = str(image['id'])
            #根据glance配置为该镜像生成url
            image_ref = glance.generate_image_url(image_id)
    
            resp = webob.Response(status_int=202)
            resp.headers['Location'] = image_ref
            return resp

    因此可以看到,执行基于磁盘启动虚机快照时,实际走的是“ compute_api.snapshot_volume_backed ”

    nova/compute/api.py
    @profiler.trace_cls("compute_api")
    class API(base.Base):
        """API for interacting with the compute manager."""
       def snapshot_volume_backed(self, context, instance, name,
                                   extra_properties=None):
            """Snapshot the given volume-backed instance.
    
            :param instance: nova.objects.instance.Instance object
            :param name: name of the backup or snapshot
            :param extra_properties: dict of extra image properties to include
    
            :returns: the new image metadata
            """
            #获取虚机的metadata属性
            image_meta = self._initialize_instance_snapshot_metadata(instance, name, extra_properties)------s1步
            # the new image is simply a bucket of properties (particularly the
            # block device mapping, kernel and ramdisk IDs) with no image data,
            # hence the zero size
            新镜像只是一堆属性(特别是块设备映射、内核和ramdisk id),没有映像数据,因此大小为零
            image_meta['size'] = 0
            for attr in ('container_format', 'disk_format'):---清除镜像metadata属性中的 container_format,disk_format 属性
                image_meta.pop(attr, None)
            properties = image_meta['properties']
            # clean properties before filling
            # 清除properties属性里面的'block_device_mapping', 'bdm_v2', 'root_device_name'相关属性值
            for key in ('block_device_mapping', 'bdm_v2', 'root_device_name'):
                properties.pop(key, None)
            # 将实例中的‘root_device_name’属性更新到properties属性里,image_meta的最终内容如:
            # {
            #     'name': u'snapshot1',
            #     u'min_ram': u'0',
            #     u'min_disk': u'20',
            #     'is_public': False,
            #     'properties': {
            #         u'base_image_ref': u'',
            #         'root_device_name': u'/dev/vda'
            #     },
            #     'size': 0
            # }
            if instance.root_device_name:
                properties['root_device_name'] = instance.root_device_name
    
            quiesced = False
            if instance.vm_state == vm_states.ACTIVE:
                try:
                    # 判断虚拟机的状态,如果虚拟机处于active,则通过rpc通知虚拟机进入静默状态
                    self.compute_rpcapi.quiesce_instance(context, instance)
                    quiesced = True
                except (exception.InstanceQuiesceNotSupported,
                        exception.QemuGuestAgentNotEnabled,
                        exception.NovaException, NotImplementedError) as err:
                    if strutils.bool_from_string(instance.system_metadata.get(
                            'image_os_require_quiesce')):
                        raise
                    else:
                        LOG.info(_LI('Skipping quiescing instance: '
                                     '%(reason)s.'), {'reason': err},
                                 instance=instance)
            # 从数据库中获取该虚机所关联的所有块设备,结果会返回一个BlockDeviceMappingList对象
            bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context, instance.uuid)
    
            mapping = []
            #做快照的操作,虚机挂在了多少个卷设备,就要做多少次快照
            for bdm in bdms:--------s2步
                #映射关系中没有块设备,则忽略此条映射
                if bdm.no_device:
                    continue
    
                if bdm.is_volume:----这个读取的是block_device_mapping表里面的destination_type字段
                    # create snapshot based on volume_id
                    volume = self.volume_api.get(context, bdm.volume_id)----调用cinderclient去,根据卷的volume_id从数据库获取卷的详细信息---s4步
                    # NOTE(yamahata): Should we wait for snapshot creation?
                    #                 Linux LVM snapshot creation completes in
                    #                 short time, it doesn't matter for now.
                    name = _('snapshot for %s') % image_meta['name']----快照的名字进行组装,比如快照名称是snapshot1,则这里就是snapshot for snapshot1
                    LOG.debug('Creating snapshot from volume %s.', volume['id'],
                              instance=instance)
                    snapshot = self.volume_api.create_snapshot_force(------------s3步,调用cinderclient,给cinder发送强制创建快照的请求
                        context, volume['id'], name, volume['display_description'])
                    mapping_dict = block_device.snapshot_from_bdm(snapshot['id'],bdm)-----s5步
                    #过滤掉已经在数据库中存在的字段
                    mapping_dict = mapping_dict.get_image_mapping()
                else:
                    mapping_dict = bdm.get_image_mapping()
    
                mapping.append(mapping_dict)----将云主机所有的映射关系都添加到mapping中
            #通过rpc.case发送异步请求给nova-compute
            # nova-compute接收到消息后,会等到快照完成后对文件系统进行解冻(
            if quiesced:
                self.compute_rpcapi.unquiesce_instance(context, instance, mapping)
    
            # 更新云主机metadata信息中的properties信息
            if mapping:
                properties['block_device_mapping'] = mapping
                properties['bdm_v2'] = True
                """
                #到这一步时,会到添加一条记录到glance快照(镜像)数据库条目
                #(会在Dashboard的镜像面板显示一条名为snapshot1的快照记录),
                # 快照的大部分信息都拷贝至系统盘属性,这是因为卷快照是可以直接用来启动云主机的,
                # 另外'block_device_mapping'属性中包含所有的volume设备快照信息(如果有的话),
                # 每个volume设备快照信息作为一条记录,记录在image_properties数据表;
                # 
                  {
                     'name': u'snapshot1',
                     'min_ram': u'0',
                     'min_disk': u'20',
                     'is_public': False,
                     'properties': {
                             'bdm_v2': True,
                             'block_device_mapping': [{
                                      'guest_format': None,
                                      'boot_index': 0,
                                      'no_device': None,
                                      'image_id': None,
                                      'volume_id': None,
                                      'device_name': u'/dev/vda',
                                      'disk_bus': u'virtio',
                                      'volume_size': 20,
                                      'source_type': 'snapshot',
                                      'device_type': u'disk',
                                      'snapshot_id': u'cede2421-ea68-4a8e-937d-c27074b9024b',
                                      'destination_type': 'volume',
                                      'delete_on_termination': False
                             }],
                             'base_image_ref': u'',
                             'root_device_name': u'/dev/vda'
                     },
                     'size': 0
                }
                """
            return self.image_api.create(context, image_meta)

    s1步,主要作用是根据虚机的镜像元数据初始化该虚机快照的元数据

    nova/compute/api.py
    class API(base.Base):
        def _initialize_instance_snapshot_metadata(self, instance, name,
                                                   extra_properties=None):
            """Initialize new metadata for a snapshot of the given instance.
            :param instance: nova.objects.instance.Instance object
            :param name: string for name of the snapshot
            :param extra_properties: dict of extra metadata properties to include
            :returns: the new instance snapshot metadata
            """
            image_meta = utils.get_image_from_system_metadata(instance.system_metadata)-------获取虚机的镜像源数据
            image_meta.update({'name': name,'is_public': False})----把镜像元数据的中镜像的名字,更改为快照的名字
    
            # Delete properties that are non-inheritable
            properties = image_meta['properties']
            for key in CONF.non_inheritable_image_properties:----删除镜像数据中不能继承的属性,
                properties.pop(key, None)
    
            # The properties in extra_properties have precedence
            properties.update(extra_properties or {})
            return image_meta
    返回值为
     {
         u'min_disk': u'20',
         'is_public': False,
         'min_ram': u'0',
         'properties': {
             'base_image_ref': u''
          },
         'name': u'snapshot1'
     }

     s2 步 block_device_mapping表里面的数据样例

     *************************** 376. row ***************************
               created_at: 2019-07-18 12:35:24
               updated_at: 2019-07-18 12:35:46
               deleted_at: 2019-07-18 12:35:46
                       id: 24081
              device_name: /dev/vdb
    delete_on_termination: 0
              snapshot_id: NULL
                volume_id: e6f0ddca-74cf-40c9-8db6-d64f32d8ded4
              volume_size: NULL
                no_device: NULL
          connection_info: null
            instance_uuid: f9846a41-72c2-4e67-99d3-8391fae7a3ce
                  deleted: 24081
              source_type: volume
         destination_type: volume
             guest_format: NULL
              device_type: NULL
                 disk_bus: NULL
               boot_index: NULL
                 image_id: NULL
                      tag: NULL
    *************************** 377. row ***************************
               created_at: 2019-07-19 03:02:57----------基于镜像启动block_device_mapping形式
               updated_at: 2019-07-19 03:02:57
               deleted_at: 2019-07-19 03:10:42
                       id: 24084
              device_name: /dev/vda
    delete_on_termination: 1
              snapshot_id: NULL
                volume_id: NULL
              volume_size: NULL
                no_device: 0
          connection_info: NULL
            instance_uuid: 99f10424-2ad3-4cdb-8ca5-ebd166d5853c
                  deleted: 24084
              source_type: image
         destination_type: local
             guest_format: NULL
              device_type: disk
                 disk_bus: NULL
               boot_index: 0
                 image_id: 3006d221-72a6-4fe0-bcdc-d4ace809d8c7
                      tag: NULL

    s5步  block_device.snapshot_from_bdm(snapshot['id'],bdm)

    根据bdm信息,来构建快照的dict格式属性信息,返回一个BlockDeviceDict对象,属性如下:
     {
      'guest_format': None, 
      'boot_index': 0, 
      'no_device': None, 
      'connection_info': None, 
      'snapshot_id': u'cede2421-ea68-4a8e-937d-c27074b9024b',
      'volume_size': 20, 
      'device_name': u'/dev/vda', 
      'disk_bus': u'virtio', 
      'image_id': None, 
      'source_type': 'snapshot', 
      'device_type': u'disk', 
      'volume_id': None, 
      'destination_type': 'volume', 
      'delete_on_termination': False
     }

    nova-api主要是 完成了以下工作:
    1)如果是在线快照,则冻结/解冻结文件系统
    2)创建glance数据库镜像记录(包含所有卷的快照信息)

    2、cinder-api服务的相关处理

    nova-api服务里面s3步的操作,调用cinder的api create_snapshot_force 创建新的卷,实为cinder api的接受请求,进行相关的处理

    其详解如下,
    from nova.volume import cinder
    self.volume_api = volume_api or cinder.API()
    snapshot = self.volume_api.create_snapshot_force(context, volume['id'], name, volume['display_description'])
    nova/volume/cinder.py
        def create_snapshot_force(self, context, volume_id, name, description):
            item = cinderclient(context).volume_snapshots.create(volume_id,True,name,description)
            return _untranslate_snapshot_summary_view(context, item)

    因此可以看出,实际调用的是cinder client的 volume_snapshots 的 create 方法,其在cinder api的入口函数为

    cinder/api/v2/snapshots.py
    class SnapshotsController(wsgi.Controller):
        @wsgi.response(202)
        def create(self, req, body):
            """Creates a new snapshot."""
            # 根据上下文的分析,当nova-api等其他client在发送创建卷快照的请求之后,本方法会接受到请求
            # 方法接收到的参数有:
            # req:Request对象,包含有本次请求的上下内容,包含有用于鉴权的凭证等内容
            # body:快照的属性信息,包含有如下内容:
            #  {
            #      u'snapshot': {
            #           u'volume_id': u'60e16af2-0684-433c-a1b6-c1af1c2523fc',
            #           u'force': True,
            #           u'description': u'',
            #           u'name': u'snapshot for snapshot1',
            #           u'metadata': {}
            #      }
            #  }
            kwargs = {}
            #获取上下文的context信息和获取快照属性中的信息
            context = req.environ['cinder.context']
            self.assert_valid_body(body, 'snapshot')
            snapshot = body['snapshot']
            #获取快照的metadata信息,snapshot_id
            kwargs['metadata'] = snapshot.get('metadata', None)
    
            try:
                volume_id = snapshot['volume_id']
            except KeyError:
                msg = _("'volume_id' must be specified")
                raise exc.HTTPBadRequest(explanation=msg)
            #从数据库中获取卷信息
            volume = self.volume_api.get(context, volume_id)
            #获取传递进来的参数中是否使用强制快照,force=True表示采取强制快照
            force = snapshot.get('force', False)
            msg = _LI("Create snapshot from volume %s")
            LOG.info(msg, volume_id)
            #验证快照名及快照描述是否合法,长度不能超过256个字符
            self.validate_name_and_description(snapshot)
    
            # NOTE(thingee): v2 API allows name instead of display_name
            # 用display_name代替name参数
            if 'name' in snapshot:
                snapshot['display_name'] = snapshot.pop('name')
            try:
            #参数类型转换,如果是非True/False的值,则抛异常
                force = strutils.bool_from_string(force, strict=True)
            except ValueError as error:
                err_msg = encodeutils.exception_to_unicode(error)
                msg = _("Invalid value for 'force': '%s'") % err_msg
                raise exception.InvalidParameterValue(err=msg)
            # 开始进行快照的操作,根据force值的不同走不通的分支
            if force:
                new_snapshot = self.volume_api.create_snapshot_force(-----s2.1
                    context,
                    volume,
                    snapshot.get('display_name'),
                    snapshot.get('description'),
                    **kwargs)
            else:
                new_snapshot = self.volume_api.create_snapshot(-----s2.2
                    context,
                    volume,
                    snapshot.get('display_name'),
                    snapshot.get('description'),
                    **kwargs)
            req.cache_db_snapshot(new_snapshot)
            return self._view_builder.detail(req, new_snapshot)
    """
    from cinder import volume
    self.volume_api = volume.API()
    """
    
    cinder/volume/api.py
    class API(base.Base):
        def create_snapshot_force(self, context,
                                  volume, name,
                                  description, metadata=None):
            result = self._create_snapshot(context, volume, name, description,
                                           True, metadata)
            LOG.info(_LI("Snapshot force create request issued successfully."),
                     resource=result)
            return result
    
        def create_snapshot(self, context,
                            volume, name, description,
                            metadata=None, cgsnapshot_id=None,
                            group_snapshot_id=None):
            result = self._create_snapshot(context, volume, name, description,
                                           False, metadata, cgsnapshot_id,
                                           group_snapshot_id)
            LOG.info(_LI("Snapshot create request issued successfully."),
                     resource=result)
            return result
    
        def _create_snapshot(self, context,
                             volume, name, description,
                             force=False, metadata=None,
                             cgsnapshot_id=None,
                             group_snapshot_id=None):
            #保证卷操作处于冻结状态,并且是可进行快照,检查配额是否可用
            volume.assert_not_frozen()
            #在cinder的snapshot数据表中创建一条快照记录,即会在云硬盘快照面板显示一条名为“snapshot for snapshot1”的记录
            snapshot = self.create_snapshot_in_db(----s2.3
                context, volume, name,
                description, force, metadata, cgsnapshot_id,
                True, group_snapshot_id)
            # 调用rpc.case将create_snapshot的消息投递到消息队列该消息
            self.volume_rpcapi.create_snapshot(context, volume, snapshot)---s2.4 给cinder-volume发送rpc请求信息
            return snapshot    

    可以看到两个方法都是调用了“ _create_snapshot ”,只是在传递第5个参数 force 时不一样,同时force为False时,
    需要传递其他几个参数(实际上也为空)

    cinder-api的操作总结为如下两个方面:
    1)卷状态条件检查及配额检查
    2)创建glance数据库快照记录(记录的是单个卷快照的信息)

    3、cinder-volume服务对快照的处理

    s2.4步中,cinder-api通过rpc给cinder-volume服务发送创建快照的请求,cinder-volume服务接受到请求,并处理,其函数入口为manager.py文件中create_snapshot方法

    cinder/volume/manager.py
    class VolumeManager(manager.CleanableManager,
                        manager.SchedulerDependentManager):
        """Manages attachable block storage devices."""
        @objects.Snapshot.set_workers
        def create_snapshot(self, context, snapshot):
            """Creates and exports the snapshot."""
            # 获取请求上下文
            context = context.elevated()
             # 通过消息队列,通知ceilometer快照发生变化
            self._notify_about_snapshot_usage(
                context, snapshot, "create.start")
    
            try:
            """异常处理代码,有任何异常则退出并设置快照状态为error"""
                # NOTE(flaper87): Verify the driver is enabled
                # before going forward. The exception will be caught
                # and the snapshot status updated.
                # 确保存储驱动已经初始化,否则抛出异常
                utils.require_driver_initialized(self.driver)
    
                # Pass context so that drivers that want to use it, can,
                # but it is not a requirement for all drivers.
                snapshot.context = context
                # 调用后端存储驱动执行快照
                model_update = self.driver.create_snapshot(snapshot)---------s3.1# 完成之后,更新数据库条目,若返回的是None,则不执行
                if model_update:
                    snapshot.update(model_update)
                    snapshot.save()
            except Exception as error:
            # 若之前几步操作出现问题,则将快照的状态置为error
                with excutils.save_and_reraise_exception():
                    snapshot.status = fields.SnapshotStatus.ERROR
                    snapshot.save()
    
                    self.db.snapshot_metadata_update(
                        context,
                        snapshot.id,
                        {'error': six.text_type(error)},
                        False)
            # 从cinder的数据库中获取卷的信息
            vol_ref = self.db.volume_get(context, snapshot.volume_id)
            # 如果该卷的bootable属性为True,表示该卷是启动卷,表示云主机是通过卷启动的,即系统盘,
            # 如果是非启动卷,则跳过
            if vol_ref.bootable:
                try:
                 # 用卷的metadata信息来更新snapshot的metadata信息,需要保证系统盘的元数据与其快照的元数据一致
                    self.db.volume_glance_metadata_copy_to_snapshot(
                        context, snapshot.id, snapshot.volume_id)
                except exception.GlanceMetadataNotFound:
                    # 更新snapshot的元数据如果抛出GlanceMetadataNotFound,
                    # 表示从glance中找不到卷的元数据信息,可以直接跳过
                    # If volume is not created from image, No glance metadata
                    # would be available for that volume in
                    # volume glance metadata table
                    pass
                except exception.CinderException as ex:
                    LOG.exception(_LE("Failed updating snapshot"
                                      " metadata using the provided volumes"
                                      " %(volume_id)s metadata"),
                                  {'volume_id': snapshot.volume_id},
                                  resource=snapshot)
                    # 如果抛出cinder方面的异常,则有可能是快照出现问题,则直接将快照的状态置为error
                    snapshot.status = fields.SnapshotStatus.ERROR
                    snapshot.save()
    
                    self.db.snapshot_metadata_update(context,
                                                     snapshot.id,
                                                     {'error': six.text_type(ex)},
                                                     False)
                    raise exception.MetadataCopyFailure(reason=six.text_type(ex))
            # 若一路过来没有出现异常,则代表快照完成,将快照状态标记为可用,进度为100%,并保存状态
            snapshot.status = fields.SnapshotStatus.AVAILABLE
            snapshot.progress = '100%'
            snapshot.save()
            # 通过消息队列,通知ceilometer快照完成
            self._notify_about_snapshot_usage(context, snapshot, "create.end")
            LOG.info(_LI("Create snapshot completed successfully"),
                     resource=snapshot)
            return snapshot.id

    从上面的代码中可以找到,执行快照其实是调用底层的后端存储来做的,即s3.1 步的“driver.create_snapshot(snapshot)”,
    针对不同的存储类型,会有不同的处理方式
    因此cinder-volume服务快照功能很简单:调用后端存储执行快照,然后更新glance数据库快照记录

    学习过程中,参考如下博客 https://www.cnblogs.com/qianyeliange/p/9713146.html

  • 相关阅读:
    使用钉钉对接禅道的bug系统,实现禅道提的bug实时在钉钉提醒并艾特对应的开发人员处理
    Python3数据驱动ddt
    Python3发送邮件功能
    Python3的日志添加功能
    【最小生成树】BZOJ1016: [JSOI2008]最小生成树计数
    【k短路&A*算法】BZOJ1975: [Sdoi2010]魔法猪学院
    【最小生成树+子集枚举】Uva1151 Buy or Build
    【最小生成树】UVA1494Qin Shi Huang's National Road System秦始皇修路
    【最小生成树+贪心】BZOJ1821: [JSOI2010]Group 部落划分 Group
    【差分+前缀和】BZOJ1637: [Usaco2007 Mar]Balanced Lineup
  • 原文地址:https://www.cnblogs.com/potato-chip/p/11226831.html
Copyright © 2020-2023  润新知