OpenStack快照分析:(三)从磁盘启动云主机离线(在线)快照分析

时间:2024-01-21 19:42:04

磁盘启动云主机离线(在线)快照

1.1.   nova-api处理过程

磁盘启动的云主机在做离线快照时,还是首先是nova-api接收请求,函数入口和前述一样,还是 nova/api/openstack/compute/servers.py/ServersController._action_create_image下面一起来看看:

@wsgi.response(202)
@
extensions.expected_errors((400, 403, 404, 409))
# 定义关联的API接口
@
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', {})
    snapshot_id = entity.get(
"snapshot_id", None)

   
# 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)
   
    instance =
self._get_server(context, req, id)

    snapshot = snapshot_current(context
, instance, self.compute_rpcapi)
   
if snapshot:  # if there are snapshots, then create an image with snashots.
       
if not snapshot_id:
            snapshot_id = snapshot[
"id"]
        image = snapshot_create_image(context
, snapshot_id, instance, self.compute_rpcapi, entity)
   
else:
       
#从数据库中获取实例对象(InstanceV2)及块设备映射列表
        bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context
, instance.uuid)

       
# 判断实例是镜像启动还是磁盘启动
       
if compute_utils.is_volume_backed_instance(context, instance, bdms):
           
# 校验执行权限
            context.can(server_policies.SERVERS % 
'create_image:allow_volume_backed')
    
       # 这里执行的是磁盘启动方式的快照,传递的参数包括:
            # 1
、关于权限的上下文context
            # 2
、虚拟机的实例对象instance
            # 3
、快照的名称image_name
            #
本章节讲的磁盘启动的云主机快照就是进入该分支进行操作。
            image =
self.compute_api.snapshot_volume_backed(contextinstanceimage_nameextra_properties=metadata)
       
else:
           
# 这里是镜像启动的云主机快照的入口,即上节内容
            image =
self.compute_api.snapshot(context, instance, image_name, extra_properties=metadata)

   
if api_version_request.is_supported(req, '2.45'):
       
return {'image_id': image['id']}

   
# build location of newly-created image entity
   
image_id = str(image['id'])
    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/API.snapshot_volume_backed

@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, vm_states.SUSPENDED])
def snapshot_volume_backed(self, context, instance, name, extra_properties=None):
   
