天天看點

海量小檔案存儲與Ceph實踐

 海量小檔案存儲(簡稱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)進行存儲,如下圖所示,對象的擴充屬性資料與對象資料存儲在同一塊盤上;

海量小檔案存儲與Ceph實踐

  使用本結構存儲後,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++描述):

海量小檔案存儲與Ceph實踐
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());      
海量小檔案存儲與Ceph實踐

  對于寫小檔案可直接使用WriteFullObj;對于寫大檔案可使用帶offset的Write,寫完所有資料後,調用WriteFinish;對于讀取整個檔案可直接使用ReadFullObj;對于随機讀取檔案部分資料可使用帶offset的Read;Stat用于檢視檔案狀态資訊;Remove用于删除檔案;當使用第二種hash規則時,可使用BatchWriteFullObj提高寫操作的吞吐量。

------------------------------------

轉載  http://www.cnblogs.com/wuhuiyuan/p/ceph-small-file-compound-storage.html

繼續閱讀