Cinder磁盤備份原理與實踐
一、背景
1.1 數(shù)據(jù)保護(hù)技術(shù)概述
快照(Snapshot)、復(fù)制(Replication)、備份(Backup)是存儲領(lǐng)域中最為常見的數(shù)據(jù)保護(hù)技術(shù)。快照用于捕捉數(shù)據(jù)卷在某一個時刻的狀態(tài),用戶可以隨時回滾到這個狀態(tài),也可以基于該快照創(chuàng)建新的數(shù)據(jù)卷。備份就是對數(shù)據(jù)進(jìn)行導(dǎo)出拷貝并傳輸?shù)竭h(yuǎn)程存儲設(shè)備中。當(dāng)數(shù)據(jù)損壞時,用戶可以從遠(yuǎn)端下載備份的數(shù)據(jù),手動從備份數(shù)據(jù)中恢復(fù),從而避免了數(shù)據(jù)損失。快照類似于git的commit操作,我們可以隨時reset/checkout到任意歷史commit中,但一旦保存git倉庫的磁盤損壞,提交的commit信息將永久丟失,不能恢復(fù)。而備份則類似于git的push操作,即使本地的數(shù)據(jù)損壞,我們也能從遠(yuǎn)端的git倉庫中恢復(fù)。簡而言之,快照主要用于快速回溯,而備份則用于容災(zāi),還能避免誤刪除操作造成數(shù)據(jù)丟失。數(shù)據(jù)復(fù)制則類似于mysql的master/slave主從同步,通常只有master支持寫操作,slave不允許用戶直接寫數(shù)據(jù),它只負(fù)責(zé)自動同步master的數(shù)據(jù),但一旦master出現(xiàn)故障,slave能夠提升為master接管寫操作。因此復(fù)制不僅提供了實時備份的功能,還實現(xiàn)了故障自動恢復(fù)(即高可用)。
1.2 Cinder數(shù)據(jù)保護(hù)功能介紹
Cinder是OpenStack中相對成熟的組件(總分為8分的成熟度評分中獲得了8分滿分),也是OpenStack的核心組件之一,為OpenStack云主機(jī)提供彈性的塊存儲服務(wù),承載著用戶大多數(shù)的業(yè)務(wù)數(shù)據(jù),即使出現(xiàn)數(shù)據(jù)的絲毫損壞也將可能導(dǎo)致災(zāi)難性后果,因此數(shù)據(jù)的完整性保護(hù)至關(guān)重要。不得不說Cinder對數(shù)據(jù)卷保護(hù)方面支持度還是比較給力的,目前Cinder已經(jīng)同時支持了對數(shù)據(jù)卷的快照、復(fù)制和備份功能。
快照應(yīng)該是Cinder非常熟悉非常受歡迎的功能了,也是Cinder默認(rèn)支持的功能,幾乎所有的存儲后端都支持快照。而備份作為Cinder的可選功能之一,由于數(shù)據(jù)卷的存儲后端很多已經(jīng)提供了多副本功能(比如Ceph存儲后端默認(rèn)為三副本),通常很少人會再部署一套備份存儲集群,因此部署率并不是很高。復(fù)制也是Cinder的可選功能之一,目前支持的存儲后端還非常有限,最常采用的RBD存儲后端也是在Ocata版本才開始支持,并且要求Ceph版本需要支持rbd-mirror(jewel版本以上),因此受到用戶的關(guān)注度還不是很高,部署率較低。
二、深入理解Cinder數(shù)據(jù)卷備份原理
2.1 cinder backup功能介紹
cinder磁盤備份為用戶的數(shù)據(jù)卷實例提供備份和恢復(fù)功能,實現(xiàn)了基于塊的容災(zāi)功能。從K版本開始,Cinder引入了增量備份功能,相對全量備份需要拷貝和傳輸整個數(shù)據(jù)卷,增量備份只需要傳輸變化的部分,大大節(jié)省了傳輸開銷和存儲開銷。通常情況下,當(dāng)用戶執(zhí)行備份或者恢復(fù)操作時,需要手動卸載數(shù)據(jù)卷,即數(shù)據(jù)卷不支持在掛載狀態(tài)下熱備份。從L版本開始,新增了force選項,當(dāng)用戶指定force選項時能夠?qū)燧d的數(shù)據(jù)卷強(qiáng)制執(zhí)行備份操作,這樣可能帶來數(shù)據(jù)不一致的風(fēng)險,不過社區(qū)針對這種情況做了些優(yōu)化,比如在創(chuàng)建備份前先基于該數(shù)據(jù)卷快照創(chuàng)建臨時數(shù)據(jù)卷,然后基于臨時數(shù)據(jù)卷執(zhí)行后續(xù)備份操作。
Cinder開啟備份功能,需要單獨部署cinder-backup服務(wù)。cinder-backup服務(wù)和cinder-volume服務(wù)類似,也支持各種不同的驅(qū)動,對接不同的存儲后端,目前支持的存儲驅(qū)動列表如下:
- swift,備份數(shù)據(jù)保存在OpenStack Swift對象存儲中。
- google,備份數(shù)據(jù)保存在Google Cloud Storage(GCS)中。
- glusterfs,保存到glusterfs中。
- nfs,保存到NFS中。
- posix,保存到本地文件系統(tǒng)。
- tsm,保存在IBM Tivoli Storage Manager(TSM)。
- ceph,保存到ceph集群中。
從列表中看,目前cinder backup尚不支持備份數(shù)據(jù)到AWS S3中。
除了數(shù)據(jù)卷本身的備份,cinder backup還支持將元數(shù)據(jù)序列化導(dǎo)出(export record),這樣即使數(shù)據(jù)庫中的數(shù)據(jù)丟失了,也能從導(dǎo)出的元數(shù)據(jù)中快速恢復(fù)。
2.2 cinder backup原理剖析
前面提到cinder backup支持多種后端存儲驅(qū)動,但大體可以分為兩類:
- 存儲系統(tǒng)本身就提供塊存儲服務(wù),比如ceph。這種情況只需要直接導(dǎo)入到該存儲系統(tǒng)即可。
- 存儲系統(tǒng)不支持塊存儲服務(wù),只支持基于文件的存儲,以上除了ceph和tsm都屬于此類。此時備份采取了分塊備份策略,即首先把數(shù)據(jù)卷切割為一個個獨立的文件,然后分別把這些文件存儲到設(shè)備中?;謴?fù)時只需要重組這些小文件即可。
接下來我們針對此兩種情況深入研究下cinder backup的實現(xiàn)原理。
2.2.1 分塊備份策略在介紹之前先了解兩個重要的參數(shù)
- chunk_size: 表示將volume切割成多大的塊進(jìn)行備份,一個塊稱為一個chunk。在NFS中這個值叫做backup_file_size,默認(rèn)是1999994880Byte,大約1.8G。在Swift中這個值叫做backup_swift_object_size,默認(rèn)是52428800Byte,也就是50M。這個參數(shù)決定數(shù)據(jù)卷備份后塊的數(shù)量(Object Count),比如一個2GB的數(shù)據(jù)卷,如果chunk_size為500MB,則大約需要4個塊,如果使用本地文件系統(tǒng)存儲的話,對應(yīng)就是4個文件。
- sha_block_size: 這個值用于增量備份,決定多大的塊求一次hash,hash相同說明內(nèi)容沒有變化,不需要備份。它決定了增量備份的粒度。在NFS中,這個值叫做backup_sha_block_size_bytes,在Swift中,這個值叫做backup_swift_block_size。默認(rèn)都是32768Byte,也就是32K。在Ceph,沒有對應(yīng)的概念。
對一個數(shù)據(jù)卷做全量備份時,每次從數(shù)據(jù)卷讀入chunk_size字節(jié)的數(shù)據(jù)構(gòu)成一個chunk,然后每sha_block_size個字節(jié)做一次sha計算,并將結(jié)果保存起來,最后把chunk_size的數(shù)據(jù)進(jìn)行壓縮(可以配置不壓縮)后保存到對應(yīng)的存儲系統(tǒng)上,這就形成了NFS上的一個文件或者Swift中的一個object[6]。如此循環(huán)直到把整個數(shù)據(jù)卷都備份到存儲系統(tǒng)。
那恢復(fù)的時候怎么重組呢?這就需要保存元數(shù)據(jù)信息,元數(shù)據(jù)信息包括:
- backup信息:其實就是數(shù)據(jù)庫中的信息,或者說就是一個backup object實例的序列化,包括backup name、description、volume_id等。
- volume信息:數(shù)據(jù)卷信息,即volume實例的序列化,包括size、name等。
- 塊信息:即objects信息,這是最重要的數(shù)據(jù),記錄了每一個塊的長度、偏移量、壓縮算法、md5值,備份恢復(fù)時主要通過這些塊信息拼接而成。
- 版本:序列化和持久化必不可少的參數(shù)。
除了保存以上元數(shù)據(jù)信息,還會按順序保存每一個block的sha256值。這些信息主要用于支持增量備份。做增量備份時,也是每次從數(shù)據(jù)卷讀入chunk_size字節(jié)的chunk數(shù)據(jù),然后計算該chunk的每個block的sha值。不同的是,Cinder會把每一個block的sha值與其父備份對應(yīng)的sha值比較,僅當(dāng)該block的sha值與父備份block的sha值不一樣時,才保存對應(yīng)的block數(shù)據(jù)。如果sha值和父備份的sha值相同,說明這個block的數(shù)據(jù)沒有更新,不需要重新保存該block數(shù)據(jù),而只需要保存sha值。當(dāng)然,如果有多個連續(xù)block的sha值都不一樣,則保存時會合并成一個object,通過元數(shù)據(jù)記錄該object在原volume的偏移量以及長度。
如圖1所示,假設(shè)一個chunk分為9個block,每個block為100KB,注意每個block都保存了sha256值,圖中沒有標(biāo)識?;谠揷hunk做一次增量備份后,假設(shè)只有block 2、7、8有更新,則增量備份只會保存block 2、7、8,由于7和8是連續(xù)的,因此會自動合并成一個chunk,而block 2單獨形成一個chunk,即原來的chunk分裂成了兩個chunk,但總大小為300KB,節(jié)省了1/3的存儲空間。
圖1 增量備份原理圖
備份的恢復(fù)參考文獻(xiàn)[6]講得非常清楚,這里直接引用:
全量備份的恢復(fù)很簡單,只需要找到對應(yīng)的備份,將其內(nèi)容寫回對應(yīng)的volume即可。那么這里有個問題,每個備份都對應(yīng)存儲上哪些文件呢,每個文件又對于原始volume中哪些數(shù)據(jù)?還記得創(chuàng)建備份時生成的metadata文件嗎,答案就在其中?;謴?fù)備份時,會讀取這個文件,然后將每一個備份文件恢復(fù)到對應(yīng)的位置。當(dāng)然,如果有壓縮也會根據(jù)metadata中的描述,先解壓再寫入對應(yīng)的volume中。
增量備份的恢復(fù)稍微復(fù)雜一些,正如之前的描述,增量備份之間有依賴,會形成備份鏈,我們需要恢復(fù)所選備份及其在備份鏈上之前所有的數(shù)據(jù)。在恢復(fù)時,需要查詢數(shù)據(jù)庫,獲取當(dāng)前備份及備份鏈上之前的所有備份,其順序是[所選備份,父備份,父父備份,…,全量備份],恢復(fù)的時候會按照相反的順序依次進(jìn)行,即首先恢復(fù)全量備份,接著創(chuàng)建的第一個增量備份,第二個增量備份,直到所選的備份。每個備份恢復(fù)時依賴創(chuàng)建備份時生成的metadata文件,將備份包含的文件,恢復(fù)到volume中。每個備份的恢復(fù)和全量備份的恢復(fù)方式一樣。
從備份的原理可以看出,增量備份能夠節(jié)省存儲空間,但隨著備份鏈長度越來越長,恢復(fù)時會越來越慢,性能越來越差,實際生產(chǎn)環(huán)境中應(yīng)該權(quán)衡存儲空間和性能,控制備份鏈的長度。
Swift、NFS、本地文件系統(tǒng)、GCS等都是使用以上的備份策略,實際上實現(xiàn)也是完全一樣的,區(qū)別僅僅在于實現(xiàn)不同存儲系統(tǒng)的Reader、Writer驅(qū)動。
2.2.2 直接導(dǎo)入策略
直接導(dǎo)入策略即把原數(shù)據(jù)卷導(dǎo)出后直接導(dǎo)入到目標(biāo)存儲系統(tǒng)中。對于支持差量導(dǎo)入的存儲系統(tǒng),增量備份時則可以進(jìn)一步優(yōu)化。
以Ceph為例,我們知道Ceph RBD支持將某個image在不同時刻的狀態(tài)進(jìn)行比較后導(dǎo)出(export-diff)補(bǔ)丁(patch)文件,然后可以隨時將這個補(bǔ)丁文件打到某個image中(import-diff)。即ceph原生支持差量備份,利用該特性實現(xiàn)增量備份就不難了。不過有個前提是,必須保證cinder-volume后端和cinder-backup后端都是ceph后端,否則仍然是一塊一塊的全量拷貝。
如果是對volume進(jìn)行第一次備份,則:
- 在用于備份的ceph集群創(chuàng)建一個base image,size和原volume一樣,name為"volume-VOLUMD_UUID.backup.base" % volume_id。
- 在原volume創(chuàng)建一個新的快照,name為backup.BACKUP_ID.snap.TIMESTRAMP。
- 在原RBD image上使用export-diff命令導(dǎo)出與創(chuàng)建時比較的差量數(shù)據(jù),然后通過管道將差量數(shù)據(jù)導(dǎo)入剛剛在備份集群上新創(chuàng)建的RBD image中。
如果不是對volume第一次備份,則:
- 在原volume中找出滿足r"^backup\.([a-z0-9\-]+?)\.snap\.(.+)$"的最近的一次快照。
- 在原volume創(chuàng)建一個新的快照,name為backup.BACKUP_ID.snap.TIMESTRAMP。
- 在原RBD image上使用export-diff命令導(dǎo)出與最近的一次快照比較的差量數(shù)據(jù),然后通過管道將差量數(shù)據(jù)導(dǎo)入到備份集群的RBD image中。
恢復(fù)時相反,只需要從備份集群找出對應(yīng)的快照并導(dǎo)出差量數(shù)據(jù),導(dǎo)入到原volume即可。
注意:
- volume和backup都使用ceph后端存儲時,每次都會嘗試使用增量備份,無論用戶是否傳遞incremental參數(shù)值。
- 使用直接導(dǎo)入策略,不需要元數(shù)據(jù)信息以及sha256信息。
三、踩過的“坑”
雖然在前期做了大量關(guān)于cinder backup的調(diào)研工作,但實際部署過程中仍然踩了不少坑,PoC測試過程也非一帆風(fēng)順,還好我們在填坑的過程還是比較順利的。本小節(jié)總結(jié)我們在實踐過程中遇到的坑,避免后來者重復(fù)踩“坑”。
3.1 熱備份導(dǎo)致quota值異常
我們知道備份是一個IO開銷和網(wǎng)絡(luò)開銷都比較大的操作,非常耗時。當(dāng)對已經(jīng)掛載的數(shù)據(jù)卷執(zhí)行在線備份時,Cinder為了優(yōu)化性能,減少數(shù)據(jù)不一致的風(fēng)險,首先會基于該數(shù)據(jù)卷創(chuàng)建一個臨時卷,然后基于臨時卷創(chuàng)建備份,備份完成時會自動刪除臨時數(shù)據(jù)卷。從代碼中看,創(chuàng)建臨時卷時并沒有計算quota,換句話說,創(chuàng)建的臨時磁盤是不占用quota值的。但刪除時調(diào)用的是標(biāo)準(zhǔn)的刪除接口,該接口會釋放對應(yīng)數(shù)據(jù)卷占用的數(shù)據(jù)卷quota值(主要影響gigabytes和volumes值)。也就是說,創(chuàng)建的臨時磁盤使volume quota值只減不增,用戶可以通過這種方式繞過quota限制。目前該問題社區(qū)還未修復(fù),已提交bug:https://bugs.launchpad.net/cinder/+bug/1670636。
3.2 不支持ceph多后端情況
我們內(nèi)部cinder對接了多個ceph集群,不同的ceph集群通過不同的配置文件區(qū)分。但cinder-backup服務(wù)向cinder-volume服務(wù)獲取connection info時并沒有返回ceph的配置文件路徑,于是cinder-backup服務(wù)使用默認(rèn)的配置文件/etc/ceph/ceph.conf,該ceph集群顯然找不到對應(yīng)volume的RBD image,因此在多backend情況下可能導(dǎo)致備份失敗。不過該問題在新版本中不存在了。
3.3 使用ceph存儲后端時不支持差量備份
我們前面提到如果cinder-volume和cinder-backup后端都是ceph,則會利用ceph內(nèi)置的rbd差量備份實現(xiàn)增量備份。那cinder-backup服務(wù)怎么判斷數(shù)據(jù)卷對應(yīng)的后端是否ceph呢?實現(xiàn)非常簡單,只需要判斷數(shù)據(jù)卷的連接信息是否存在rbd_image屬性,實現(xiàn)代碼如下:
- def _file_is_rbd(self, volume_file):
- """Returns True if the volume_file is actually an RBD image."""
- return hasattr(volume_file, 'rbd_image')
社區(qū)從M版開始把與存儲后端交互的代碼獨立出來,建立了一個新的項目–os-brick,與之前的ceph驅(qū)動存在不兼容,沒有rbd_image這個屬性。因此backup服務(wù)會100%判斷數(shù)據(jù)卷不是ceph后端,因此100%執(zhí)行全量備份。該問題社區(qū)還未完全修復(fù),可參考https://bugs.launchpad.net/cinder/+bug/1578036。
四、我們的改進(jìn)
4.1 獲取父備份ID
當(dāng)備份存在子備份時,用戶無法直接刪除該備份,而必須先刪除所有依賴的子備份。目前Cinder API只返回備份是否存在依賴的子備份,而沒有返回子備份的任何信息,也沒有返回父備份的信息。當(dāng)用戶創(chuàng)建了很多備份實例時,很難弄清楚備份之間的父子關(guān)系。我們修改了Cinder API,向用戶返回備份的父備份id(parent_id),并且支持基于parent_id過濾搜索備份。當(dāng)用戶發(fā)現(xiàn)備份存在依賴時,能夠快速檢索出被依賴的子備份。當(dāng)然,如果存在很長的父子關(guān)系時,需要一層一層判斷,仍然不太方便,并且不能很清楚的輸出備份的父子關(guān)系。于是我們引入了備份鏈的概念,下節(jié)詳細(xì)討論。
4.2 引入備份鏈概念
為了方便查看備份之間的父子關(guān)系,我們引入了備份鏈(backup chain)的概念,一個數(shù)據(jù)卷可以有多個備份鏈,每條備份鏈包括一個全量備份以及多個增量備份組成。我們新增了兩個API,其中一個API輸出指定數(shù)據(jù)卷的備份鏈列表,另一個API輸出指定備份鏈的所有備份點,按照父子關(guān)系輸出。目前我們的備份鏈只支持線性鏈,暫時不支持分叉的情況。通過備份鏈,用戶能夠非常方便地查看備份之間的父子關(guān)系和備份時間序列,如圖2。
圖2 備份鏈展示
4.3 創(chuàng)建備份時指定備份鏈
創(chuàng)建增量備份時,默認(rèn)是基于時間戳選擇最新的備份點作為父備份,我們擴(kuò)展了該特性,支持用戶選擇在指定備份鏈上創(chuàng)建備份,這樣也可以避免備份鏈過長的情況。
五、后續(xù)工作
Cinder backup功能已經(jīng)相對比較完善了,但仍然存在一些功能不能滿足客戶需求,我們制定了二期規(guī)劃,主要工作包括如下:
5.1 級聯(lián)刪除
目前Cinder不支持備份的級聯(lián)刪除,即如果一個備份實例存在依賴的子備份,則不能刪除該備份,必須先刪除其依賴的所有子備份。如果備份鏈很長時,刪除備份時非常麻煩。在二期規(guī)劃中,我們將實現(xiàn)備份的級聯(lián)刪除功能,通過指定--force選項,支持刪除備份以及其依賴的所有備份,甚至刪除整個備份鏈。
5.2 獲取增量備份大小
目前Cinder備份的實例大小是繼承自原volume的大小,基于分塊策略備份還有Object Count(chunk 數(shù)量)的概念,但這只是顯示分成了幾個chunk,每個chunk大小不一定是一樣的,并不能根據(jù)chunk數(shù)量計算實際占用的存儲空間。備份存儲空間是我們計費(fèi)系統(tǒng)的計量標(biāo)準(zhǔn)之一,全量備份和增量備份成本肯定是不一樣的,如果價錢一樣,則用戶并不一定樂于使用增量備份。在二期規(guī)劃中,我們將實現(xiàn)計算備份占用的實際存儲空間的接口。
5.3 備份到S3
很多私有云用戶考慮各種成本,不一定會部署額外用于備份的Ceph集群,也不一定需要Swift對象存儲,而更傾向于將數(shù)據(jù)備份到價格低廉、穩(wěn)定可靠的AWS S3中。目前Cinder backup后端還不支持S3接口,為了滿足客戶需求,我們計劃在二期中實現(xiàn)S3接口,用戶可以隨時把volume數(shù)據(jù)備份到S3中。
六、總結(jié)
本文首先介紹了數(shù)據(jù)保護(hù)的幾種常用技術(shù),指出備份是數(shù)據(jù)保護(hù)的關(guān)鍵技術(shù)之一。接下來重點介紹了cinder backup的原理,對比了基于分塊備份策略和直接導(dǎo)入策略。然后吐槽了我們在實踐中踩到的各種“坑”。最后分享了我們做的一些優(yōu)化改進(jìn)工作以及后期工作。
【本文是51CTO專欄作者“付廣平”的原創(chuàng)文章,如需轉(zhuǎn)載請通過51CTO獲得聯(lián)系】