一、问题及背景
- cinder下载镜像(download流程)速度太快,导致临时文件存放目录/opt/cinder/conversion所在的磁盘写入过快,而一旦镜像过大,长时间的快速写入会影响磁盘性能,进而导致依赖此磁盘的其余多个服务崩溃。而恰巧我们的etcd就依赖这个磁盘,影响节点状态检查watchdog的心跳上报,导致多个节点重启。
- cinder下载好的临时镜像转换到目标系统盘(convert流程)的过快,这个流程分为两部分,首先是读,影响读性能的是/opt/cinder/conversion所在的磁盘,其次是写,影响写性能的是挂载到节点上的目标磁盘。这个客户cinder使用的后端存储是一个第三方的存储,性能有所欠缺,在往存储写的速度过快,导致了存储集群的性能下降,进而影响了多个业务虚机的使用。
- 镜像过大,创建系统盘时间过长。客户的镜像过大,有1TB+多,而每次下载镜像转换镜像会耗费太多的时间。这个客户的问题仅仅是时间太长,但既然写到这了,我们分析可以发现,其实这种情况也是有可能会触发问题1和2的条件之一。
二、处理方式
针对问题1,因为cinder的下载是通过python的request库自带的分片下载功能进行的,且无法进行限速。但我们的镜像存放是ceph,ceph支持对存储池级别限速,所以我们最终通过对存放镜像的存储池进行限速,限制镜像读取的速度,变相限制镜像下载速度来workaroud了。
针对问题2,cinder的一个配置项volume_copy_bps_limit,这个配置项是针对convert流程的一个限速,默认值是0,也就是不限速,我们通过这个配置项进行限速解决。
针对问题3,cinder本身有一个功能或者说机制,镜像盘缓存(image_volume_cache),这个功能会在每一个镜像第一次创建系统盘后,会多克隆一份留作缓存盘使用(这个缓存盘的大小就是上传镜像时设置的最小容量),之后再创建这个镜像的系统盘时,就可以直接在存储侧进行克隆,cinder则无需再次下载、转换。这么做能够大幅度提升速度及效率,不过有一点影响,就是每个镜像可能有需要在存储侧有一个缓存盘,有一点点磁盘空间的占用,不过现在这个时代,磁盘容量已经不是什么关键问题了。
三、相关流程及代码分析
本次分析从create_volume流程中cinder.volume.flows.manager.create_volume.CreateVolumeFromSpecTask开始,这也是创建云硬盘的最主要的部分之一。
镜像盘的创建流程(镜像下载、转换、缓存盘创建):
- 根据create_type选择对应的创建方式,包括raw、snap、source_vol、image、backup,其他方式都很简单明了,当前只分析image这种方式 ->2, ->3
- 如果开启了镜像缓存功能且存在镜像缓存盘,则直接从镜像缓存盘克隆出新盘,镜像盘创建结束
- 先下载镜像到cinder-volume的一个临时目录 ->4, ->5
- 如果没有开启镜像缓存功能,则和请求的容量大小一致的空盘A ->6
- 如果开启了镜像缓存功能,但没有镜像缓存盘,则先创建一块和上传镜像时设置的镜像需要的最小容量相同大小的空盘A ->6
- 挂载空盘A到cinder-volume
- 将镜像数据导入到空盘A
- 卸载导入了数据的盘A -9, ->10
- 如果没有开启镜像缓存功能,则镜像盘A创建结束
- 如果开启了镜像缓存功能,但没有镜像缓存盘,则从盘A克隆出一块同样大小的新盘B,留做镜像缓存盘
- 如果盘A容量小于请求的容量,则将盘A扩容到请求的容量大小,镜像盘A创建结束
1. 镜像下载流程分析
因为我们是从镜像创建系统盘,所以最先进入的就是_create_from_image
CreateVolumeFromSpecTask.execute
# execute里主要有5中方式
# 创建没有数据空盘_create_raw_volume
# 从云硬盘的快照创建新盘_create_from_snapshot
# 从已有的云硬盘克隆新盘_create_from_source_volume
# 从一个已有副本创建新盘_create_from_source_replica
# 从镜像创建新盘_create_from_image
->CreateVolumeFromSpecTask._create_from_image
#_create_from_image中主要有4中创建方式
# 直接调用driver中的clone_image接口,不常用。这个接口仅限于需要创建的盘和镜像是在同一个存储集群,且存储支持从镜像
# 直接创建出新盘。例如:glance和cinder都使用同一ceph存储,glance对
# 外提供镜像url实际是存放镜像的rbd设备的快照,而ceph是可以从一个rbd设
# 备的快照克隆到一个新rbd的
# 从_clone_image_volume调用driver._clone_image_volume接口,不常用。和clone_image接口有一点区别,这个接口克隆
# 的源必须是raw格式设备,所以当glance对外提
# 供的镜像也是一个raw设备时,可以调用此接口
# 若已经存在镜像缓存盘了_create_from_image_cache,开启了镜像缓存功能,镜像曾经通过最后一种方式创建过缓存盘时可用
->CreateVolumeFromSpecTask._create_from_image_cache
->CreateVolumeFromSpecTask._create_from_source_volume
->driver.create_cloned_volume
# 若上面三种都不满足时,需要从glance下载镜像到cinder
->image_utils.TemporaryImages.fetch
->image_utils.fetch_verify_image
->image_utils.fetch
->glance.GlanceImageService.download # 参见代码参考1.1
->glance.GlanceClientWrapper.call # 返回一个迭代器,包含镜像数据,参见代码参考1.2
->glanceclient.v2.images.Controller.data
->http_client.get # 这里的http_client有SessionClient, HTTPClient两种,便于理解
# 选择HTTPClient进行说明
->glanceclient.common.http.HttpClient._request
->SessionClient.requset #得到resp
->HttpClient._handle_response(resp) #解析resp
->http._close_after_stream
->requsets.models.Response.iter_content # 将二进制流解析为迭代
# 器,参见参考1.3
->data.write # 将迭代器中的数据写入临时文件,保存在/opt/cinder/conversion
# 目录,参见代码参考1.1
代码参考1.1
# cinder/image/glance.py
class GlanceImageService(object):
def download(self, context, image_id, data=None):
...
try:
image_chunks = self._client.call(context, 'data', image_id) # 获取包含镜像数据的迭代器
except Exception:
_reraise_translated_image_exception(image_id)
if not data:
return image_chunks
else:
for chunk in image_chunks:
data.write(chunk) # 将迭代器中的数据写入本地临时文件
代码参考1.2:
# cinder/image/glance.py
class GlanceClientWrapper(object):
def call(self, context, method, *args, **kwargs):
...
controller = getattr(client,
kwargs.pop('controller', 'images')) # client是glanceclient库中的glanceclient.
# v2.client.py中的Client,没有controller属性,
# 但有self.images = images.Controller
return getattr(controller, method)(*args, **kwargs) # method是'data',
# 即返回glanceclient.v2.images.Controller.data()
...
代码参考1.3:
# requests/models.py
class Response(object):
def iter_content(self, chunk_size=1, decode_unicode=False):
...
# Special case for urllib3.
if hasattr(self.raw, 'stream'):
try:
for chunk in self.raw.stream(chunk_size, decode_content=True):
yield chunk
except ProtocolError as e:
raise ChunkedEncodingError(e)
except DecodeError as e:
raise ContentDecodingError(e)
except ReadTimeoutError as e:
raise ConnectionError(e)
else:
# Standard file-like object.
while True:
chunk = self.raw.read(chunk_size)
if not chunk:
break
yield chunk
...
2. 镜像转换流程分析
CreateVolumeFromSpecTask._create_from_image
->image_utils.TemporaryImages.fetch # 下载好临时镜像保存在/opt/cinder/conversion目录
->CreateVolumeFromSpecTask._create_from_image_download
->driver.create_volume # 若没有开启镜像缓存功能,空云硬盘容量大小为请求创建的容量,
# 否则容量大小为镜像需要的最小容量空云硬盘
->CreateVolumeFromSpecTask._copy_image_to_volume
->driver.copy_image_to_volume # 若存储厂商的驱动没有实现该接口,则会使用cinder/volume/driver.py基类中的
# copy_image_to_volume。 但部分存储有自己单独实现,例如ceph,是直接将临时
# 文件import到目标rbd设备。这里仅分析通用流程
->driver.BaseVD._copy_image_data_to_volume
->image_utils.fetch_to_raw
->image_utils.fetch_to_volume_format
# fetch_to_volume_format有两个实现, 两者均通过volume_copy_bps_limit进行限速,参见代码参考2.1
# 如果临时镜像不是qemu镜像,则使用volume_utils.copy_volume。这种最终使用dd,
# 将临时文件写入挂载的空云硬盘
# 如果临时镜像是qemu镜像,则使用image_utils.convert_image。这种最终使用qemu-img convert,
# 将临时文件写入挂载的空云硬盘
->image_utils.convert_image
->image_utils._convert_image # 使用qemu-img convert命令
代码参考2.1:
# cinder/volume/driver.py
class BaseVD(object):
def set_throttle(self): # 在初始化时VolumeManager.init_host()中就会调用
bps_limit = ((self.configuration and
self.configuration.safe_get('volume_copy_bps_limit')) or
CONF.volume_copy_bps_limit)
cgroup_name = ((self.configuration and
self.configuration.safe_get(
'volume_copy_blkio_cgroup_name')) or
CONF.volume_copy_blkio_cgroup_name)
self._throttle = None
if bps_limit:
try:
self._throttle = throttling.BlkioCgroup(int(bps_limit),
cgroup_name)
except processutils.ProcessExecutionError as err:
LOG.warning(_LW('Failed to activate volume copy throttling: '
'%(err)s'), {'err': err})
throttling.Throttle.set_default(self._throttle)
# cinder/image/image_utils.py
def convert_image(source, dest, out_format, run_as_root=True, throttle=None):
if not throttle:
volume_utils.check_cgroup()
throttle = throttling.Throttle.get_default()
with throttle.subcommand(source, dest) as throttle_cmd:
_convert_image(tuple(throttle_cmd['prefix']),
source, dest,
out_format, run_as_root=run_as_root)
# cinder/volume/utils.py
def copy_volume(src, dest, size_in_m, blocksize, sync=False,
execute=utils.execute, ionice=None, throttle=None,
sparse=False):
if (isinstance(src, six.string_types) and
isinstance(dest, six.string_types)):
if not throttle:
check_cgroup()
throttle = throttling.Throttle.get_default()
with throttle.subcommand(src, dest) as throttle_cmd:
_copy_volume_with_path(throttle_cmd['prefix'], src, dest,
size_in_m, blocksize, sync=sync,
execute=execute, ionice=ionice,
sparse=sparse)
else:
_copy_volume_with_file(src, dest, size_in_m)
3. 镜像缓存盘创建流程分析
CreateVolumeFromSpecTask._create_from_image
->image_utils.TemporaryImages.fetch # 下载好临时镜像保存在/opt/cinder/conversion目录
->CreateVolumeFromSpecTask._create_from_image_download # 将临时镜像写入目标云硬盘。若没有开启镜像缓存功能,
# 空云硬盘容量大小为请求创建的容量,
# 否则容量大小为镜像需要的最小容量空云硬盘
->cinder.volume.manager.VolumeManager._create_image_cache_volume_entry # 若开启了镜像缓存功能,则需要从
# _create_from_image_download创建的盘克隆出
# 一个新盘当作镜像缓存盘
->VolumeManager._clone_image_volume
->VolumeManager.create_volume # 新盘(缓存盘)的source_volid即为_create_from_image_download接口创建的云硬盘
->create_volume.get_flow
->CreateVolumeFromSpecTask.execute
->CreateVolumeFromSpecTask._create_from_source_volume
->driver.create_cloned_volume
->driver.extend_volume # 若_create_from_image_download创建的云硬盘容量不是请求的系统盘容量,则进行扩容
4. 从卷上传镜像
VolumeActionsController._volume_upload_image
-> cinder.volume.api.API.copy_volume_to_image
-> cinder.volume.api.API._merge_volume_image_meta
-> cinder.volume.manager.VolumeManager.copy_volume_to_image
# copy_volume_to_image有两种实现:
# 1. _clone_image_volume_and_add_location 当使用cinder来存放镜像时,直接克隆一个新盘留作镜像
# 2. copy_volume_to_image 将盘挂载到cinder-volume节点上,再上传到glance,如果是in-use的盘,需要支持multiattach
-> VolumeManager._clone_image_volume_and_add_location
-> VolumeManager._clone_image_volume
- VolumeManager.create_volume #创建一个新盘source_id设置为需要上传的volume.id
-> driver.copy_volume_to_image #cinder/volume/driver.py里有实现,不部分厂商的驱动都没有单独再实现此接口,分为三步
-> cinder.volume.driver.BaseVD._attach_volume # 1. 挂盘
-> driver.create_export
-> driver.initialize_connection #存储添加映射
-> driver._connect_device
-> connector.connect_volume #调用os-brick扫盘
-> cinder.volume.volume_utils.upload_volume # 2. 上传
-> cinder.image.image_utils.upload_volume
-> cinder.image.glance.GlanceImageService.update
-> glance.GlanceClientWrapper.call
-> glanceclient.v2.images.Controller.upload # 上传镜像data
-> glance.GlanceClientWrapper.call('update')
-> glanceclient.v2.images.Controller.update # 更新镜像metadata
-> cinder.volume.driver.BaseVD._detach_volume # 3. 卸盘
-> connector.disconnect_volume #调用os-brick清理节点磁盘
-> driver.terminate_connection #存储解除映射
-> driver.remove_export
5. retype卷
-> cinder.volume.manager.VolumeManager.retype
-> volume_types.volume_types_diff # 如果两个volume_type(包括extra_specs,qos_specs,encryption)完全一致,则结束retype
-> self.driver.retype # 如果目标type和源type是同一个后端,则调用driver.retype
-> cinder.volume.manager.VolumeManager.migrate_volume # 如果driver.retype失败或者没有完成,则使用migrate_volume
-> self.driver.migrate_volume # 先尝试调用驱动的migrate_volume
-> cinder.volume.manager.VolumeManager._migrate_volume_generic # 如果驱动没有完成迁移,则使用cinder自带
# 方法_migrate_volume_generic
-> rpc.create_volume
# 两种实现:
# 1. 如果源盘没有挂载信息,则cinder直接挂载两个盘并通过dd完成retype工作
-> VolumeManager._copy_volume_data
-> cinder.volume.manager.VolumeManager._attach_volume # 挂载目标盘
-> cinder.volume.manager.VolumeManager._attach_volume # 挂载源盘
-> cinder.volume.volume_utils.copy_volume
-> volume_utils._copy_volume_with_path # 通过dd进行拷贝
-> _detach_volume # 卸载目标盘
-> _detach_volume # 卸载源盘
-> VolumeManager.migrate_volume_completion
-> rpcapi.update_migrated_volume # 调用driver.update_migrated_volume更新信息,
# 如果driver没有单独实现,就将新盘的provider_location更新到源盘
-> cinder.objects.volume.finish_volume_migration # 将目标盘的信息除id, provider_location,
# glance_metadata, volume_type外替换到源盘
-> rpcapi.delete_volume # 删除新盘
# 2. 如果源盘有挂载信息(in-use),则由nova主导完成retype工作
-> cinder.compute.nova.API.update_server_volume
-> novaclient.v2.volumes.VolumeManager.update_server_volume