OpenStack快照分析:(三)从磁盘启动云主机离线(在线)快照分析
1. 磁盘启动云主机,离线(在线)快照
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(
context,
instance,
image_name,
extra_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_format,disk_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)
# 调用cinder的api
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_api的create来创建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_api(cinder-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.py的API类中,有这两个方法:
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("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时不一样,同时force为False时,需要传递其他几个参数(实际上也为空)。下面具体分析_create_snapshot方法:
def _create_snapshot(self,
context, volume, name, description, force=False, metadata=None,
cgsnapshot_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()
# 在cinder的snapshot数据表中创建一条快照记录,即会在云硬盘快照面板显示一条名为“snapshot for snapshot1”的记录
snapshot = self.create_snapshot_in_db(
context, volume, name, description, force, metadata, cgsnapshot_id,
True, group_snapshot_id)
kwargs = {'snapshot_id': snapshot.id,
'volume_properties': objects.VolumeProperties(size=volume.size)}
# 调用rpc.case将create_snapshot的消息投递到消息队列该消息
self.scheduler_rpcapi.create_snapshot(context, volume, snapshot,
volume.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信息来更新snapshot的metadata信息,需要保证系统盘的元数据与其快照的元数据一致
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提供有很多的后端存储驱动,比如EMC、HPE、SVC等,查看整个驱动目录,支持:
比如,我们使用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的镜像面板显示一条镜像记录,在卷快照面板显示一条或者多条(如果有多个卷的话)卷快照记录。