1 背景知识
1.1 Ceph简介
- Ceph是当前非常流行的开源分布式存储系统,具有高扩展性、高性能、高可靠性等优点,同时提供块存储服务(rbd)、对象存储服务(rgw)以及文件系统存储服务(cephfs)。目前也是OpenStack的主流后端存储,和OpenStack亲如兄弟,为OpenStack提供统一共享存储服务。使用Ceph作为OpenStack后端存储,具有如下优点:
- 所有的计算节点共享存储,迁移时不需要拷贝根磁盘,即使计算节点挂了,也能立即在另一个计算节点启动虚拟机(evacuate)。
- 利用COW(Copy On Write)特性,创建虚拟机时,只需要基于镜像clone即可,不需要下载整个镜像,而clone操作基本是0开销,从而实现了秒级创建虚拟机。
Ceph RBD支持thin provisioning,即按需分配空间,有点类似Linux文件系统的sparse稀疏文件。创建一个20GB的虚拟硬盘时,最开始并不占用物理存储空间,只有当写入数据时,才按需分配存储空间。
Ceph的更多知识可以参考官方文档,这里我们只关注RBD,RBD管理的核心对象为块设备(block device),通常我们称为volume,不过Ceph中习惯称之为image(注意和OpenStack image的区别)。Ceph中还有一个pool的概念,类似于namespace,不同的pool可以定义不同的副本数、pg数、放置策略等。每个image都必须指定pool。image的命名规范为pool_name/image_name@snapshot,比如openstack/test-volume@test-snap,表示在openstackpool中test-volumeimage的快照test-snap。因此以下两个命令效果是等同的:
- rbd snap create --pool openstack --image test-image --snap test-snap
- rbd snap create openstack/test-image@test-snap
在openstack pool上创建一个1G的image命令为:
- rbd -p openstack create --size 1024 int32bit-test-1
image支持快照(snapshot)的功能,创建一个快照即保存当前image的状态,相当于git commit操作,用户可以随时把image回滚到任意快照点上(git reset)。创建快照命令如下:
- rbd -p openstack snap create int32bit-test-1@snap-1
查看rbd列表:
- $ rbd -p openstack ls -l | grep int32bit-test
- int32bit-test-1 1024M 2
- int32bit-test-1@snap-1 1024M 2
基于快照可以创建一个新的image,称为clone,clone不会立即复制原来的image,而是使用COW策略,即写时拷贝,只有当需要写入一个对象时,才从parent中拷贝那个对象到本地,因此clone操作基本秒级完成,并且需要注意的是基于同一个快照创建的所有image共享快照之前的image数据,因此在clone之前我们必须保护(protect)快照,被保护的快照不允许删除。clone操作类似于git branch操作,clone一个image命令如下:
- rbd -p openstack snap protect int32bit-test-1@snap-1
- rbd -p openstack clone int32bit-test-1@snap-1 int32bit-test-2
我们可以查看一个image的子image(children)有哪些,也能查看一个image是基于哪个image clone的(parent):
- $ rbd -p openstack children int32bit-test-1@snap-1
- openstack/int32bit-test-2
- $ rbd -p openstack info int32bit-test-2 | grep parent
- parent: openstack/int32bit-test-1@snap-1
以上我们可以发现int32bit-test-2是int32bit-test-1的children,而int32bit-test-1是int32bit-test-2的parent。
不断地创建快照并clone image,就会形成一条很长的image链,链很长时,不仅会影响读写性能,还会导致管理非常麻烦。可幸的是Ceph支持合并链上的所有image为一个独立的image,这个操作称为flatten,类似于git merge操作,flatten需要一层一层拷贝所有顶层不存在的数据,因此通常会非常耗时。
- $ rbd -p openstack flatten int32bit-test-2
- Image flatten: 31% complete...
此时我们再次查看其parrent-children关系:
- rbd -p openstack children int32bit-test-1@snap-1
此时int32bit-test-1没有children了,int32bit-test-2完全独立了。
当然Ceph也支持完全拷贝,称为copy:
- rbd -p openstack cp int32bit-test-1 int32bit-test-3
copy会完全拷贝一个image,因此会非常耗时,但注意copy不会拷贝原来的快照信息。
Ceph支持将一个RBD image导出(export):
- rbd -p openstack export int32bit-test-1 int32bit-1.raw
导出会把整个image导出,Ceph还支持差量导出(export-diff),即指定从某个快照点开始导出:
- rbd -p openstack export-diff \
- int32bit-test-1 --from-snap snap-1 \
- --snap snap-2 int32bit-test-1-diff.raw
以上导出从快照点snap-1到快照点snap-2的数据。
当然与之相反的操作为import以及import-diff。通过export/import支持image的全量备份,而export-diff/import-diff实现了image的差量备份。
Rbd image是动态分配存储空间,通过du命令可以查看image实际占用的物理存储空间:
- $ rbd du int32bit-test-1
- NAME PROVISIONED USED
- int32bit-test-1 1024M 12288k
以上image分配的大小为1024M,实际占用的空间为12288KB。
删除image,注意必须先删除其所有快照,并且保证没有依赖的children:
- rbd -p openstack snap unprotect int32bit-test-1@snap-1
- rbd -p openstack snap rm int32bit-test-1@snap-1
- rbd -p openstack rm int32bit-test-1
1.2 OpenStack简介
OpenStack是一个IaaS层的云计算平台开源实现,关于OpenStack的更多介绍欢迎访问我的个人博客,这里只专注于当OpenStack对接Ceph存储系统时,基于源码分析一步步探测Ceph到底做了些什么工作。本文不会详细介绍OpenStack的整个工作流程,而只关心与Ceph相关的实现,如果有不清楚OpenStack源码架构的,可以参考我之前写的文章如何阅读OpenStack源码。
阅读完本文可以理解以下几个问题:
- 为什么上传的镜像必须要转化为raw格式?
- 如何高效上传一个大的镜像文件?
- 为什么能够实现秒级创建虚拟机?
- 为什么创建虚拟机快照需要数分钟时间,而创建volume快照能够秒级完成?
- 为什么当有虚拟机存在时,不能删除镜像?
- 为什么一定要把备份恢复到一个空卷中,而不能覆盖已经存在的volume?
- 从镜像中创建volume,能否删除镜像?
注意本文都是在基于使用Ceph存储的前提下,即Glance、Nova、Cinder都是使用的Ceph,其它情况下结论不一定成立。
(注:原文有源代码,已经超过5000字的篇幅限制,因此做了精简,如果需要看详细推导验证过程,请查看原文链接,另外你可以快速跳到总结部分查看OpenStack各个操作对应的Ceph工作。)
2 Glance
2.1 Glance介绍
Glance管理的核心实体是image,它是OpenStack的核心组件之一,为OpenStack提供镜像服务(Image as Service),主要负责OpenStack镜像以及镜像元数据的生命周期管理、检索、下载等功能。Glance支持将镜像保存到多种存储系统中,后端存储系统称为store,访问镜像的地址称为location,location可以是一个http地址,也可以是一个rbd协议地址。只要实现store的driver就可以作为Glance的存储后端,其中driver的主要接口如下:
- get: 获取镜像的location。
- get_size: 获取镜像的大小。
- get_schemes: 获取访问镜像的URL前缀(协议部分),比如rbd、swift+https、http等。
- add: 上传镜像到后端存储中。
- delete: 删除镜像。
- set_acls: 设置后端存储的读写访问权限。
为了便于维护,glance store目前已经作为独立的库从Glance代码中分离出来,由项目glance_store维护。目前社区支持的store列表如下:
- filesystem: 保存到本地文件系统,默认保存/var/lib/glance/images到目录下。
- cinder: 保存到Cinder中。
- rbd:保存到Ceph中。
- sheepdog:保存到sheepdog中。
- swift: 保存到Swift对象存储中。
- vmware datastore: 保存到Vmware datastore中。
http: 以上的所有store都会保存镜像数据,唯独http store比较特殊,它不保存镜像的任何数据,因此没有实现add方法,它仅仅保存镜像的URL地址,启动虚拟机时由计算节点从指定的http地址中下载镜像。
本文主要关注rbd store,它的源码在这里,该store的driver代码主要由国内Fei Long Wang负责维护,其它store的实现细节可以参考源码glance store drivers.
3 Nova
3.1 Nova介绍
Nova管理的核心实体为server,为OpenStack提供计算服务,它是OpenStack最核心的组件。注意Nova中的server不只是指虚拟机,它可以是任何计算资源的抽象,除了虚拟机以外,也有可能是baremetal裸机、容器等。
不过我们在这里假定:
- server为虚拟机。
- image type为rbd。
- compute driver为libvirt。
启动虚拟机之前首先需要准备根磁盘(root disk),Nova称为image,和Glance一样,Nova的image也支持存储到本地磁盘、Ceph以及Cinder(boot from volume)中。需要注意的是,image保存到哪里是通过image type决定的,存储到本地磁盘可以是raw、qcow2、ploop等,如果image type为rbd,则image存储到Ceph中。不同的image type由不同的image backend负责,其中rbd的backend为nova/virt/libvirt/imageackend中的Rbd类模块实现。
4 Cinder
4.1 Cinder介绍
Cinder是OpenStack的块存储服务,类似AWS的EBS,管理的实体为volume。Cinder并没有实现volume provide功能,而是负责管理各种存储系统的volume,比如Ceph、fujitsu、netapp等,支持volume的创建、快照、备份等功能,对接的存储系统我们称为backend。只要实现了cinder/volume/driver.py中VolumeDriver类定义的接口,Cinder就可以对接该存储系统。
Cinder不仅支持本地volume的管理,还能把本地volume备份到远端存储系统中,比如备份到另一个Ceph集群或者Swift对象存储系统中,本文将只考虑从源Ceph集群备份到远端Ceph集群中的情况。
5 总结
5.1 Glance
1. 上传镜像
- rbd -p ${GLANCE_POOL} create --size ${SIZE} ${IMAGE_ID}rbd -p ${GLANCE_POOL} snap create ${IMAGE_ID}@snap
- rbd -p ${GLANCE_POOL} snap protect ${IMAGE_ID}@snap
2. 删除镜像
- rbd -p ${GLANCE_POOL} snap unprotect ${IMAGE_ID}@snap
- rbd -p ${GLANCE_POOL} snap rm ${IMAGE_ID}@snap
- rbd -p ${GLANCE_POOL} rm ${IMAGE_ID}
5.2 Nova
1 创建虚拟机
- rbd clone \${GLANCE_POOL}/${IMAGE_ID}@snap \${NOVA_POOL}/${SERVER_ID}_disk
2 创建虚拟机快照
- # Snapshot the disk and clone # it into Glance's storage poolrbd -p ${NOVA_POOL} snap create \${SERVER_ID}_disk@${RANDOM_UUID}rbd -p ${NOVA_POOL} snap protect \${SERVER_ID}_disk@${RANDOM_UUID}rbd clone \${NOVA_POOL}/${SERVER_ID}_disk@${RANDOM_UUID} \${GLANCE_POOL}/${IMAGE_ID} # Flatten the image, which detaches it from the # source snapshotrbd -p ${GLANCE_POOL} flatten ${IMAGE_ID} # all done with the source snapshot, clean it uprbd -p ${NOVA_POOL} snap unprotect \${SERVER_ID}_disk@${RANDOM_UUID}rbd -p ${NOVA_POOL} snap rm \${SERVER_ID}_disk@${RANDOM_UUID} # Makes a protected snapshot called 'snap' on # uploaded images and hands it outrbd -p ${GLANCE_POOL} snap create ${IMAGE_ID}@snap
- rbd -p ${GLANCE_POOL} snap protect ${IMAGE_ID}@snap
3 删除虚拟机
- for image in $(rbd -p ${NOVA_POOL} ls | grep "^${SERVER_ID}");do
- rbd -p ${NOVA_POOL} rm "$image"; done
5.3 Cinder
1 创建volume
(1) 创建空白卷
- rbd -p ${CINDER_POOL} create \--new-format --size ${SIZE} \volume-${VOLUME_ID}
(2) 从快照中创建
- rbd clone \${CINDER_POOL}/volume-${SOURCE_VOLUME_ID}@snapshot-${SNAPSHOT_ID} \${CINDER_POOL}/volume-${VOLUME_ID}rbd resize --size ${SIZE} \openstack/volume-${VOLUME_ID}
(3) 从volume中创建
- # Do full copy if rbd_max_clone_depth <= 0.if [[ "$rbd_max_clone_depth" -le 0 ]]; then
- rbd copy \
- ${CINDER_POOL}/volume-${SOURCE_VOLUME_ID} \
- ${CINDER_POOL}/volume-${VOLUME_ID}
- exit 0fi# Otherwise do COW clone.# Create new snapshot of source volumerbd snap create \${CINDER_POOL}/volume-${SOURCE_VOLUME_ID}@volume-${VOLUME_ID}.clone_snap
- rbd snap protect \${CINDER_POOL}/volume-${SOURCE_VOLUME_ID}@volume-${VOLUME_ID}.clone_snap# Now clone source volume snapshotrbd clone \${CINDER_POOL}/volume-${SOURCE_VOLUME_ID}@volume-${VOLUME_ID}.clone_snap \${CINDER_POOL}/volume-${VOLUME_ID}# If dest volume is a clone and rbd_max_clone_depth reached,# flatten the dest after cloning.depth=$(get_clone_depth ${CINDER_POOL}/volume-${VOLUME_ID})if [[ "$depth" -ge "$rbd_max_clone_depth" ]]; then
- # Flatten destination volume
- rbd flatten ${CINDER_POOL}/volume-${VOLUME_ID}
- # remove temporary snap
- rbd snap unprotect \
- ${CINDER_POOL}/volume-${SOURCE_VOLUME_ID}@volume-${VOLUME_ID}.clone_snap
- rbd snap rm \
- ${CINDER_POOL}/volume-${SOURCE_VOLUME_ID}@volume-${VOLUME_ID}.clone_snapfi
(4) 从镜像中创建
- rbd clone \${GLANCE_POOL}/${IMAGE_ID}@snap \${CINDER_POOL}/volume-${VOLUME_ID}if [[ -n "${SIZE}" ]]; then
- rbd resize --size ${SIZE} ${CINDER_POOL}/volume-${VOLUME_ID}fi
2 创建快照
- rbd -p ${CINDER_POOL} snap create \volume-${VOLUME_ID}@snapshot-${SNAPSHOT_ID}rbd -p ${CINDER_POOL} snap protect \volume-${VOLUME_ID}@snapshot-${SNAPSHOT_ID}
3 创建备份
(1) ***次备份
- rbd -p ${BACKUP_POOL} create \
- --size ${VOLUME_SIZE} \
- volume-${VOLUME_ID}.backup.base
- NEW_SNAP=volume-${VOLUME_ID}@backup.${BACKUP_ID}.snap.${TIMESTAMP}
- rbd -p ${CINDER_POOL} snap create ${NEW_SNAP}
- rbd export-diff ${CINDER_POOL}/volume-${VOLUME_ID}${NEW_SNAP} - \
- | rbd import-diff --pool ${BACKUP_POOL} - \
- volume-${VOLUME_ID}.backup.base
(2) 增量备份
- rbd -p ${CINDER_POOL} snap create \volume-${VOLUME_ID}@backup.${BACKUP_ID}.snap.${TIMESTAMP} rbd export-diff --pool ${CINDER_POOL} \--from-snap backup.${PARENT_ID}.snap.${LAST_TIMESTAMP} \${CINDER_POOL}/volume-${VOLUME_ID}@backup.${BACKUP_ID}.snap.${TIMESTRAMP} - \| rbd import-diff --pool ${BACKUP_POOL} - \${BACKUP_POOL}/volume-${VOLUME_ID}.backup.base
- rbd -p ${CINDER_POOL} snap rm \volume-${VOLUME_ID}.backup.base@backup.${PARENT_ID}.snap.${LAST_TIMESTAMP}
4 备份恢复
- rbd export-diff --pool ${BACKUP_POOL} \volume-${SOURCE_VOLUME_ID}.backup.base@backup.${BACKUP_ID}.snap.${TIMESTRAMP} - \| rbd import-diff --pool ${CINDER_POOL} - \volume-${DEST_VOLUME_ID}rbd -p ${CINDER_POOL} resize \--size ${new_size} volume-${DEST_VOLUME_ID}