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,表示在openstackpool中test-volumeimage的快照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-2int32bit-test-1的children,而int32bit-test-1int32bit-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源碼。

閱讀完本文可以理解以下幾個問題:

  1. 為什麼上傳的鏡像必須要轉化為raw格式?
  2. 如何高效上傳一個大的鏡像文件?
  3. 為什麼能夠實現秒級創建虛擬機?
  4. 為什麼創建虛擬機快照需要數分鐘時間,而創建volume快照能夠秒級完成?
  5. 為什麼當有虛擬機存在時,不能刪除鏡像?
  6. 為什麼一定要把備份恢復到一個空卷中,而不能覆蓋已經存在的volume?
  7. 從鏡像中創建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.pyspawn方法,其中創建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的。我們查看Rbdclone()方法,代碼位於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:

  1. Glance中的rbd image location不合法,rbd location必須包含fsid、pool、image id,snapshot 4個欄位,欄位通過/劃分。
  2. Glance和Nova對接的是不同的Ceph集群。
  3. Glance鏡像非raw格式。
  4. 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

啟示

  1. 創建虛擬機時並沒有拷貝鏡像,也不需要下載鏡像,而是一個簡單clone操作,因此創建虛擬機基本可以在秒級完成。
  2. 如果鏡像中還有虛擬機依賴,則不能刪除該鏡像,換句話說,刪除鏡像之前,必須刪除基於該鏡像創建的所有虛擬機。

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不做任何區別。

虛擬機的快照由libvirtdriver的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.pydestroy方法:

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_fnlambda disk: disk.startswith(instance.uuid),即所有以虛擬機uuid開頭的disk(rbd image)。需要注意,這裡沒有調用imagebackendRbd 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.pyVolumeDriver類定義的介面,Cinder就可以對接該存儲系統。

Cinder不僅支持本地volume的管理,還能把本地volume備份到遠端存儲系統中,比如備份到另一個Ceph集群或者Swift對象存儲系統中,本文將只考慮從源Ceph集群備份到遠端Ceph集群中的情況。

4.2 創建volume

創建volume由cinder-volume服務完成,入口為cinder/volume/manager.pycreate_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_namevolume-${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)

我們查看RBDDrivercreate_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_snapshotFalse(默認值),則對應的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 > 0depth < 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.pycreate_snapshot()方法,該方法沒有使用taskflow框架,而是直接調用的driver create_snapshot()方法,如下:

...try: utils.require_driver_initialized(self.driver) snapshot.context = context model_update = self.driver.create_snapshot(snapshot) ...except Exception: ...

RBDDrivercreate_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使用openstackpool,而backup使用cinder_backuppool。

另外,Cinder支持增量備份,用戶可以指定--incremental參數決定使用的是全量備份還是增量備份。但是對於Ceph後端來說,Cinder總是先嘗試執行增量備份,只有當增量備份失敗時,才會fallback到全量備份,而不管用戶有沒有指定--incremental參數。儘管如此,我們仍然把備份分為全量備份和增量備份兩種情況,注意只有第一次備份才有可能是全量備份,剩下的備份都是增量備份。

全量備份(第一次備份)

我們直接查看CephBackupDriverbackup()方法,代碼位於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_snapNonebase_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.pyrestore()方法,該方法直接調用了_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的部署架構的一點兒疑問?

TAG:Ceph | OpenStack | 云计算 |