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