OpenStack使用Ceph存儲,Ceph到底做了什麼?
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
,表示在openstack
pool中test-volume
image的快照test-snap
。因此以下兩個命令效果是等同的:
rbd snap create --pool openstack --image test-image --snap test-snaprbd 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-testint32bit-test-1 1024M 2int32bit-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-1rbd -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-1openstack/int32bit-test-2$ rbd -p openstack info int32bit-test-2 | grep parentparent: 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-2Image 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-1NAME PROVISIONED USEDint32bit-test-1 1024M 12288k
以上image分配的大小為1024M,實際佔用的空間為12288KB。
刪除image,注意必須先刪除其所有快照,並且保證沒有依賴的children:
rbd -p openstack snap unprotect int32bit-test-1@snap-1rbd -p openstack snap rm int32bit-test-1@snap-1rbd -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,其它情況下結論不一定成立。
另外本文會先貼源代碼,很長很枯燥,你可以快速跳到總結部分查看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.
2.2 鏡像上傳
由前面的介紹可知,鏡像上傳主要由store的add
方法實現:
@capabilities.checkdef add(self, image_id, image_file, image_size, context=None, verifier=None): checksum = hashlib.md5() image_name = str(image_id) with self.get_connection(conffile=self.conf_file, rados_id=self.user) as conn: fsid = None if hasattr(conn, "get_fsid"): fsid = conn.get_fsid() with conn.open_ioctx(self.pool) as ioctx: order = int(math.log(self.WRITE_CHUNKSIZE, 2)) try: loc = self._create_image(fsid, conn, ioctx, image_name, image_size, order) except rbd.ImageExists: msg = _("RBD image %s already exists") % image_id raise exceptions.Duplicate(message=msg) ...
其中注意image_file
不是一個文件,而是LimitingReader
實例,該實例保存了鏡像的所有數據,通過read(bytes)
方法讀取鏡像內容。
從以上源碼中看,glance首先獲取ceph的連接session,然後調用_create_image
方法創建了一個rbd image,大小和鏡像的size一樣:
def _create_image(self, fsid, conn, ioctx, image_name, size, order, context=None): librbd = rbd.RBD() features = conn.conf_get("rbd_default_features") librbd.create(ioctx, image_name, size, order, old_format=False, features=int(features)) return StoreLocation({ "fsid": fsid, "pool": self.pool, "image": image_name, "snapshot": DEFAULT_SNAPNAME, }, self.conf)
因此以上步驟通過rbd命令表達大致為:
rbd -p ${rbd_store_pool} create --size ${image_size} ${image_id}
在ceph中創建完rbd image後,接下來:
with rbd.Image(ioctx, image_name) as image: bytes_written = 0 offset = 0 chunks = utils.chunkreadable(image_file, self.WRITE_CHUNKSIZE) for chunk in chunks: offset += image.write(chunk, offset) checksum.update(chunk)
可見Glance逐塊從image_file中讀取數據寫入到剛剛創建的rbd image中並計算checksum,其中塊大小由rbd_store_chunk_size
配置,默認為8MB。
我們接著看最後步驟:
if loc.snapshot: image.create_snap(loc.snapshot) image.protect_snap(loc.snapshot)
從代碼中可以看出,最後步驟為創建image快照(快照名為snap)並保護起來。
假設我們上傳的鏡像為cirros,鏡像大小為39MB,鏡像uuid為d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6
,配置保存在ceph的openstack
pool中,則對應ceph的操作流程大致為:
rbd -p openstack create --size 39 d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6rbd -p openstack snap create d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snaprbd -p openstack snap protect d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snap
我們可以通過rbd命令驗證:
$ rbd ls -l | grep d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6 40162k 2d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snap 40162k 2 yes
啟示
我們前面介紹了鏡像上傳到Ceph的過程,省略了鏡像上傳到Glance的流程,但毋容置疑的是鏡像肯定是通過Glance API上傳到Glance中的。當鏡像非常大時,由於通過Glance API走HTTP協議,導致非常耗時且佔用API管理網帶寬。我們可以通過rbd import
直接導入鏡像的方式大幅度提高上傳鏡像的效率。
首先使用Glance創建一個空鏡像,記下它的uuid:
glance image-create d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6 | awk "/sids/{print $4}"}
假設uuid為d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6
,使用rbd命令直接導入鏡像並創建快照:
rbd -p openstack import cirros.raw --image=d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6rbd -p openstack snap create d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snaprbd -p openstack snap protect d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snap
設置glance鏡像location url:
FS_ID=`ceph -s | grep cluster | awk "{print $2}"`glance location-add --url rbd://${FS_ID}/openstack/d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6/snap d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6
設置glance鏡像其它屬性:
glance image-update --name="cirros" --disk-format=raw --container-format=bare d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6
2.3 鏡像刪除
刪除鏡像就是相反的過程,即先執行unprotext
-> snap rm
-> rm
,如下:
try: self._unprotect_snapshot(image, snapshot_name) image.remove_snap(snapshot_name)except rbd.ImageBusy as exc: raise exceptions.InUseByStore()rbd.RBD().remove(ioctx, image_name)
刪除鏡像必須保證當前rbd image沒有子image,否則刪除會失敗。
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
類模塊實現。
3.2 創建虛擬機
創建虛擬機的過程不再詳細分析,不清楚的可以查看我之前寫的博客,我們直接進入研究Nova的libvirt driver是如何為虛擬機準備根磁碟image的,代碼位於nova/virt/libvirt/driver.py
的spawn
方法,其中創建image調用了_create_image
方法。
def spawn(self, context, instance, image_meta, injected_files, admin_password, network_info=None, block_device_info=None): ... self._create_image(context, instance, disk_info["mapping"], injection_info=injection_info, block_device_info=block_device_info) ...
_create_image
方法部分代碼如下:
def _create_image(self, context, instance, disk_mapping, injection_info=None, suffix="", disk_images=None, block_device_info=None, fallback_from_host=None, ignore_bdi_for_swap=False): booted_from_volume = self._is_booted_from_volume(block_device_info) ... # ensure directories exist and are writable fileutils.ensure_tree(libvirt_utils.get_instance_path(instance)) ... self._create_and_inject_local_root(context, instance, booted_from_volume, suffix, disk_images, injection_info, fallback_from_host) ...
該方法首先在本地創建虛擬機的數據目錄/var/lib/nova/instances/${uuid}/
,然後調用了_create_and_inject_local_root
方法創建根磁碟。
def _create_and_inject_local_root(self, context, instance, booted_from_volume, suffix, disk_images, injection_info, fallback_from_host): ... if not booted_from_volume: root_fname = imagecache.get_cache_fname(disk_images["image_id"]) size = instance.flavor.root_gb * units.Gi backend = self.image_backend.by_name(instance, "disk" + suffix, CONF.libvirt.images_type) if backend.SUPPORTS_CLONE: def clone_fallback_to_fetch(*args, **kwargs): try: backend.clone(context, disk_images["image_id"]) except exception.ImageUnacceptable: libvirt_utils.fetch_image(*args, **kwargs) fetch_func = clone_fallback_to_fetch else: fetch_func = libvirt_utils.fetch_image self._try_fetch_image_cache(backend, fetch_func, context, root_fname, disk_images["image_id"], instance, size, fallback_from_host) ...
其中image_backend.by_name()
方法通過image type名稱返回image backend
實例,這裡是Rbd
。從代碼中看出,如果backend支持clone操作(SUPPORTS_CLONE),則會調用backend的clone()
方法,否則通過fetch_image()
方法下載鏡像。顯然Ceph rbd是支持clone的。我們查看Rbd
的clone()
方法,代碼位於nova/virt/libvirt/imagebackend.py
模塊:
def clone(self, context, image_id_or_uri): ... for location in locations: if self.driver.is_cloneable(location, image_meta): LOG.debug("Selected location: %(loc)s", {"loc": location}) return self.driver.clone(location, self.rbd_name) ...
該方法遍歷Glance image的所有locations,然後通過driver.is_cloneable()
方法判斷是否支持clone,若支持clone則調用driver.clone()
方法。其中driver
是Nova的storage driver,代碼位於nova/virt/libvirt/storage
,其中rbd driver在rbd_utils.py
模塊下,我們首先查看is_cloneable()
方法:
def is_cloneable(self, image_location, image_meta): url = image_location["url"] try: fsid, pool, image, snapshot = self.parse_url(url) except exception.ImageUnacceptable as e: return False if self.get_fsid() != fsid: return False if image_meta.get("disk_format") != "raw": return False # check that we can read the image try: return self.exists(image, pool=pool, snapshot=snapshot) except rbd.Error as e: LOG.debug("Unable to open image %(loc)s: %(err)s", dict(loc=url, err=e)) return False
可見如下情況不支持clone:
- Glance中的rbd image location不合法,rbd location必須包含fsid、pool、image id,snapshot 4個欄位,欄位通過
/
劃分。 - Glance和Nova對接的是不同的Ceph集群。
- Glance鏡像非raw格式。
- Glance的rbd image不存在名為
snap
的快照。
其中尤其注意第三條,如果鏡像為非raw格式,Nova創建虛擬機時不支持clone操作,因此必須從Glance中下載鏡像。這就是為什麼Glance使用Ceph存儲時,鏡像必須轉化為raw格式的原因。
最後我們看clone
方法:
def clone(self, image_location, dest_name, dest_pool=None): _fsid, pool, image, snapshot = self.parse_url( image_location["url"]) with RADOSClient(self, str(pool)) as src_client: with RADOSClient(self, dest_pool) as dest_client: try: RbdProxy().clone(src_client.ioctx, image, snapshot, dest_client.ioctx, str(dest_name), features=src_client.features) except rbd.PermissionError: raise exception.Forbidden(_("no write permission on " "storage pool %s") % dest_pool)
該方法只調用了ceph的clone
方法,可能會有人疑問都是使用同一個Ceph cluster,為什麼需要兩個ioctx
?這是因為Glance和Nova可能使用的不是同一個Ceph pool,一個pool對應一個ioctx
。
以上操作大致相當於如下rbd命令:
rbd clone ${glance_pool}/${鏡像uuid}@snap ${nova_pool}/${虛擬機uuid}.disk
假設Nova和Glance使用的pool都是openstack
,Glance鏡像uuid為d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6
,Nova虛擬機的uuid為cbf44290-f142-41f8-86e1-d63c902b38ed
,則對應的rbd命令大致為:
rbd clone openstack/d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snap openstack/cbf44290-f142-41f8-86e1-d63c902b38ed_disk
我們進一步驗證:
int32bit $ rbd -p openstack ls | grep cbf44290-f142-41f8-86e1-d63c902b38edcbf44290-f142-41f8-86e1-d63c902b38ed_diskint32bit $ rbd -p openstack info cbf44290-f142-41f8-86e1-d63c902b38ed_diskrbd image "cbf44290-f142-41f8-86e1-d63c902b38ed_disk": size 2048 MB in 256 objects order 23 (8192 kB objects) block_name_prefix: rbd_data.9f756763845e format: 2 features: layering, exclusive-lock, object-map, fast-diff, deep-flatten flags: create_timestamp: Wed Nov 22 05:11:17 2017 parent: openstack/d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snap overlap: 40162 kB
由輸出可見,Nova確實創建了一個名為cbf44290-f142-41f8-86e1-d63c902b38ed_disk
rbd image,並且它的parent為openstack/d1a06da9-8ccd-4d3e-9b63-6dcd3ead29e6@snap
。
啟示
- 創建虛擬機時並沒有拷貝鏡像,也不需要下載鏡像,而是一個簡單clone操作,因此創建虛擬機基本可以在秒級完成。
- 如果鏡像中還有虛擬機依賴,則不能刪除該鏡像,換句話說,刪除鏡像之前,必須刪除基於該鏡像創建的所有虛擬機。
3.3 創建虛擬機快照
首先說點題外話,我感覺Nova把create image和create snapshot弄混亂了,我理解的這二者的區別:
- create image:把虛擬機的根磁碟上傳到Glance中。
- create snapshot: 根據image格式對虛擬機做快照,qcow2和rbd格式顯然都支持快照。快照不應該保存到Glance中,由Nova或者Cinder(boot from Cinder)管理。
可事實上,Nova創建快照的子命令為image-create
,API方法也叫_action_create_image()
,之後調用的方法叫snapshot()
。而實際上,對於大多數image type,如果不是從雲硬碟啟動(boot from volume),其實就是create image,即上傳鏡像到Glance中,而非真正的snapshot。
當然只是命名的區別而已,這裡對create image和create snapshot不做任何區別。
虛擬機的快照由libvirt
driver的snapshot()
方法實現,代碼位於nova/virt/libvirt/driver.py
,核心代碼如下:
def snapshot(self, context, instance, image_id, update_task_state): ... root_disk = self.image_backend.by_libvirt_path( instance, disk_path, image_type=source_type) try: update_task_state(task_state=task_states.IMAGE_UPLOADING, expected_state=task_states.IMAGE_PENDING_UPLOAD) metadata["location"] = root_disk.direct_snapshot( context, snapshot_name, image_format, image_id, instance.image_ref) self._snapshot_domain(context, live_snapshot, virt_dom, state, instance) self._image_api.update(context, image_id, metadata, purge_props=False) except (NotImplementedError, exception.ImageUnacceptable) as e: ...
Nova首先通過disk_path
獲取對應的image backend,這裡返回的是imagebackend.Rbd
,然後調用了backend的direct_snapshot()
方法,該方法如下:
def direct_snapshot(self, context, snapshot_name, image_format, image_id, base_image_id): fsid = self.driver.get_fsid() parent_pool = self._get_parent_pool(context, base_image_id, fsid) self.driver.create_snap(self.rbd_name, snapshot_name, protect=True) location = {"url": "rbd://%(fsid)s/%(pool)s/%(image)s/%(snap)s" % dict(fsid=fsid, pool=self.pool, image=self.rbd_name, snap=snapshot_name)} try: self.driver.clone(location, image_id, dest_pool=parent_pool) self.driver.flatten(image_id, pool=parent_pool) finally: self.cleanup_direct_snapshot(location) self.driver.create_snap(image_id, "snap", pool=parent_pool, protect=True) return ("rbd://%(fsid)s/%(pool)s/%(image)s/snap" % dict(fsid=fsid, pool=parent_pool, image=image_id))
從代碼中分析,大體可分為以下幾個步驟:
- 獲取Ceph集群的fsid。
- 對虛擬機根磁碟對應的rbd image創建一個臨時快照,快照名是一個隨機uuid。
- 將創建的快照保護起來(protect)。
- 基於快照clone一個新的rbd image,名稱為snapshot uuid。
- 對clone的image執行flatten操作。
- 刪除創建的臨時快照。
- 對clone的rbd image創建快照,快照名為snap,並執行protect。
對應rbd命令,假設虛擬機uuid為cbf44290-f142-41f8-86e1-d63c902b38ed
,快照的uuid為db2b6552-394a-42d2-9de8-2295fe2b3180
,則對應rbd命令為:
# Snapshot the disk and clone it into Glance"s storage poolrbd -p openstack snap create cbf44290-f142-41f8-86e1-d63c902b38ed_disk@3437a9bbba5842629cc76e78aa613c70rbd -p openstack snap protect cbf44290-f142-41f8-86e1-d63c902b38ed_disk@3437a9bbba5842629cc76e78aa613c70rbd -p openstack clone cbf44290-f142-41f8-86e1-d63c902b38ed_disk@3437a9bbba5842629cc76e78aa613c70 db2b6552-394a-42d2-9de8-2295fe2b3180# Flatten the image, which detaches it from the source snapshotrbd -p openstack flatten db2b6552-394a-42d2-9de8-2295fe2b3180# all done with the source snapshot, clean it uprbd -p openstack snap unprotect cbf44290-f142-41f8-86e1-d63c902b38ed_disk@3437a9bbba5842629cc76e78aa613c70rbd -p openstack snap rm cbf44290-f142-41f8-86e1-d63c902b38ed_disk@3437a9bbba5842629cc76e78aa613c70# Makes a protected snapshot called "snap" on uploaded images # and hands it outrbd -p openstack snap create db2b6552-394a-42d2-9de8-2295fe2b3180@snaprbd -p openstack snap protect db2b6552-394a-42d2-9de8-2295fe2b3180@snap
其中3437a9bbba5842629cc76e78aa613c70
是產生的臨時快照名稱,它一個隨機生成的uuid。
啟示
其它存儲後端主要耗時會在鏡像上傳過程,而當使用Ceph存儲時,主要耗在rbd的flatten過程,因此創建虛擬機快照通常要好幾分鐘的時間。有人可能會疑問,為什麼一定要執行flatten操作呢,直接clone不就完事了嗎?社區這麼做是有原因的:
- 如果不執行flatten操作,則虛擬機快照依賴於虛擬機,換句話說,虛擬機只要存在快照就不能刪除虛擬機了,這顯然不合理。
- 上一個問題繼續延展,假設基於快照又創建虛擬機,虛擬機又創建快照,如此反覆,整個rbd image的依賴會非常複雜,根本管理不了。
- 當rbd image鏈越來越長時,對應的IO讀寫性能也會越來越差。
- …
3.4 刪除虛擬機
libvirt driver刪除虛擬機的代碼位於nova/virt/libvirt/driver.py
的destroy
方法:
def destroy(self, context, instance, network_info, block_device_info=None, destroy_disks=True): self._destroy(instance) self.cleanup(context, instance, network_info, block_device_info, destroy_disks)
注意前面的_destroy
方法其實就是虛擬機關機操作,即Nova會首先讓虛擬機先關機再執行刪除操作。緊接著調用cleanup()
方法,該方法執行資源的清理工作。這裡我們只關注清理disks的過程:
if destroy_disks: # NOTE(haomai): destroy volumes if needed if CONF.libvirt.images_type == "lvm": self._cleanup_lvm(instance, block_device_info) if CONF.libvirt.images_type == "rbd": self._cleanup_rbd(instance)...
由於我們的image type為rbd,因此調用的_cleanup_rbd()
方法:
def _cleanup_rbd(self, instance): if instance.task_state == task_states.RESIZE_REVERTING: filter_fn = lambda disk: (disk.startswith(instance.uuid) and disk.endswith("disk.local")) else: filter_fn = lambda disk: disk.startswith(instance.uuid) LibvirtDriver._get_rbd_driver().cleanup_volumes(filter_fn)
我們只考慮正常刪除操作,忽略resize撤回操作,則filter_fn
為lambda disk: disk.startswith(instance.uuid)
,即所有以虛擬機uuid開頭的disk(rbd image)。需要注意,這裡沒有調用imagebackend
的Rbd
driver,而是直接調用storage driver
,代碼位於nova/virt/libvirt/storage/rbd_utils.py
:
def cleanup_volumes(self, filter_fn): with RADOSClient(self, self.pool) as client: volumes = RbdProxy().list(client.ioctx) for volume in filter(filter_fn, volumes): self._destroy_volume(client, volume)
該方法首先獲取所有的rbd image列表,然後通過filter_fn
方法過濾以虛擬機uuid開頭的image,調用_destroy_volume
方法:
def _destroy_volume(self, client, volume, pool=None): """Destroy an RBD volume, retrying as needed. """ def _cleanup_vol(ioctx, volume, retryctx): try: RbdProxy().remove(ioctx, volume) raise loopingcall.LoopingCallDone(retvalue=False) except rbd.ImageHasSnapshots: self.remove_snap(volume, libvirt_utils.RESIZE_SNAPSHOT_NAME, ignore_errors=True) except (rbd.ImageBusy, rbd.ImageHasSnapshots): LOG.warning("rbd remove %(volume)s in pool %(pool)s failed", {"volume": volume, "pool": self.pool}) retryctx["retries"] -= 1 if retryctx["retries"] <= 0: raise loopingcall.LoopingCallDone() # NOTE(danms): We let it go for ten seconds retryctx = {"retries": 10} timer = loopingcall.FixedIntervalLoopingCall( _cleanup_vol, client.ioctx, volume, retryctx) timed_out = timer.start(interval=1).wait() if timed_out: # NOTE(danms): Run this again to propagate the error, but # if it succeeds, don"t raise the loopingcall exception try: _cleanup_vol(client.ioctx, volume, retryctx) except loopingcall.LoopingCallDone: pass
該方法最多會嘗試10+1次_cleanup_vol()
方法刪除rbd image,如果有快照,則會先刪除快照。
假設虛擬機的uuid為cbf44290-f142-41f8-86e1-d63c902b38ed
,則對應rbd命令大致為:
for image in $(rbd -p openstack ls | grep "^cbf44290-f142-41f8-86e1-d63c902b38ed");do rbd -p openstack rm "$image";done
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集群中的情況。
4.2 創建volume
創建volume由cinder-volume服務完成,入口為cinder/volume/manager.py
的create_volume()
方法,
def create_volume(self, context, volume, request_spec=None, filter_properties=None, allow_reschedule=True): ... try: # NOTE(flaper87): Driver initialization is # verified by the task itself. flow_engine = create_volume.get_flow( context_elevated, self, self.db, self.driver, self.scheduler_rpcapi, self.host, volume, allow_reschedule, context, request_spec, filter_properties, image_volume_cache=self.image_volume_cache, ) except Exception: msg = _("Create manager volume flow failed.") LOG.exception(msg, resource={"type": "volume", "id": volume.id}) raise exception.CinderException(msg)...
Cinder創建volume的流程使用了taskflow框架,taskflow具體實現位於cinder/volume/flows/manager/create_volume.py
,我們關注其execute()
方法:
def execute(self, context, volume, volume_spec): ... if create_type == "raw": model_update = self._create_raw_volume(volume, **volume_spec) elif create_type == "snap": model_update = self._create_from_snapshot(context, volume, **volume_spec) elif create_type == "source_vol": model_update = self._create_from_source_volume( context, volume, **volume_spec) elif create_type == "image": model_update = self._create_from_image(context, volume, **volume_spec) else: raise exception.VolumeTypeNotFound(volume_type_id=create_type) ...
從代碼中我們可以看出,創建volume分為4種類型:
- raw: 創建空白卷。
- create from snapshot: 基於快照創建volume。
- create from volume: 相當於複製一個已存在的volume。
- create from image: 基於Glance image創建一個volume。
raw
創建空白卷是最簡單的方式,代碼如下:
def _create_raw_volume(self, volume, **kwargs): ret = self.driver.create_volume(volume) ...
直接調用driver的create_volume()
方法,這裡driver是RBDDriver
,代碼位於cinder/volume/drivers/rbd.py
:
def create_volume(self, volume): with RADOSClient(self) as client: self.RBDProxy().create(client.ioctx, vol_name, size, order, old_format=False, features=client.features) try: volume_update = self._enable_replication_if_needed(volume) except Exception: self.RBDProxy().remove(client.ioctx, vol_name) err_msg = (_("Failed to enable image replication")) raise exception.ReplicationError(reason=err_msg, volume_id=volume.id)
其中size
單位為MB,vol_name
為volume-${volume_uuid}
。
假設volume的uuid為bf2d1c54-6c98-4a78-9c20-3e8ea033c3db
,Ceph池為openstack
,創建的volume大小為1GB,則對應的rbd命令相當於:
rbd -p openstack create --new-format --size 1024 volume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db
我們可以通過rbd命令驗證:
int32bit $ rbd -p openstack ls | grep bf2d1c54-6c98-4a78-9c20-3e8ea033c3dbvolume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db
create from snapshot
從快照中創建volume也是直接調用driver的方法,如下:
def _create_from_snapshot(self, context, volume, snapshot_id, **kwargs): snapshot = objects.Snapshot.get_by_id(context, snapshot_id) model_update = self.driver.create_volume_from_snapshot(volume, snapshot)
我們查看RBDDriver
的create_volume_from_snapshot()
方法:
def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" volume_update = self._clone(volume, self.configuration.rbd_pool, snapshot.volume_name, snapshot.name) if self.configuration.rbd_flatten_volume_from_snapshot: self._flatten(self.configuration.rbd_pool, volume.name) if int(volume.size): self._resize(volume) return volume_update
從代碼中看出,從snapshot中創建快照分為3個步驟:
- 從rbd快照中執行clone操作。
- 如果
rbd_flatten_volume_from_snapshot
配置為True
,則執行flatten
操作。 - 如果創建中指定了
size
,則執行resize
操作。
假設新創建的volume的uuid為e6bc8618-879b-4655-aac0-05e5a1ce0e06
,快照的uuid為snapshot-e4e534fc-420b-45c6-8e9f-b23dcfcb7f86
,快照的源volume uuid為bf2d1c54-6c98-4a78-9c20-3e8ea033c3db
,指定的size為2,rbd_flatten_volume_from_snapshot
為False
(默認值),則對應的rbd命令為:
rbd clone openstack/volume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db@snapshot-e4e534fc-420b-45c6-8e9f-b23dcfcb7f86 openstack/volume-e6bc8618-879b-4655-aac0-05e5a1ce0e06rbd resize --size 2048 openstack/volume-e6bc8618-879b-4655-aac0-05e5a1ce0e06
從源碼上分析,Cinder從快照中創建volume時,用戶可以配置是否執行flatten操作:
- 如果執行flatten操作,則從快照中創建volume可能需要數分鐘的時間,創建後可以隨時刪除快照。
- 如果不執行flatten操作,則需要注意在刪除所有基於該快照創建的volume之前,不能刪除該快照,也不能刪除快照的源volume。
第二點可能會更複雜,比如基於快照創建了一個volume,然後基於該volume又創建了快照,基於該快照創建了volume,則用戶不能刪除源volume,不能刪除快照。
create from volume
從volume中創建volume,需要指定源volume id(source_volid
):
def _create_from_source_volume(self, context, volume, source_volid, **kwargs): srcvol_ref = objects.Volume.get_by_id(context, source_volid) model_update = self.driver.create_cloned_volume(volume, srcvol_ref)
我們直接查看driver的create_cloned_volume()
方法,該方法中有一個很重要的配置項rbd_max_clone_depth
,即允許rbd image clone允許的最長深度,如果rbd_max_clone_depth <= 0
,則表示不允許clone:
# Do full copy if requestedif self.configuration.rbd_max_clone_depth <= 0: with RBDVolumeProxy(self, src_name, read_only=True) as vol: vol.copy(vol.ioctx, dest_name) self._extend_if_required(volume, src_vref) return
此時相當於rbd的copy命令。
如果rbd_max_clone_depth > 0
:
# Otherwise do COW clone.with RADOSClient(self) as client: src_volume = self.rbd.Image(client.ioctx, src_name) LOG.debug("creating snapshot="%s"", clone_snap) try: # Create new snapshot of source volume src_volume.create_snap(clone_snap) src_volume.protect_snap(clone_snap) # Now clone source volume snapshot LOG.debug("cloning "%(src_vol)s@%(src_snap)s" to " ""%(dest)s"", {"src_vol": src_name, "src_snap": clone_snap, "dest": dest_name}) self.RBDProxy().clone(client.ioctx, src_name, clone_snap, client.ioctx, dest_name, features=client.features)
這個過程和創建虛擬機快照非常相似,二者都是先基於源image創建snapshot,然後基於snapshot執行clone操作,區別在於是否執行flatten操作,創建虛擬機快照時一定會執行flatten操作,而該操作則取決於clone深度:
depth = self._get_clone_depth(client, src_name)if depth >= self.configuration.rbd_max_clone_depth: dest_volume = self.rbd.Image(client.ioctx, dest_name) try: dest_volume.flatten() except Exception as e: ... try: src_volume.unprotect_snap(clone_snap) src_volume.remove_snap(clone_snap) except Exception as e: ...
如果當前depth超過了允許的最大深度rbd_max_clone_depth
則執行flatten操作,並刪除創建的快照。
假設創建的volume uuid為3b8b15a4-3020-41a0-80be-afaa35ed5eef
,源volume uuid為bf2d1c54-6c98-4a78-9c20-3e8ea033c3db
,則對應的rbd命令為:
VOLID=3b8b15a4-3020-41a0-80be-afaa35ed5eefSOURCE_VOLID=bf2d1c54-6c98-4a78-9c20-3e8ea033c3dbCINDER_POOL=openstack# Do full copy if rbd_max_clone_depth <= 0.if [[ "$rbd_max_clone_depth" -le 0 ]]; then rbd copy ${CINDER_POOL}/volume-${SOURCE_VOLID} openstack/volume-${VOLID} exit 0fi# Otherwise do COW clone.# Create new snapshot of source volumerbd snap create ${CINDER_POOL}/volume-${SOURCE_VOLID}@volume-${VOLID}.clone_snaprbd snap protect ${CINDER_POOL}/volume-${SOURCE_VOLID}@volume-${VOLID}.clone_snap# Now clone source volume snapshotrbd clone ${CINDER_POOL}/volume-${SOURCE_VOLID}@volume-${VOLID}.clone_snap ${CINDER_POOL}/volume-${VOLID}# If dest volume is a clone and rbd_max_clone_depth reached,# flatten the dest after cloning.depth=$(get_clone_depth ${CINDER_POOL}/volume-${VOLID})if [[ "$depth" -ge "$rbd_max_clone_depth" ]]; then # Flatten destination volume rbd flatten ${CINDER_POOL}/volume-${VOLID} # remove temporary snap rbd snap unprotect ${CINDER_POOL}/volume-${SOURCE_VOLID}@volume-${VOLID}.clone_snap rbd snap rm ${CINDER_POOL}/volume-${SOURCE_VOLID}@volume-${VOLID}.clone_snapfi
當rbd_max_clone_depth > 0
且depth < rbd_max_clone_depth
時,通過rbd命令驗證:
int32bit $ rbd info volume-3b8b15a4-3020-41a0-80be-afaa35ed5eefrbd image "volume-3b8b15a4-3020-41a0-80be-afaa35ed5eef": size 1024 MB in 256 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.ae2e437c177a format: 2 features: layering, exclusive-lock, object-map, fast-diff, deep-flatten flags: create_timestamp: Wed Nov 22 12:32:09 2017 parent: openstack/volume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db@volume-3b8b15a4-3020-41a0-80be-afaa35ed5eef.clone_snap overlap: 1024 MB
可見volume-3b8b15a4-3020-41a0-80be-afaa35ed5eef
的parent為:
volume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db@volume-3b8b15a4-3020-41a0-80be-afaa35ed5eef.clone_snap.
create from image
從鏡像中創建volume,這裡假定Glance和Cinder都使用的同一個Ceph集群,則Cinder可以直接從Glance中clone,不需要下載鏡像:
def _create_from_image(self, context, volume, image_location, image_id, image_meta, image_service, **kwargs): ... model_update, cloned = self.driver.clone_image( context, volume, image_location, image_meta, image_service) ...
我們查看driver的clone_image()
方法:
def clone_image(self, context, volume, image_location, image_meta, image_service): # iterate all locations to look for a cloneable one. for url_location in url_locations: if url_location and self._is_cloneable( url_location, image_meta): _prefix, pool, image, snapshot = self._parse_location(url_location) volume_update = self._clone(volume, pool, image, snapshot) volume_update["provider_location"] = None self._resize(volume) return volume_update, True return ({}, False)
rbd直接clone,這個過程和創建虛擬機基本一致。如果創建volume時指定了新的大小,則調用rbd resize執行擴容操作。
假設新創建的volume uuid為87ee1ec6-3fe4-413b-a4c0-8ec7756bf1b4
,glance image uuid為db2b6552-394a-42d2-9de8-2295fe2b3180
,則rbd命令為:
rbd clone openstack/db2b6552-394a-42d2-9de8-2295fe2b3180@snap openstack/volume-87ee1ec6-3fe4-413b-a4c0-8ec7756bf1b4if [[ -n "$size" ]]; then rbd resize --size $size openstack/volume-87ee1ec6-3fe4-413b-a4c0-8ec7756bf1b4fi
通過rbd命令驗證如下:
int32bit $ rbd info openstack/volume-87ee1ec6-3fe4-413b-a4c0-8ec7756bf1b4rbd image "volume-87ee1ec6-3fe4-413b-a4c0-8ec7756bf1b4": size 3072 MB in 768 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.affc488ac1a format: 2 features: layering, exclusive-lock, object-map, fast-diff, deep-flatten flags: create_timestamp: Wed Nov 22 13:07:50 2017 parent: openstack/db2b6552-394a-42d2-9de8-2295fe2b3180@snap overlap: 2048 MB
可見新創建的rbd image的parent為openstack/db2b6552-394a-42d2-9de8-2295fe2b3180@snap
。
註:其實我個人認為該方法需要執行flatten
操作,否則當有volume存在時,Glance不能刪除鏡像,相當於Glance服務依賴於Cinder服務狀態,這有點不合理。
4.3 創建快照
創建快照入口為cinder/volume/manager.py
的create_snapshot()
方法,該方法沒有使用taskflow框架,而是直接調用的driver create_snapshot()
方法,如下:
...try: utils.require_driver_initialized(self.driver) snapshot.context = context model_update = self.driver.create_snapshot(snapshot) ...except Exception: ...
RBDDriver
的create_snapshot()
方法非常簡單:
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)
因此volume的快照其實就是對應Ceph rbd image快照,假設snapshot uuid為e4e534fc-420b-45c6-8e9f-b23dcfcb7f86
,volume uuid為bf2d1c54-6c98-4a78-9c20-3e8ea033c3db
,則對應的rbd命令大致如下:
rbd -p openstack snap create volume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db@snapshot-e4e534fc-420b-45c6-8e9f-b23dcfcb7f86rbd -p openstack snap protect volume-bf2d1c54-6c98-4a78-9c20-3e8ea033c3db@snapshot-e4e534fc-420b-45c6-8e9f-b23dcfcb7f86
從這裡我們可以看出虛擬機快照和volume快照的區別,虛擬機快照需要從根磁碟rbd image快照中clone然後flatten,而volume的快照只需要創建rbd image快照,因此虛擬機快照通常需要數分鐘的時間,而volume快照能夠秒級完成。
4.4 創建volume備份
在了解volume備份之前,首先需要理清快照和備份的區別。我們可以通過git
類比,快照類似git commit
操作,只是表明數據提交了,主要用於回溯與回滾。當集群奔潰導致數據丟失,通常不能從快照中完全恢複數據。而備份則類似於git push
,把數據安全推送到了遠端存儲系統中,主要用於保證數據安全,即使本地數據丟失,也能從備份中恢復。Cinder的磁碟備份也支持多種存儲後端,這裡我們只考慮volume和backup driver都是Ceph的情況,其它細節可以參考Cinder數據卷備份原理與實踐。生產中volume和backup必須使用不同的Ceph集群,這樣才能保證當volume ceph集群掛了,也能從另一個集群中快速恢複數據。本文只是為了測試功能,因此使用的是同一個Ceph集群,通過pool區分,volume使用openstack
pool,而backup使用cinder_backup
pool。
另外,Cinder支持增量備份,用戶可以指定--incremental
參數決定使用的是全量備份還是增量備份。但是對於Ceph後端來說,Cinder總是先嘗試執行增量備份,只有當增量備份失敗時,才會fallback到全量備份,而不管用戶有沒有指定--incremental
參數。儘管如此,我們仍然把備份分為全量備份和增量備份兩種情況,注意只有第一次備份才有可能是全量備份,剩下的備份都是增量備份。
全量備份(第一次備份)
我們直接查看CephBackupDriver
的backup()
方法,代碼位於cinder/backup/drivers/ceph.py
。
if self._file_is_rbd(volume_file): # If volume an RBD, attempt incremental backup. LOG.debug("Volume file is RBD: attempting incremental backup.") try: updates = self._backup_rbd(backup, volume_file, volume.name, length) except exception.BackupRBDOperationFailed: LOG.debug("Forcing full backup of volume %s.", volume.id) do_full_backup = True
這裡主要判斷源volume是否是rbd,即是否使用Ceph後端,只有當volume也使用Ceph存儲後端情況下才能執行增量備份。
我們查看_backup_rbd()
方法:
from_snap = self._get_most_recent_snap(source_rbd_image)base_name = self._get_backup_base_name(volume_id, diff_format=True)image_created = Falsewith rbd_driver.RADOSClient(self, backup.container) as client: if base_name not in self.rbd.RBD().list(ioctx=client.ioctx): ... # Create new base image self._create_base_image(base_name, length, client) image_created = True else: ...
from_snap
為上一次備份時的快照點,由於我們這是第一次備份,因此from_snap
為None
,base_name
格式為volume-%s.backup.base
,這個base是做什麼的呢?我們查看下_create_base_image()
方法就知道了:
def _create_base_image(self, name, size, rados_client): old_format, features = self._get_rbd_support() self.rbd.RBD().create(ioctx=rados_client.ioctx, name=name, size=size, old_format=old_format, features=features, stripe_unit=self.rbd_stripe_unit, stripe_count=self.rbd_stripe_count)
可見base其實就是一個空卷,大小和之前的volume大小一致。
也就是說如果是第一次備份,在backup的Ceph集群首先會創建一個大小和volume一樣的空卷。
我們繼續看源碼:
def _backup_rbd(self, backup, volume_file, volume_name, length): ... new_snap = self._get_new_snap_name(backup.id) LOG.debug("Creating backup snapshot="%s"", new_snap) source_rbd_image.create_snap(new_snap) try: self._rbd_diff_transfer(volume_name, rbd_pool, base_name, backup.container, src_user=rbd_user, src_conf=rbd_conf, dest_user=self._ceph_backup_user, dest_conf=self._ceph_backup_conf, src_snap=new_snap, from_snap=from_snap) def _get_new_snap_name(self, backup_id): return utils.convert_str("backup.%s.snap.%s" % (backup_id, time.time()))
首先在源volume中創建了一個新快照,快照名為backup.${backup_id}.snap.${timestamp}
,然後調用了rbd_diff_transfer()
方法:
def _rbd_diff_transfer(self, src_name, src_pool, dest_name, dest_pool, src_user, src_conf, dest_user, dest_conf, src_snap=None, from_snap=None): src_ceph_args = self._ceph_args(src_user, src_conf, pool=src_pool) dest_ceph_args = self._ceph_args(dest_user, dest_conf, pool=dest_pool) cmd1 = ["rbd", "export-diff"] + src_ceph_args if from_snap is not None: cmd1.extend(["--from-snap", from_snap]) if src_snap: path = utils.convert_str("%s/%s@%s" % (src_pool, src_name, src_snap)) else: path = utils.convert_str("%s/%s" % (src_pool, src_name)) cmd1.extend([path, "-"]) cmd2 = ["rbd", "import-diff"] + dest_ceph_args rbd_path = utils.convert_str("%s/%s" % (dest_pool, dest_name)) cmd2.extend(["-", rbd_path]) ret, stderr = self._piped_execute(cmd1, cmd2) if ret: msg = (_("RBD diff op failed - (ret=%(ret)s stderr=%(stderr)s)") % {"ret": ret, "stderr": stderr}) LOG.info(msg) raise exception.BackupRBDOperationFailed(msg)
方法調用了rbd命令,先通過export-diff
子命令導出源rbd image的差量文件,然後通過import-diff
導入到backup的image中。
假設源volume的uuid為075c06ed-37e2-407d-b998-e270c4edc53c
,大小為1GB,backup uuid為db563496-0c15-4349-95f3-fc5194bfb11a
,這對應的rbd命令大致如下:
VOLUME_ID=075c06ed-37e2-407d-b998-e270c4edc53cBACKUP_ID=db563496-0c15-4349-95f3-fc5194bfb11arbd -p cinder_backup create --size 1024 volume-${VOLUME_ID}.backup.basenew_snap=volume-${VOLUME_ID}@backup.${BACKUP_ID}.snap.1511344566.67rbd -p openstack snap create ${new_snap}rbd export-diff --pool openstack ${new_snap} - | rbd import-diff --pool cinder_backup - volume-${VOLUME_ID}.backup.base
我們可以通過rbd命令驗證如下:
# volume ceph clusterint32bit $ rbd -p openstack snap ls volume-075c06ed-37e2-407d-b998-e270c4edc53cSNAPID NAME SIZE TIMESTAMP 52 backup.db563496-0c15-4349-95f3-fc5194bfb11a.snap.1511344566.67 1024 MB Wed Nov 22 17:56:15 2017# backup ceph clusterint32bit $ rbd -p cinder_backup ls -lNAME SIZE PARENT FMT PROT LOCKvolume-075c06ed-37e2-407d-b998-e270c4edc53c.backup.base 1024M 2volume-075c06ed-37e2-407d-b998-e270c4edc53c.backup.base@backup.db563496-0c15-4349-95f3-fc5194bfb11a.snap.1511344566.67 1024M 2
從輸出上看,源volume創建了一個快照,ID為52
,在backup的Ceph集群中創建了一個空卷volume-075c06ed-37e2-407d-b998-e270c4edc53c.backup.base
,並且包含一個快照backup.xxx.snap.1511344566.67
,該快照是通過import-diff
創建的。
增量備份
前面的過程和全量備份一樣,我們直接跳到_backup_rbd()
方法:
from_snap = self._get_most_recent_snap(source_rbd_image)with rbd_driver.RADOSClient(self, backup.container) as client: if base_name not in self.rbd.RBD().list(ioctx=client.ioctx): ... else: if not self._snap_exists(base_name, from_snap, client): errmsg = (_("Snapshot="%(snap)s" does not exist in base " "image="%(base)s" - aborting incremental " "backup") % {"snap": from_snap, "base": base_name}) LOG.info(errmsg) raise exception.BackupRBDOperationFailed(errmsg)
首先獲取源volume對應rbd image的最新快照最為parent,然後判斷在backup的Ceph集群的base中是否存在相同的快照(根據前面的全量備份,一定存在和源volume一樣的快照。
我們繼續看後面的部分:
new_snap = self._get_new_snap_name(backup.id)source_rbd_image.create_snap(new_snap)try: before = time.time() self._rbd_diff_transfer(volume_name, rbd_pool, base_name, backup.container, src_user=rbd_user, src_conf=rbd_conf, dest_user=self._ceph_backup_user, dest_conf=self._ceph_backup_conf, src_snap=new_snap, from_snap=from_snap) if from_snap: source_rbd_image.remove_snap(from_snap)
這個和全量備份基本是一樣的,唯一區別在於此時from_snap
不是None
,並且後面會刪掉from_snap
。_rbd_diff_transfer
方法可以翻前面代碼。
假設源volume uuid為075c06ed-37e2-407d-b998-e270c4edc53c
,backup uuid為e3db9e85-d352-47e2-bced-5bad68da853b
,parent backup uuid為db563496-0c15-4349-95f3-fc5194bfb11a
,則對應的rbd命令大致如下:
VOLUME_ID=075c06ed-37e2-407d-b998-e270c4edc53cBACKUP_ID=e3db9e85-d352-47e2-bced-5bad68da853bPARENT_ID=db563496-0c15-4349-95f3-fc5194bfb11arbd -p openstack snap create volume-${VOLUME_ID}@backup.${BACKUP_ID}.snap.1511348180.27 rbd export-diff --pool openstack --from-snap backup.${PARENT_ID}.snap.1511344566.67 openstack/volume-${VOLUME_ID}@backup.${BACKUP_ID}.snap.1511348180.27 - | rbd import-diff --pool cinder_backup - cinder_backup/volume-${VOLUME_ID}.backup.baserbd -p openstack snap rm volume-${VOLUME_ID}.backup.base@backup.${PARENT_ID}.snap.1511344566.67
我們通過rbd命令驗證如下:
int32bit $ rbd -p openstack snap ls volume-075c06ed-37e2-407d-b998-e270c4edc53cSNAPID NAME SIZE TIMESTAMP 53 backup.e3db9e85-d352-47e2-bced-5bad68da853b.snap.1511348180.27 1024 MB Wed Nov 22 18:56:20 2017int32bit $ rbd -p cinder_backup ls -lNAME SIZE PARENT FMT PROT LOCKvolume-075c06ed-37e2-407d-b998-e270c4edc53c.backup.base 1024M 2volume-075c06ed-37e2-407d-b998-e270c4edc53c.backup.base@backup.db563496-0c15-4349-95f3-fc5194bfb11a.snap.1511344566.67 1024M 2volume-075c06ed-37e2-407d-b998-e270c4edc53c.backup.base@backup.e3db9e85-d352-47e2-bced-5bad68da853b.snap.1511348180.27 1024M 2
和我們分析的結果一致,源volume的快照會刪除舊的而只保留最新的一個,backup則會保留所有的快照。
4.5 備份恢復
備份恢復是備份的逆過程,即從遠端存儲還原數據到本地。備份恢復的源碼位於cinder/backup/drivers/ceph.py
的restore()
方法,該方法直接調用了_restore_volume()
方法,因此我們直接看_restore_volume()
方法:
def _restore_volume(self, backup, volume, volume_file): length = int(volume.size) * units.Gi base_name = self._get_backup_base_name(backup.volume_id, diff_format=True) with rbd_driver.RADOSClient(self, backup.container) as client: diff_allowed, restore_point = self._diff_restore_allowed(base_name, backup, volume, volume_file, client)
其中_diff_restore_allowed()
是一個非常重要的方法,該方法判斷是否支持通過直接導入方式恢復,我們查看該方法實現:
def _diff_restore_allowed(self, base_name, backup, volume, volume_file, rados_client): rbd_exists, base_name = self._rbd_image_exists(base_name, backup.volume_id, rados_client) if not rbd_exists: return False, None restore_point = self._get_restore_point(base_name, backup.id) if restore_point: if self._file_is_rbd(volume_file): if volume.id == backup.volume_id: return False, restore_point if self._rbd_has_extents(volume_file.rbd_image): return False, restore_point return True, restore_point
從該方法中我們可以看出支持差量導入方式恢複數據,需要滿足以下所有條件:
- backup集群對應volume的rbd base image必須存在。
- 恢復點必須存在,即backup base image對應的快照必須存在。
- 恢複目標的volume必須是RBD,即volume的存儲後端也必須是Ceph。
- 恢複目標的volume必須是空卷,既不支持覆蓋已經有內容的image。
- 恢複目標的volume uuid和backup的源volume uuid不能是一樣的,即不能覆蓋原來的volume。
換句話說,雖然Cinder支持將數據還復到已有的volume(包括源volume)中,但如果使用Ceph後端就不支持增量恢復,導致效率會非常低。
因此如果使用Ceph存儲後端,官方文檔中建議將備份恢復到空卷中(不指定volume),不建議恢復到已有的volume中。
Note that Cinder supports restoring to a new volume or the original volume the backup was taken from. For the latter case, a full copy is enforced since this was deemed the safest action to take. It is therefore recommended to always restore to a new volume (default).
這裡假定我們恢復到空卷中,命令如下:
cinder backup-restore --name int32bit-restore-1 e3db9e85-d352-47e2-bced-5bad68da853b
注意我們沒有指定--volume
參數。此時執行增量恢復,代碼實現如下:
def _diff_restore_rbd(self, backup, restore_file, restore_name, restore_point, restore_length): rbd_user = restore_file.rbd_user rbd_pool = restore_file.rbd_pool rbd_conf = restore_file.rbd_conf base_name = self._get_backup_base_name(backup.volume_id, diff_format=True) before = time.time() try: self._rbd_diff_transfer(base_name, backup.container, restore_name, rbd_pool, src_user=self._ceph_backup_user, src_conf=self._ceph_backup_conf, dest_user=rbd_user, dest_conf=rbd_conf, src_snap=restore_point) except exception.BackupRBDOperationFailed: raise self._check_restore_vol_size(backup, restore_name, restore_length, rbd_pool)
可見增量恢復非常簡單,僅僅調用前面介紹的_rbd_diff_transfer()
方法把backup Ceph集群對應的base image的快照export-diff
到volume的Ceph集群中,並調整大小。
假設backup uuid為e3db9e85-d352-47e2-bced-5bad68da853b
,源volume uuid為075c06ed-37e2-407d-b998-e270c4edc53c
,目標volume uuid為f65cf534-5266-44bb-ad57-ddba21d9e5f9
,則對應的rbd命令為:
BACKUP_ID=e3db9e85-d352-47e2-bced-5bad68da853bSOURCE_VOLUME_ID=075c06ed-37e2-407d-b998-e270c4edc53cDEST_VOLUME_ID=f65cf534-5266-44bb-ad57-ddba21d9e5f9rbd export-diff --pool cinder_backup cinder_backup/volume-${SOURCE_VOLUME_ID}.backup.base@backup.${BACKUP_ID}.snap.1511348180.27 - | rbd import-diff --pool openstack - openstack/volume-${DEST_VOLUME_ID}rbd -p openstack resize --size ${new_size} volume-${DEST_VOLUME_ID}
如果不滿足以上5個條件之一,則Cinder會執行全量備份,全量備份就是一塊一塊數據寫入:
def _transfer_data(self, src, src_name, dest, dest_name, length): chunks = int(length / self.chunk_size) for chunk in range(0, chunks): before = time.time() data = src.read(self.chunk_size) dest.write(data) dest.flush() delta = (time.time() - before) rate = (self.chunk_size / delta) / 1024 # yield to any other pending backups eventlet.sleep(0) rem = int(length % self.chunk_size) if rem: dest.write(data) dest.flush() # yield to any other pending backups eventlet.sleep(0)
這種情況下效率很低,非常耗時,不建議使用。
5 總結
5.1 Glance
1. 上傳鏡像
rbd -p ${GLANCE_POOL} create --size ${SIZE} ${IMAGE_ID}rbd -p ${GLANCE_POOL} snap create ${IMAGE_ID}@snaprbd -p ${GLANCE_POOL} snap protect ${IMAGE_ID}@snap
2. 刪除鏡像
rbd -p ${GLANCE_POOL} snap unprotect ${IMAGE_ID}@snaprbd -p ${GLANCE_POOL} snap rm ${IMAGE_ID}@snaprbd -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}@snaprbd -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_snaprbd 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.baseNEW_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.baserbd -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}
推薦閱讀:
※Stateful firewall in OpenFlow based SDN
※VLAN Trunk in OpenStack Neutron and SDN
※優雅安裝OpenStack
※關於openstack的部署架構的一點兒疑問?