"""Snapshot the given volume-backed instance.从实例的system_metadata生成镜像属性(排除不可继承属性),如下:
      
# {
       #     u'min_disk': u'20',
       #     'is_public': False,
       #     'min_ram': u'0',
       #     'properties': {
       #         'base_image_ref': u''
       #      },
       #     'name': u'snapshot1'
       # }


    :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)
   
image_meta['size'] = 0
   
# 清除镜像metadata属性中的container_formatdisk_forma属性
   
for attr in ('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

   
# 从数据库中获取该云主机所关联的所有块设备,结果会返回一个BlockDeviceMappingList对象
    bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context
, instance.uuid)
   
# 接下来开始做快照的操作,注意,云主机挂在了多少个卷设备,就要做多少次快照
    mapping = [] 
# list of BDM dicts that can go into the image properties
    # Do some up-front filtering of the list of BDMs from
    # which we are going to create snapshots.
   
volume_bdms = []
   
for bdm in bdms:
       
if bdm.no_device:
           
# 映射关系中没有块设备,则忽略此条映射
           
continue
        if
bdm.is_volume:
           
# These will be handled below.此映射包含块设备,加入到volume_bdms,准备做快照
           
volume_bdms.append(bdm)
       
else:
            mapping.append(bdm.get_image_mapping())

   
# Check limits in Cinder before creating snapshots to avoid going over
    # quota in the middle of a list of volumes. This is a best-effort check
    # but concurrently running snapshot requests from the same project
    # could still fail to create volume snapshots if they go over limit.
   
# 在创建快照之前,需要首先在Cinder中检查配额限制,以避免超过配额限制
   
if volume_bdms:
        limits =
self.volume_api.get_absolute_limits(context)
        total_snapshots_used = limits[
'totalSnapshotsUsed']
        max_snapshots = limits[
'maxTotalSnapshots']
        
# -1 means there is unlimited quota for snapshots
       
if (max_snapshots > -1 and
               
len(volume_bdms) + total_snapshots_used > max_snapshots):
            LOG.debug(
'Unable to create volume snapshots for instance. Currently has %s snapshots, requesting %s new snapshots, with a limit of %s.',
                     
total_snapshots_used, len(volume_bdms),
                     
max_snapshots, instance=instance)
           
raise exception.OverQuota(overs='snapshots')

    quiesced =
False
   
# 判断虚拟机的状态,如果虚拟机处于active,则通过rpc通知虚拟机进入静默状态(异常处理省略)
   
if instance.vm_state == vm_states.ACTIVE:
        LOG.info(
"Attempting to quiesce instance before volume snapshot.", instance=instance)
       
self.compute_rpcapi.quiesce_instance(context, instance)
        quiesced =
True

   
# 定义一个获取云主机上的及具体卷信息的方法,返回云主机的卷映射
    @
wrap_instance_event(prefix='api')
   
def snapshot_instance(self, context, instance, bdms):
       
for bdm in volume_bdms:
            
# create snapshot based on volume_id
           
#根据卷的volume_id从数据库获取卷的详细信息
            
volume = self.volume_api.get(context, bdm.volume_id)
           
# 组装出一个貌似是desc的消息,比如快照名称是snapshot1,则这里就是snapshot for snapshot1
            
name = _('snapshot for %s') % image_meta['name']
            LOG.debug(
'Creating snapshot from volume %s.', volume['id'], instance=instance)
           
# 调用cinderapi create_snapshot_force创建新的卷
           
# “create_snapshot_force”中实际上是通过cinderclient来调用volume_snapshots.create
            #
来发起创建卷的请求,具体是由cinder-volume来完成卷的快照,返回的内容为卷快照的信息,
            #
格式如:
            #{

            #  'status': u'creating',
            #  'display_name': u'snapshot for snapshot1',
            #  'created_at': u'2016-06-24T09:23:00.517279',
            #  'display_description': u'',
            #  'volume_size': 20,
            #  'volume_id': u'60e16af2-0684-433c-a1b6-c1af1c2523fc',
            #  'progress': None,
            #  'project_id': u'25520b29dce346d38bc4b055c5ffbfcb',
            #  'id': u'cede2421-ea68-4a8e-937d-c27074b9024b',
            #  'size': 20
            # }

            snapshot = self.volume_api.create_snapshot_force(
                context
, volume['id'], name, volume['display_description'])
           
# 接着会根据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
            # }

            mapping_dict = block_device.snapshot_from_bdm(snapshot[
'id'], bdm)
           
# 过滤掉已经在数据库中存在的字段
            mapping_dict = mapping_dict.get_image_mapping()
           
# 将云主机所有的映射关系都添加到mapping
            mapping.append(mapping_dict)
        
return mapping

   
self._record_action_start(context, instance, instance_actions.CREATE_IMAGE)
   
# 调用“snapshot_instance”,获取云主机所有的mapping关系
    mapping = snapshot_instance(
self, context, instance, bdms)

   
# 如果此时卷的文件系统已静默,这里则进行解冻处理,实现过程就是通过rpc.case发送异步请求给nova-compute
    # nova-compute
接收到消息后,会等到快照完成后对文件系统进行解冻(需要agent支持)
   
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)

最后一步通过调用image_apicreate来创建image,跟踪过去实际上就只有一句session.create(context, image_info, data=data),调用RESTful API来创建image

至此,nova-api的工作已经做完,总结一下,nov-api主要是 完成了一下工作:

l  如果是在线快照,则冻结/解冻结文件系统

l 创建glance数据库镜像记录(包含所有卷的快照信息)

1.2. cinder创建磁盘快照

1.2.1.  cinder-api处理过程

上节中讲到在创建卷快照的时候,nova-api在处理请求时,使用cinderclient调用volume_apicinder-api)来通过http方式发送快照的请求,cinder-api会接受该请求,处理代码如下:

@wsgi.response(http_client.ACCEPTED)
@
validation.schema(snapshot.create)
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']
    snapshot = body[
'snapshot']
   
# 获取快照的metadata信息,snapshot_id
    kwargs[
'metadata'] = snapshot.get('metadata', None)
    volume_id = snapshot[
'volume_id']
   
# 从数据库中获取卷信息
    volume =
self.volume_api.get(context, volume_id)
   
# 这里是获取传递进来的参数中是否使用强制快照,force=True表示采取强制快照
    force = snapshot.get(
'force', False)
   
# 参数类型转换,如果是非True/False的值,则抛异常
    force = strutils.bool_from_string(force
, strict=True)
    LOG.info(
"Create snapshot from volume %s", volume_id)
   
# 验证快照名及快照描述是否合法,长度不能超过256个字符
   
self.validate_name_and_description(snapshot, check_length=False)
   
# NOTE(thingee): v2 API allows name instead of display_name
   
# display_name代替name参数
   
if 'name' in snapshot:
        snapshot[
'display_name'] = snapshot.pop('name')
   
# 开始进行快照的操作,根据force值得不同走不通的分支,其实都是对_create_snapshot的封装
   
if force:
        new_snapshot =
self.volume_api.create_snapshot_force(context

                                                             volume

                                                             snapshot.get('display_name')

                                                             snapshot.get('description')

                                                             **kwargs)
   
else:
        new_snapshot =
self.volume_api.create_snapshot(context

                                                       volume

                                                       snapshot.get('display_name')

                                                       snapshot.get('description')

                                                       **kwargs)
    req.cache_db_snapshot(new_snapshot)

   
return self._view_builder.detail(req, new_snapshot)

cinder/volume/api.pyAPI类中,有这两个方法:

def create_snapshot(self, context, volume, name, descriptionmetadata=None, cgsnapshot_id=None, group_snapshot_id=None):
    result =
self._create_snapshot(context, volume, name, descriptionFalse, metadata, cgsnapshot_id, group_snapshot_id)
    LOG.info(
"Snapshot create request issued successfully.", resource=result)
   
return result

def create_snapshot_force(self, context, volume, name, description, metadata=None):
    result =
self._create_snapshot(context, volume, name, description, True, metadata)
    LOG.info(
"Snapshot force create request issued successfully.", resource=result)
   
return result

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

def _create_snapshot(self, context, volume, name, description, force=False, metadata=Nonecgsnapshot_id=None, group_snapshot_id=None):
    """
