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