如何基于 Ceph 攻破海量小文件存儲(chǔ)難題
海量小文件存儲(chǔ)(簡(jiǎn)稱LOSF,lots of small files)出現(xiàn)后,就一直是業(yè)界的難題,眾多博文(如 [1] )對(duì)此問(wèn)題進(jìn)行了闡述與分析,許多互聯(lián)網(wǎng)公司也針對(duì)自己的具體場(chǎng)景研發(fā)了自己的存儲(chǔ)方案(如taobao開(kāi)源的 TFS ,facebook自主研發(fā)的 Haystack ),還有一些公司在現(xiàn)有開(kāi)源項(xiàng)目(如hbase,fastdfs,mfs等)基礎(chǔ)上做針對(duì)性改造優(yōu)化以滿足業(yè)務(wù)存儲(chǔ)需求;
一. 海量小文件存儲(chǔ)側(cè)重于解決的問(wèn)題
通過(guò)對(duì)若干分布式存儲(chǔ)系統(tǒng)的調(diào)研、測(cè)試與使用,與其它分布式系統(tǒng)相比,海量小文件存儲(chǔ)更側(cè)重于解決兩個(gè)問(wèn)題:
1. 海量小文件的元數(shù)據(jù)信息組織與管理: 對(duì)于百億量級(jí)的數(shù)據(jù),每個(gè)文件元信息按100B計(jì)算,元信息總數(shù)據(jù)量為1TB,遠(yuǎn)超過(guò)目前單機(jī)服務(wù)器內(nèi)存大小;若使用本地持久化設(shè)備存儲(chǔ),須高效滿足每次 文件存取請(qǐng)求的元數(shù)據(jù)查詢尋址(對(duì)于上層有cdn的業(yè)務(wù)場(chǎng)景,可能不存在明顯的數(shù)據(jù)熱點(diǎn)),為了避免單點(diǎn),還要有備用元數(shù)據(jù)節(jié)點(diǎn);同時(shí),單組元數(shù)據(jù)服務(wù)器 也成為整個(gè)集群規(guī)模擴(kuò)展的瓶頸;或者使用獨(dú)立的存儲(chǔ)集群存儲(chǔ)管理元數(shù)據(jù)信息,當(dāng)數(shù)據(jù)存儲(chǔ)節(jié)點(diǎn)的狀態(tài)發(fā)生變更時(shí),應(yīng)該及時(shí)通知相應(yīng)元數(shù)據(jù)信息進(jìn)行變更;
對(duì)此問(wèn)題,tfs/fastdfs設(shè)計(jì)時(shí),就在文件名中包含了部分元數(shù)據(jù)信息,減小了元數(shù)據(jù)規(guī)模,元數(shù)據(jù)節(jié)點(diǎn)只負(fù)責(zé)管理粒度更大的分片結(jié)構(gòu)信息; 商用分布式文件系統(tǒng)龍存,通過(guò)升級(jí)優(yōu)化硬件,使用分布式元數(shù)據(jù)架構(gòu)——多組(每組2臺(tái))高性能ssd服務(wù)器——存儲(chǔ)集群的元數(shù)據(jù)信息,滿足單次io元數(shù)據(jù) 查詢的同時(shí),也實(shí)現(xiàn)了元數(shù)據(jù)存儲(chǔ)的擴(kuò)展性;Haystack Directory模塊提供了圖片邏輯卷到物理卷軸的映射存儲(chǔ)與查詢功能,使用Replicated Database存儲(chǔ),并通過(guò)cache集群來(lái)降低延時(shí)提高并發(fā),其對(duì)外提供的讀qps在百萬(wàn)量級(jí);
2. 本地磁盤(pán)文件的存儲(chǔ)與管理(本地存儲(chǔ)引擎):對(duì)于常見(jiàn)的linux文件系統(tǒng),讀取一個(gè)文件通常需要三次磁盤(pán)IO(讀取目錄元數(shù)據(jù)到內(nèi)存,把文件的 inode節(jié)點(diǎn)裝載到內(nèi)存,***讀取實(shí)際的文件內(nèi)容);按目前主流2TB~4TB的sata盤(pán),可存儲(chǔ)2kw~4kw個(gè)100KB大小的文件,由于文件數(shù) 太多,無(wú)法將所有目錄及文件的inode信息緩存到內(nèi)存,很難實(shí)現(xiàn)每個(gè)圖片讀取只需要一次磁盤(pán)IO的理想狀態(tài),而長(zhǎng)尾現(xiàn)象使得熱點(diǎn)緩存無(wú)明顯效果;當(dāng)請(qǐng)求 尋址到具體的一塊磁盤(pán),如何減少文件存取的io次數(shù),高效地響應(yīng)請(qǐng)求(尤其是讀)已成為需要解決的另一問(wèn)題;
對(duì)此問(wèn)題,有些系統(tǒng)(如tfs,Haystack)采用了小文件合并存儲(chǔ)+索引文件的優(yōu)化方案,此方案有若干益處:a.合并后的合并大文件通常在 64MB,甚至更大,單盤(pán)所存存儲(chǔ)的合并大文件數(shù)量遠(yuǎn)小于原小文件的數(shù)量,其inode等信息可以全部被cache到內(nèi)存,減少了一次不必要的磁盤(pán) IO;b.索引文件通常數(shù)據(jù)量(通常只存儲(chǔ)小文件所在的合并文件,及offset和size等關(guān)鍵信息)很小,可以全部加載到內(nèi)存中,讀取時(shí)先訪問(wèn)內(nèi)存索 引數(shù)據(jù),再根據(jù)合并文件、offset和size訪問(wèn)實(shí)際文件數(shù)據(jù),實(shí)現(xiàn)了一次磁盤(pán)IO的目的;c.單個(gè)小文件獨(dú)立存儲(chǔ)時(shí),文件系統(tǒng)存儲(chǔ)了其guid、屬 主、大小、創(chuàng)建日期、訪問(wèn)日期、訪問(wèn)權(quán)限及其它結(jié)構(gòu)信息,有些信息可能不是業(yè)務(wù)所必需的,在合并存儲(chǔ)時(shí),可根據(jù)實(shí)際需要對(duì)文件元數(shù)據(jù)信息裁剪后在做合并, 減少空間占用。除了合并方法外,還可以使用性能更好的SSD等設(shè)備,來(lái)實(shí)現(xiàn)高效響應(yīng)本地io請(qǐng)求的目標(biāo)。
當(dāng)然,在合并存儲(chǔ)優(yōu)化方案中,刪除或修改文件操作可能無(wú)法立即回收存儲(chǔ)空間,對(duì)于存在大量刪除修改的業(yè)務(wù)場(chǎng)景,需要再做相應(yīng)的考量。
二. 海量小文件存儲(chǔ)與Ceph實(shí)踐
Ceph 是近年越來(lái)越被廣泛使用的分布式存儲(chǔ)系統(tǒng),其重要的創(chuàng)新之處是基于 CRUSH 算法的計(jì)算尋址,真正的分布式架構(gòu)、無(wú)中心查詢節(jié)點(diǎn),理論上無(wú)擴(kuò)展上限(更詳細(xì)ceph介紹見(jiàn)網(wǎng)上相關(guān)文章);Ceph的基礎(chǔ)組件RADOS本身是對(duì)象存 儲(chǔ)系統(tǒng),將其用于海量小文件存儲(chǔ)時(shí),CRUSH算法直接解決了上面提到的***個(gè)問(wèn)題;不過(guò)Ceph OSD目前的存儲(chǔ)引擎( Filestore , KeyValuestore )對(duì)于上面描述的海量小文件第二個(gè)問(wèn)題尚不能很好地解決;ceph社區(qū)曾對(duì)此問(wèn)題做過(guò)描述并提出了基于rgw的一種 方案 (實(shí)際上,在實(shí)現(xiàn)本文所述方案過(guò)程中,發(fā)現(xiàn)了社區(qū)上的方案),不過(guò)在***代碼中,一直未能找到方案的實(shí)現(xiàn);
我們?cè)贔ilestore存儲(chǔ)引擎基礎(chǔ)上對(duì)小文件存儲(chǔ)設(shè)計(jì)了優(yōu)化方案并進(jìn)行實(shí)現(xiàn),方案主要思路如下:將若干小文件合并存儲(chǔ)在RADOS系統(tǒng)的一個(gè) 對(duì)象(object)中,<小文件的名字、小文件在對(duì)象中的offset及小文件size>組成kv對(duì),作為相應(yīng)對(duì)象的擴(kuò)展屬性(或者 omap,本文以擴(kuò)展屬性表述,ceph都使用kv數(shù)據(jù)庫(kù)實(shí)現(xiàn),如leveldb)進(jìn)行存儲(chǔ),如下圖所示,對(duì)象的擴(kuò)展屬性數(shù)據(jù)與對(duì)象數(shù)據(jù)存儲(chǔ)在同一塊盤(pán)上;
使用本結(jié)構(gòu)存儲(chǔ)后,write小文件file_a操作分解為: 1)對(duì)某個(gè)object調(diào)用append小文件file_a;2)將小文件file_a在相應(yīng)object的offset和size,及小文件名字 file_a作為object的擴(kuò)展屬性存儲(chǔ)kv數(shù)據(jù)庫(kù)。read小文件file_a操作分解為:1)讀取相應(yīng)object的file_a對(duì)應(yīng)的擴(kuò)展屬性 值(及offset,size);2)讀取object的offset偏移開(kāi)始的size長(zhǎng)度的數(shù)據(jù)。對(duì)于刪除操作,直接將相應(yīng)object的 file_a對(duì)應(yīng)的擴(kuò)展屬性鍵值刪除即可,file_a所占用的存儲(chǔ)空間延遲回收,回收以后討論。另外,Ceph本身是強(qiáng)一致存儲(chǔ)系統(tǒng),其內(nèi)在機(jī)制可以保 證object及其擴(kuò)展屬性數(shù)據(jù)的可靠一致;
由于對(duì)象的擴(kuò)展屬性數(shù)據(jù)與對(duì)象數(shù)據(jù)存儲(chǔ)在同一塊盤(pán)上,小文件的讀寫(xiě)操作全部在本機(jī)本OSD進(jìn)程內(nèi)完成,避免了網(wǎng)絡(luò)交互機(jī)制潛在的問(wèn)題。另一方面, 對(duì)于寫(xiě)操作,一次小文件寫(xiě)操作對(duì)應(yīng)兩次本地磁盤(pán)隨機(jī)io(邏輯層面),且不能更少,某些kv數(shù)據(jù)庫(kù)(如leveldb)還存在write amplification問(wèn)題,對(duì)于寫(xiě)壓力大的業(yè)務(wù)場(chǎng)景,此方案不能很好地滿足;不過(guò)對(duì)于讀操作,我們可以通過(guò)配置參數(shù),盡量將kv數(shù)據(jù)保留在內(nèi)存中, 實(shí)現(xiàn)讀取操作一次磁盤(pán)io的預(yù)期目標(biāo);
如何選擇若干小文件進(jìn)行合并,及合并存儲(chǔ)到哪個(gè)對(duì)象中呢?最簡(jiǎn)單地方案是通過(guò)計(jì)算小文件key的hash值,將具有相同hash值的小文件合并存 儲(chǔ)到id為對(duì)應(yīng)hash值的object中,這樣每次存取時(shí),先根據(jù)key計(jì)算出hash值,再對(duì)id為hash值的object進(jìn)行相應(yīng)的操作;關(guān)于 hash函數(shù)的選擇,(1)可使用最簡(jiǎn)單的hash取模,這種方法需要事先確定模數(shù),即當(dāng)前業(yè)務(wù)合并操作使用的object個(gè)數(shù),且確定后不能改變,在業(yè) 務(wù)數(shù)據(jù)增長(zhǎng)過(guò)程中,小文件被平均分散到各個(gè)object中,寫(xiě)壓力被均勻分散到所有object(即所有物理磁盤(pán),假設(shè)object均勻分布) 上;object文件大小在一直增長(zhǎng),但不能***增長(zhǎng),上限與單塊磁盤(pán)容量及存儲(chǔ)的object數(shù)量有關(guān),所以在部署前,應(yīng)規(guī)劃好集群的容量和hash模 數(shù)。(2)對(duì)于某些帶目錄層次信息的數(shù)據(jù),如/a/b/c/d/efghi.jpg,可以將文件的目錄信息作為相應(yīng)object的id,及/a/b/c /d,這樣一個(gè)子目錄下的所有文件存儲(chǔ)在了一個(gè)object中,可以通過(guò)rados的listxattr命令查看一個(gè)目錄下的所有文件,方便運(yùn)維使用;另 外,隨著業(yè)務(wù)數(shù)據(jù)的增加,可以動(dòng)態(tài)增加object數(shù)量,并將之前的object設(shè)為只讀狀態(tài)(方便以后的其它處理操作),來(lái)避免object的***增 長(zhǎng);此方法需要根據(jù)業(yè)務(wù)寫(xiě)操作量及集群磁盤(pán)數(shù)來(lái)合理規(guī)劃當(dāng)前可寫(xiě)的object數(shù)量,在滿足寫(xiě)壓力的前提下將object大小控制在一定范圍內(nèi)。
本方案是為小文件(1MB及以下)設(shè)計(jì)的,對(duì)于稍大的文件存儲(chǔ)(幾十MB甚至更大),如何使用本方案存儲(chǔ)呢?我們將大文件 large_file_a切片分成若干大小一樣(如2MB,可配置,***一塊大小可能不足2MB)的若干小塊文 件:large_file_a_0, large_file_a_1 ... large_file_a_N,并將每個(gè)小塊文件作為一個(gè)獨(dú)立的小文件使用上述方案存儲(chǔ),分片信息(如總片數(shù),當(dāng)前第幾片,大文件大小,時(shí)間等) 附加在每個(gè)分片數(shù)據(jù)開(kāi)頭一并進(jìn)行存儲(chǔ),以便在讀取時(shí)進(jìn)行解析并根據(jù)操作類型做相應(yīng)操作。
根據(jù)業(yè)務(wù)的需求,我們提供如下操作接口供業(yè)務(wù)使用(c++描述):
- int WriteFullObj(const std::string& oid, bufferlist& bl, int create_time = GetCurrentTime());
- int Write(const std::string& oid, bufferlist& bl, uint64_t off, int create_time = GetCurrentTime());
- int WriteFinish(const std::string& oid, uint64_t total_size, int create_time = GetCurrentTime());
- int Read(const std::string& oid, bufferlist& bl, size_t len, uint64_t off);
- int ReadFullObj(const std::string& oid, bufferlist& bl, int* create_time = NULL);
- int Stat(const std::string& oid, uint64_t *psize, time_t *pmtime, MetaInfo* meta = NULL);
- int Remove(const std::string& oid);
- int BatchWriteFullObj(const String2BufferlistHMap& oid2data, int create_time = GetCurrentTime());
對(duì)于寫(xiě)小文件可直接使用WriteFullObj;對(duì)于寫(xiě)大文件可使用帶offset的Write,寫(xiě)完所有數(shù)據(jù)后,調(diào)用 WriteFinish;對(duì)于讀取整個(gè)文件可直接使用ReadFullObj;對(duì)于隨機(jī)讀取部分文件可使用帶offset的Read;Stat用于查看文 件狀態(tài)信息;Remove用于刪除文件;當(dāng)使用第二種hash規(guī)則時(shí),可使用BatchWriteFullObj提高寫(xiě)操作的吞吐量。