根据上文的分析:force = True

   
该方法完成如下功能:
    1.
执行卷状态条件判断,如果卷处于维护状态,迁移过程中,副本卷,
       force=False
且不是可用状态,则抛异常
    2.
执行用户快照配额管理,用户可以为不同的卷类型设置不同的配额信息,如:
       volumes, gigabytes,snapshots,
我这里使用的是ceph rbd,例如:
         {

           'gigabytes': 20,
           'snapshots_ceph': 1,
           'gigabytes_ceph': 20,
           'snapshots': 1
         }
     
用户默认配额如下:
         {
           'gigabytes': 1000,
           'snapshots_ceph': -1,
           'snapshots': 10,
           'gigabytes_ceph': -1
         }
      
如果配额不足则会抛异常
    3.
创建快照条目,例如(创建卷快照要先在cinder数据库创建snapshot数据库条目):
      {
           'status': u'creating',
           'volume_type_id': 'd494e240-17b3-4d35-a5a1-2923d8677d79',
           'display_name': u'snapshot for snapshot1',
           'user_id': 'b652f9bd65844f739684a20ed77e9a0f',
           'display_description': u'',
           'cgsnapshot_id': None,
           'volume_size': 20,
           'encryption_key_id': None,
           'volume_id': '60e16af2-0684-433c-a1b6-c1af1c2523fc',
           'progress': u'0%',
           'project_id': '25520b29dce346d38bc4b055c5ffbfcb',
           'metadata': {}
      }

      卷快照完成后,会在Dashboard的云硬盘快照面板显示一条名为'snapshot for snapshot1'的卷快照记录

    """
    # 我理解这一步应该是继续保证卷操作处于冻结状态,并且是可进行快照,检查配额是否可用
    volume.assert_not_frozen()
 
  # cindersnapshot数据表中创建一条快照记录,即会在云硬盘快照面板显示一条名为“snapshot for snapshot1”的记录
    snapshot =
self.create_snapshot_in_db(context, volume, name, description, force, metadata, cgsnapshot_idTrue, group_snapshot_id)
   
kwargs = {'snapshot_id': snapshot.id'volume_properties': objects.VolumeProperties(size=volume.size)}
   
# 调用rpc.casecreate_snapshot的消息投递到消息队列该消息
   
self.scheduler_rpcapi.create_snapshot(context, volume, snapshotvolume.service_topic_queue, objects.RequestSpec(**kwargs))
   
return snapshot

至此,cinder-api的处理结束!

小结:卷快照过程中,cinder-api的操作总结为如下两个方面:

l  卷状态条件检查及配额检查

l  创建glance数据库快照记录(记录的是单个卷快照的信息)

1.2.2.  Cinder-volume的处理过程

cinder-volume从消息队列接收到来自cinder-api的创建快照的请求消息后,cinder-volume就会调用其VolumeManager.create_snapshot方法进行处理,代码位置:cinder/volume/manager.py,如下:

@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"""
       
