原文地址:http://www.javashuo.com/article/p-wkdoqgno-bv.htmlhtml
在介紹文件存儲方案以前,我以爲有必要先介紹下關於HDFS存儲架構方面的一些知識,在對架構有初步瞭解後,纔會明白爲何要單獨針對小文件展開介紹,小文件存儲和其它文件存儲區別在什麼地方。java
這裏我只是就Hadoop生態中的存儲層展開介紹,對於其它部分本文暫未描述。衆所周知,HDFS是目前很是流行的分佈式文件存儲系統,其邏輯架構以下圖所示:
node
HDFS也是典型的Master/Slave結構,其中,Master至關於Namenode,Slave至關於Datanode。git
Namenode 負責元數據管理,維護文件和目錄樹,響應Client請求;Datanode負責實際數據存儲。至於什麼是元數據,是怎麼管理的,後續會單獨寫一篇文章來介紹程序員
Block是文件塊,HDFS中是以Block爲單位進行文件的管理的,一個文件可能有多個塊,每一個塊默認是3個副本,這些塊分別存儲在不一樣機器上。塊與文件以前的映射關係會定時上報Namenode。HDFS中一個塊的默認大小是64M,其大小由參數dfs.block.size
控制。這裏面先引伸幾個問題出來:github
- 問題1:塊大小要怎麼設置爲一個合理值,過大設置和太小設置有什麼影響?
- 問題2:若是一個文件小於所設置的塊大小,實際佔用空間會怎樣?
- 問題3:一個Namenode最多能管理多少個塊,何時會達到瓶頸?
針對這些問題,後面會展開介紹,這裏仍是先關注下架構方面。針對塊方面,有幾個單位概念須要弄清楚: Block、Packet和Chunk。Block上面有描述,Packet和Chunk以下:apache
1 Packet: 其比塊要小不少,能夠理解爲Linux操做系統最小盤塊概念,通常爲64KB,由參數
dfs.write.packet.size
控制,是client向Datanode寫入數據的粒度,即client向Datanode寫數據時不是一次以Block爲單位寫的,而是被分紅若干Packet,放入pipeline順序追加寫入到Block中,示意圖以下:網絡
2 Chunk: 比Packet更小,是針對Packet數據校驗粒度來設計的,通常是512B,由參數
io.bytes.per.checksum
控制,同時還帶有一個4B的校驗值,因此能夠認爲一個Chunk是516B架構
上面說到Chunk是針對數據校驗的,那一個Packet有多少個chunk校驗呢,若是Packet默認是64KB, 那計算公式爲:chunk個數=64KB/516B=128
。也就是對於一個Packet來講,數據值與校驗值比例大概爲128:1, 對於一個塊來講,假設是64M,會對應512KB的校驗文件。併發
Packet的示意圖中還一個Header信息,實際存儲的是Packet的元數據信息,包括Packet在block中的offset, 數據長度,校驗編碼等。
瞭解塊相關概念後,再介紹下HDFS的寫入流程,以下圖所示:
- client向Namenode發起寫文件RPC請求;
- Namenode檢查要寫的文件是否已存在元數據中,存在則拒絕寫入;同時檢查寫入用戶權限,如無權限也拒絕寫入;若文件不存在且有權限寫入,則Namenode會建立一條文件記錄,響應client端容許寫入文件;
- client根據文件大小分紅若干塊,並在Namenode中申請塊所存放的Datanode位置,若是是3副本存儲,則Namenode會選擇3臺符合條件的結點放到結點隊列中;client實際向Datanode寫數據時是以Packet爲單位來寫到Block的,這裏面會涉及兩個隊列,分別爲:data packet隊列和ack packet隊列,Packet會同時入數據隊列和ack隊列;
- 經過DataStreamer對象將數據寫入pipeline中的第一個Datanode,並依次寫入到其它兩個結點;當三個結點packet都寫成功後,會將packets 從ack queue中刪除;
- 寫操做完成後,client調用close()關閉寫操做,並通知Namenode關閉寫操做,至此,整個寫操做完成。
packet寫入流示意圖以下所示:
HDFS的讀流程比較簡單,流程程以下所示:
- client 向Namenode發起讀文件RPC請求;
- Namenode返回相應block所在datanode的位置信息;
- client經過位置信息調用FSDataInputStream API的Read方法從datanode中並行讀取block信息,如圖中4和5所示,選擇block的其中一副本返回client。
在對HDFS的讀寫流程有一個基礎瞭解後,下面針對文件塊存儲相關內容展開介紹。瞭解塊的設計、存儲和元數據相關知識對於設計小文件存儲方案也相當重要。
有人可能會問,集羣存儲有大文件也有小文件,那塊大小該如何設計呢,這裏應該要考慮2個準則:
1.減小內存佔用:對於Namenode來講,單機內存畢竟有限,文件塊越多,元數據信息越大,佔用內存越多,若是文件數量級很大的話,單機將沒法管理;
2.減小硬盤尋道時間: 數據塊在硬盤爲連續存儲,對於普通SATA盤,隨機尋址較慢, 若是塊設置太小,一個文件的塊總數會越多,意味着硬盤尋址時間會加長,天然吞吐量沒法知足要求;若是塊設置過大,一方面對於普通盤來講IO性能也比較差,加載時會很慢,另外一方面,塊過大,對於多副原本說,在副本出問題時,系統恢復時間越長。
因此設置合理的塊大小也很重要,通常來講根據集羣的需求來設定,好比對於使用到HBase的場景,通常數據量會比較大,塊不宜設置過小,參考值通常爲128MB或256MB,這樣能儘可能避免頻繁塊刷寫和塊元數據信息的膨脹;對於存儲小文件的場景,如圖片,塊可設置成默認64MB大小,一個塊中存儲多個圖片文件,後面會詳細介紹。
塊在HDFS中是怎麼存儲的呢,上面有提到多副本機制,即一個塊在HDFS中是根據dfs.replication參數所設置的值來肯定副本數的,默認爲3。三個副本是隨機存儲三臺數據結點Datanode上,三個結點的選取遵循機架感知策略,經過topology.script.file.name來設置, 若是配置中未配置機架感知,Namenode是沒法知道機房網絡拓撲,因此會隨機選取3臺結點進行塊存儲,若是設置了機架感知,則在存儲時會在同機架存儲2副本,不一樣機架放第3個副本,這樣一旦一個機架出現問題,還能保證一個副本是可用的。
若是一個文件只有幾K,且小於HDFS塊大小,實際在HDFS佔用的空間會是多少呢?答案是文件大小即爲實際佔用空間,對於幾K的文件實際佔用的空間大小也爲幾K,不會佔用一個塊空間。
上面提到,在存儲的文件數量級很大時,單機Namenode內存消耗會急劇增大,易觸發單機瓶頸,那麼到底一個Namenode能夠管理多少許級的元數據呢,其實這個能夠有一個公式來初略估算。這裏首先要了解一個概念,元數據包括哪些,正常元數據包括三個部分:文件、目錄和塊。這三部分在元數據中各佔用多少空間呢,下面是一個初略的計算:
- 單條元數據大小:文件約250B,目錄約290B,塊約368B(152B+72*副本數3)
- 集羣元數據總條數:文件數約10000個,目錄約5000個,塊約20000個
- 總佔用內存大小: 250B10000+290B5000+368B*20000=10.78M
實際內存消耗會比這多,由於還有其它一些信息須要存儲,整體內存消耗可根據上述公式來估算,這樣你就知道你集羣Namenode能承受多少文件,目錄和塊元數據信息的存儲。也能及時發現內存瓶頸,作到精細化監控運營管理。
上述介紹的三個方面也分別解答了上面提到的三個問題,具體細節這裏也不過多展開。下面正式展開對小文件存儲方面的介紹
針對小文件問題,HDFS自身也有考慮這種場景,目前已知的主要有三種方案來實現這種存儲,分別以下:
HAR
SequenceFile
CombinedFile
HAR熟稱Hadoop歸檔文件,文件以*.har結尾。歸檔的意思就是將多個小文件歸檔爲一個文件,歸檔文件中包含元數據信息和小文件內容,即從必定程度上將Namenode管理的元數據信息下沉到Datanode上的歸檔文件中,避免元數據的膨脹。
歸檔文件是怎麼生成的呢,主要仍是依賴於MapReduce原理將小文件內容進行歸併。歸檔文件的大概組成以下所示:
圖中,左邊是原始小文件,右邊是har組成。主要包括:_masterindex
、_index
、part-0...part-n
。其中_masterindex和_index就是相應的元數據信息,part-0...part-n就是相應的小文件內容。實際在集羣中的存儲結構以下:
經過hadoop archive
命令建立歸檔文件,-archiveName指定文件名, -p指定原文件路徑,-r指定要歸檔的小文件,最後指定hdfs中歸檔文件存放路徑,以下所示:
建立後,會在/usr/archive目錄下生成test.har目錄,這裏你們可能會有疑惑,上面不是說Har是一個文件嗎,這裏怎麼又是目錄了呢,其實咱們所說的歸檔文件是邏輯上的概念,而實際的har是一個目錄,是一個物理存儲概念,因此你們只要記住在實際存儲時生成的Har其實是一個目錄就好了。這個目錄中會存放元數據,實際文件內容。以下圖所示,_index文件的每一行表示的是小文件在part開頭的映射關係,包括起始和結束位置,是在哪一個part文件等,這樣在讀取har中的小文件時,根據offset位置可直接獲得小文件內容,如圖part-0文件內容所示:
要從HAR讀取一個小文件的話,須要用distcp方式,原理也是mapreduce, 指定har路徑和輸出路徑,命令以下:hadoop distcp har:///user/archive/test.har/file-1 /tmp/archive/output
HAR整體比較簡單,它有什麼缺點呢?
1.archive文件一旦建立不可修改即不能append,若是其中某個小文件有問題,得解壓處理完異常文件後從新生成新的archive文件;
2.對小文件歸檔後,原文件並未刪除,須要手工刪除;
3.建立HAR和解壓HAR依賴MapReduce,查詢文件時耗很高;
4.歸檔文件不支持壓縮。
SequenceFile本質上是一種二進制文件格式,相似key-value存儲,經過map/reducer的input/output format方式生成。文件內容由Header、Record/Block、SYNC標記組成,根據壓縮的方式不一樣,組織結構也不一樣,主要分爲Record組織模式和Block組織模式。
Record組織模式又包含兩種:未壓縮狀態CompressionType.NONE, 和壓縮狀態CompressionType.RECORD,未壓縮是指不對數據記錄進行壓縮,壓縮態是指對每條記錄的value進行壓縮,其邏輯結構以下所示:
Record結構中包含Record長度、key長度、key值和value值。Sync充斥在Record之間,其做用主要是用於文件位置定位,具體定位方式是:若是提供的文件讀取位置不是記錄的邊界可能在一個Record中間,在實際定位時會定位到所提供位置處以後的第一個Sync邊界位置,並從該Sync點日後讀相應長度的數據,若是提供的讀取位置日後沒有Sync邊界點,則直接跳轉文件末尾;若是提供的文件讀取位置是Record邊界,則直接從該位置開始讀取指定長度的數據。另外一種文件定位方式是seek, 這種方式則要求所提供的讀取位置是record的邊界位置,否則在讀取迭代讀取下一個位置時會出錯。
Block組織模式,其壓縮態爲CompressionType.BLOCK。與Record模式不一樣的時,Block是以塊爲單位進行壓縮,即將多條Record寫到一個塊中,當達到必定大小時,對該塊進行壓縮,很顯然,塊的壓縮效率會比Record要高不少,避免大量消費IO和CPU等資源。其邏輯結構以下:
從上圖中可看出,組織方式變成了塊,一個塊中又包含了塊的記錄數,key長度,key值,value長度,value值。每一個塊之間也有Sync標記,做用同Record方式。
兩中模式中,都有header標記,包含了些如版本信息、KEY類名、VALUE類名、是否壓縮標記、是否塊壓縮標記、編碼類、元數據信息和Sync標記,其結構以下:
這裏以存儲5個小的圖片文件爲例,演示下如何建立SequenceFile。首先將圖片文件上傳至hdfs的一個目錄。
其次,編寫一個MR程序來對上述圖片進行轉換,將生成的文件存放到/tmp/sequencefile/seq下,MR程序源碼在附件SmallFiles.zip中,可自行查看,以下所示:
轉換後,會在/tmp/sequencefile/seq目錄生成一個part-r-00000文件,這個文件裏面就包含了上述5個圖片文件的內容,以下所示:
若是要從該SequenceFile中獲取全部圖片文件,再經過MR程序從文件中將圖片文件取出,以下所示:
- A.支持基於記錄或塊的數據壓縮;
- B. 支持splitable,可以做爲mr 的輸入分片;
- C. 不用考慮具體存儲格式,寫入讀取較簡單.
- A. 須要一個合併文件的過程
- B. 依賴於MapReduce
- C. 二進制文件,合併後不方便查看
其原理也是基於Map/Reduce將原文件進行轉換,經過CombineFileInputFormat類將多個文件分別打包到一個split中,每一個mapper處理一個split, 提升併發處理效率,對於有大量小文件的場景,經過這種方式能快速將小文件進行整合。最終的合併文件是將多個小文件內容整合到一個文件中,每一行開始包含每一個小文件的完整hdfs路徑名,這就會出現一個問題,若是要合併的小文件不少,那麼最終合併的文件會包含過多的額外信息,浪費過多的空間,因此這種方案目前相對用得比較少,下面是使用CombineFile的示例:
hbaseadmin@10-163-161-229:~/program/mr/input> ls hbaseadmin@10-163-161-229:~/program/mr/input> touch file-1 file-2 file-3 hbaseadmin@10-163-161-229:~/program/mr/input> echo "this is file-1" >file-1 hbaseadmin@10-163-161-229:~/program/mr/input> echo "this is file-2" >file-2 hbaseadmin@10-163-161-229:~/program/mr/input> echo "this is file-3" >file-3 hbaseadmin@10-163-161-229:~/program/mr> hadoop fs -put input /tmp/combinefile/ hbaseadmin@10-163-161-229:~/program/mr> hadoop jar SmallFiles.jar com.fit.dba.mr.util.CombineFileTest /tmp/combinefile/input/ /tmp/combinefile/output hbaseadmin@10-163-161-229:~/program/mr> hadoop fs -ls /tmp/combinefile/output Found 2 items -rw-r--r-- 1 hbaseadmin supergroup 0 2018-03-25 17:26 /tmp/combinefile/output/_SUCCESS -rw-r--r-- 1 hbaseadmin supergroup 213 2018-03-25 17:26 /tmp/combinefile/output/part-r-00000 hbaseadmin@10-163-161-229:~/program/mr> hadoop fs -ls /tmp/combinefile/output/part-r-00000 Found 1 items -rw-r--r-- 1 hbaseadmin supergroup 213 2018-03-25 17:26 /tmp/combinefile/output/part-r-00000 hbaseadmin@10-163-161-229:~/program/mr> hadoop fs -cat /tmp/combinefile/output/part-r-00000 hdfs://10-163-161-229:9000/tmp/combinefile/input/file-1 this is file-1 hdfs://10-163-161-229:9000/tmp/combinefile/input/file-2 this is file-2 hdfs://10-163-161-229:9000/tmp/combinefile/input/file-3 this is file-3
上述用到的轉換程序也在附件CombineFileTest.java中。其優勢是適用於處理大量比block小的文件和內容比較少的文件合併,尤爲是文本類型/sequencefile等文件合併,其缺點是:若是沒有合理的設置maxSplitSize,minSizeNode,minSizeRack,則可能會致使一個map任務須要大量訪問非本地的Block形成網絡開銷,反而比正常的非合併方式更慢。
上面介紹了三種基於HDFS自身的一些方案,每種方案各有優缺點,其核心思想都是基於map/reduce的方式將多個文件合併成一個文件。在實際使用中,單純用上述方案仍是不太方便,下面簡要介紹下目前其它的一些小文件存儲方案。
HBase咱們知道主要是key/value存儲結構,一個key對應多個列族的多個列值。從2.0版本開始,HBase多了一個MOB的結構,具體參考HBase-11339。具體是什麼概念呢,先來看下示意圖:
上圖是一個關於HBase的架構圖,包含HBase的幾個組件master、regionserver、hdfs、hfile等。MOB FILE相似StoreFile, 它做爲一個單獨的對象存儲小文件。MOB具體結構以下所示:
MOB是由StoreFile和MOB File共同組成。其中,StoreFile存放的數據和HBase正常存儲的數據同樣,key/value結構,不過value中存儲的是關於MOB文件的長度,存放路徑等元數據信息,在MOB File中存儲的是具體的MOB文件內容,這樣經過StoreFile中的key/value能夠找到MOB所存放的文件具體位置和大小,最終獲得文件內容。
MOB是怎麼設置呢,在建立表時咱們指定表的MOB屬性,以下所示:create 't1', {NAME => 'f1', IS_MOB => true, MOB_THRESHOLD => 102400}
其中,MOB_THRESHOLD表示MOB對象所能存儲的文件對象上限閾值,推薦是存儲小於10M的文件。對於MOB的表,咱們能夠手動觸發壓縮,有compact_mob和major_compact_mob兩種方式。以下所示:compact_mob 't1' compact_mob 't1','cf1' major_compact_mob 't1' major_compact_mob 't1','cf1'
MOB的出現大大提升了咱們使用HBase存儲小文件的效率,這樣無須關注底層HDFS是怎麼存儲的,只要關注上層邏輯便可,HBase的強大優點也能保證存儲的高可靠和穩定性,管理也方便。
這種方案是目前兄弟部門正在使用的一個小圖片存儲方案,也是基於HDFS存儲實際圖片,基於HBase存儲元數據信息。這個方案中,主要也是基於壓縮的思路,將多個小圖上片壓縮成一個tar文件存放至HDFS上,經過HBASE記錄文件名和HDFS文件的位置映射關係,架構示意圖以下:
其具體思路是:
1.業務部門將圖片上傳至一箇中起色,圖片按日期目錄存儲,不一樣日期上傳的圖片放到相應日期目錄;
2.按期用腳本去將日期目錄打包成tar,一天的圖片打包成一個以日期命名的tar, tar文件解壓後是直接圖片文件,即不帶日期那層目錄,上傳至HDFS指定目錄;
3.經過tar文件解析程序獲取tar文件中各圖片文件在tar中的偏移量和長度,這個解析程序最開始是由國外一個程序員Tom Wallroth寫的工具,具體地址能夠訪問:http://github.com/devsnd/tarindexer。 這個工具能夠直接在tar文件上解析tar中各文件的偏移量和長度,很方便。
4.獲得圖片文件在tar的偏移量和長度後,設計HBase rowkey, 將圖片名和tar路徑設計到rowkey中,並經過在rowkey前綴加鹽方式使rowkey隨機散列分佈在HBase中,避免熱點現象;HBase的value存儲的是文件的偏移量和長度。這樣HBase中就保存了文件的元數據信息;
- 業務方查詢具體某個圖片時,根據圖片的日期和圖片名,先計算出HBase rowkey,再去HBase獲取該圖片的偏移量和長度;經過偏移量和長度經過HDFS的API去讀取HDFS的文件。
下面針對目前行業內用到的其它一些方案做下對比,以下圖所示。
整體來講,淘寶的TFS是功能最全的,同時支持大小文件的存儲;Ceph也是一種流行的分佈式文件存儲方案,組內對其調研後感受比較複雜,不太好管理,不太穩定;FastDFS比較簡單,適合存儲一些使用場景簡單的文件,不太靈活;其它幾種沒用過,你們可自行上網參閱相關資料。
本文介紹了關於HDFS小文件存儲的方案,不一樣方案各具特色,在使用時要根據實際業務場景進行設計,對於既要存儲大文件又要存儲小文件的場景,我建議在上層做一個邏輯處理層,在存儲時先判斷是大文件仍是小文件,再決定是否用打包壓縮仍是直接上傳至HDFS,可借鑑TFS方案。
1 http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
關於文章中用的MR轉換程序以下所示:https://github.com/ballwql/common-tool/tree/master/java