# 确保存储驱动已经初始化,否则抛出异常
       
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

       
# 调用后端存储驱动执行快照,例如使用RBDDriver,下文具体分析
        model_update =
self.driver.create_snapshot(snapshot)
       
# 完成之后,更新数据库条目,若返回的是None,则不执行
       
if model_update:
            snapshot.update(model_update)
            snapshot.save()

   
except Exception:
       
# 若之前几步操作出现问题,则将快照的状态置为error
       
with excutils.save_and_reraise_exception():
            snapshot.status = fields.SnapshotStatus.ERROR
            snapshot.save()

   
# cinder的数据库中获取卷的信息
    vol_ref =
self.db.volume_get(context, snapshot.volume_id)
    # 如果该卷的bootable属性为True,表示该卷是启动卷,表示云主机是通过卷启动的,即系统盘,
    #
如果是非启动卷,则跳过
   
if vol_ref.bootable:
       
try:
           
# 用卷的metadata信息来更新snapshotmetadata信息,需要保证系统盘的元数据与其快照的元数据一致
           
self.db.volume_glance_metadata_copy_to_snapshot(context, snapshot.id, snapshot.volume_id)
       
except exception.GlanceMetadataNotFound:
          
 # 更新snapshot的元数据如果抛出GlanceMetadataNotFound
            #
表示从glance中找不到卷的元数据信息,可以直接跳过
           
pass
        except
exception.CinderException as ex:
            LOG.exception(
"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()
           
raise exception.MetadataCopyFailure(reason=six.text_type(ex))

   
# 若一路过来没有出现异常,则代表快照完成,将快照状态标记为可用,进度为100%,并保存状态
    snapshot.status = fields.SnapshotStatus.AVAILABLE
    snapshot.progress =
'100%'
   
snapshot.encryption_key_id = vol_ref.encryption_key_id
    snapshot.save()
  
 # 通过消息队列,通知ceilometer快照完成
   
self._notify_about_snapshot_usage(context, snapshot, "create.end")
    LOG.info(
"Create snapshot completed successfully",
            
resource=snapshot)
   
return snapshot.id

从上面的代码中可以找到,执行快照其实是调用底层的后端存储来做的,即“driver.create_snapshot(snapshot)”,针对不同的存储类型,会有不同的处理方式,这也就是OpenStack的一个设计理念,只提供一个框架,具体功能的实现则是交给对应的provider来做,只要你提供的功能的调用符合OpenStack的接口标准便可以。

IDE中点击进入driver.create_snapshot方法,会出现以下选择框:

        

可以看出,cinder-volume提供有很多的后端存储驱动,比如华为的存储VMware的VcKaminar等,查看整个驱动目录,支持:

     

比如,我们使用ceph作为后端存储,这时候就会使用RBD的驱动,快照调用的方法为:

def create_snapshot(self, snapshot):
   
"""Creates an rbd snapshot."""
   
with RBDVolumeProxy(self, snapshot.volume_name) as volume:
        snap = utils.convert_str(snapshot.name)
        volume.create_snap(snap)
        volume.protect_snap(snap)

其过程也就是创建一个Image对象,然后直接调用librbd相关的方法执行秒级快照,不做具体分析了。

小结:cinder-volume快照功能很简单:调用后端存储执行快照,然后更新glance数据库快照记录

阅读完上面的分析,相信读者会发现上面的快照过程中cinder执行的就是卷的快照,nova实现的是云主机信息及其镜像记录的处理。事实确实也如此:快照执行完成后,会在Dashboard的镜像面板显示一条镜像记录,在卷快照面板显示一条或者多条(如果有多个卷的话)卷快照记录。