Hadoop中的文件格式大致上分為面向行和面向列兩類:
面向行:同一行的數據存儲在一起,即連續存儲。SequenceFile,MapFile,Avro Datafile。采用這種方式,如果只需要訪問行的一小部分數據,亦需要將整行讀入內存,推遲序列化一定程度上可以緩解這個問題,但是從磁盤讀取整行數據的開銷卻無法避免。面向行的存儲適合於整行數據需要同時處理的情況。
面向列:整個文件被切割為若干列數據,每一列數據一起存儲。Parquet , RCFile,ORCFile。面向列的格式使得讀取數據時,可以跳過不需要的列,適合於只處於行的一小部分字段的情況。但是這種格式的讀寫需要更多的內存空間,因為需要緩存行在內存中(為了獲取多行中的某一列)。同時不適合流式寫入,因為一旦寫入失敗,當前文件無法恢復,而面向行的數據在寫入失敗時可以重新同步到最后一個同步點,所以Flume采用的是面向行的存儲格式。


下面介紹幾種相關的文件格式,它們在Hadoop體系上被廣泛使用:
1. SequenceFile
SequenceFile是Hadoop API 提供的一種二進制文件,它將數據以的形式序列化到文件中。這種二進制文件內部使用Hadoop 的標准的Writable 接口實現序列化和反序列化。它與Hadoop API中的MapFile 是互相兼容的。Hive 中的SequenceFile 繼承自Hadoop API 的SequenceFile,不過它的key為空,使用value 存放實際的值, 這樣是為了避免MR 在運行map 階段的排序過程。如果你用Java API 編寫SequenceFile,並讓Hive 讀取的話,請確保使用value字段存放數據,否則你需要自定義讀取這種SequenceFile 的InputFormat class 和OutputFormat class。
SequenceFile的文件結構如下:

根據是否壓縮,以及采用記錄壓縮還是塊壓縮,存儲格式有所不同:
不壓縮:
按照記錄長度、Key長度、Value程度、Key值、Value值依次存儲。長度是指字節數。采用指定的Serialization進行序列化。
Record壓縮:
只有value被壓縮,壓縮的codec保存在Header中。
Block壓縮:
多條記錄被壓縮在一起,可以利用記錄之間的相似性,更節省空間。Block前后都加入了同步標識。Block的最小值由io.seqfile.compress.blocksize屬性設置。

2. Avro
Avro是一種用於支持數據密集型的二進制文件格式。它的文件格式更為緊湊,若要讀取大量數據時,Avro能夠提供更好的序列化和反序列化性能。並 且Avro數據文件天生是帶Schema定義的,所以它不需要開發者在API 級別實現自己的Writable對象。最近多個Hadoop 子項目都支持Avro 數據格式,如Pig 、Hive、Flume、Sqoop和Hcatalog。
3. RCFile
RCFile是Hive推出的一種專門面向列的數據格式。 它遵循“先按列划分,再垂直划分”的設計理念。當查詢過程中,針對它並不關心的列時,它會在IO上跳過這些列。需要說明的是,RCFile在map階段從 遠端拷貝仍然是拷貝整個數據塊,並且拷貝到本地目錄后RCFile並不是真正直接跳過不需要的列,並跳到需要讀取的列, 而是通過掃描每一個row group的頭部定義來實現的,但是在整個HDFS Block 級別的頭部並沒有定義每個列從哪個row group起始到哪個row group結束。所以在讀取所有列的情況下,RCFile的性能反而沒有SequenceFile高。
Hive的Record Columnar File,這種類型的文件先將數據按行划分成Row Group,在Row Group內部,再將數據按列划分存儲。其結構如下:

相比較於單純地面向行和面向列:


更詳細的介紹參考RCFile論文。
4. ORCFile
ORCFile(Optimized Record Columnar File)提供了一種比RCFile更加高效的文件格式。其內部將數據划分為默認大小為250M的Stripe。每個Stripe包括索引、數據和Footer。索引存儲每一列的最大最小值,以及列中每一行的位置。

在Hive中,如下命令用於使用ORCFile:
CREATE TABLE ... STORED AS ORC
ALTER TABLE ... SET FILEFORMAT ORC
SET hive.default.fileformat=ORC
5. Parquet
一種通用的面向列的存儲格式,基於Google的Dremel。特別擅長處理深度嵌套的數據。

對於嵌套結構,Parquet將其轉換為平面的列存儲,嵌套結構通過Repeat Level和Definition Level來表示(R和D),在讀取數據重構整條記錄的時候,使用元數據重構記錄的結構。下面是R和D的一個例子:
AddressBook { contacts: { phoneNumber: "555 987 6543" } contacts: { } } AddressBook {}

文件存儲大小比較與分析
我們選取一個TPC-H標准測試來說明不同的文件格式在存儲上的開銷。因為此數據是公開的,所以讀者如果對此結果感興趣,也可以對照后面的實驗自行 做一遍。Orders 表文本格式的原始大小為1.62G。 我們將其裝載進Hadoop 並使用Hive 將其轉化成以上幾種格式,在同一種LZO 壓縮模式下測試形成的文件的大小

表1:不同格式文件大小對比
從上述實驗結果可以看到,SequenceFile無論在壓縮和非壓縮的情況下都比原始純文本TextFile大,其中非壓縮模式下大11%, 壓縮模式下大6.4%。這跟SequenceFile的文件格式的定義有關: SequenceFile在文件頭中定義了其元數據,元數據的大小會根據壓縮模式的不同略有不同。一般情況下,壓縮都是選取block 級別進行的,每一個block都包含key的長度和value的長度,另外每4K字節會有一個sync-marker的標記。對於TextFile文件格 式來說不同列之間只需要用一個行間隔符來切分,所以TextFile文件格式比SequenceFile文件格式要小。但是TextFile 文件格式不定義列的長度,所以它必須逐個字符判斷每個字符是不是分隔符和行結束符。因此TextFile 的反序列化開銷會比其他二進制的文件格式高幾十倍以上。
RCFile文件格式同樣也會保存每個列的每個字段的長度。但是它是連續儲存在頭部元數據塊中,它儲存實際數據值也是連續的。另外RCFile 會每隔一定塊大小重寫一次頭部的元數據塊(稱為row group,由hive.io.rcfile.record.buffer.size控制,其默認大小為4M),這種做法對於新出現的列是必須的,但是如 果是重復的列則不需要。RCFile 本來應該會比SequenceFile 文件大,但是RCFile 在定義頭部時對於字段長度使用了Run Length Encoding進行壓縮,所以RCFile 比SequenceFile又小一些。Run length Encoding針對固定長度的數據格式有非常高的壓縮效率,比如Integer、Double和Long等占固定長度的數據類型。在此提一個特例—— Hive 0.8引入的TimeStamp 時間類型,如果其格式不包括毫秒,可表示為”YYYY-MM-DD HH:MM:SS”,那么就是固定長度占8個字節。如果帶毫秒,則表示為”YYYY-MM-DD HH:MM:SS.fffffffff”,后面毫秒的部分則是可變的。
Avro文件格式也按group進行划分。但是它會在頭部定義整個數據的模式(Schema), 而不像RCFile那樣每隔一個row group就定義列的類型,並且重復多次。另外,Avro在使用部分類型的時候會使用更小的數據類型,比如Short或者Byte類型,所以Avro的數 據塊比RCFile 的文件格式塊更小。
序列化與反序列化開銷分析
我們可以使用Java的profile工具來查看Hadoop 運行時任務的CPU和內存開銷。以下是在Hive 命令行中的設置:
hive>set mapred.task.profile=true;
hive>set mapred.task.profile.params =-agentlib:hprof=cpu=samples,heap=sites, depth=6,force=n,thread=y,verbose=n,file=%s
當map task 運行結束后,它產生的日志會寫在$logs/userlogs/job- 文件夾下。當然,你也可以直接在JobTracker的Web界面的logs或jobtracker.jsp 頁面找到日志。
我們運行一個簡單的SQL語句來觀察RCFile 格式在序列化和反序列化上的開銷:
hive> select O_CUSTKEY,O_ORDERSTATUS from orders_rc2 where O_ORDERSTATUS='P';
其中的O_CUSTKEY列為integer類型,O_ORDERSTATUS為String類型。在日志輸出的最后會包含內存和CPU 的消耗。
下表是一次CPU 的開銷:

表2:一次CPU的開銷
其中第五列可以對照上面的Track信息查看到底調用了哪些函數。比如CPU消耗排名20的函數對應Track:
TRACE 315554: (thread=200001) org.apache.hadoop.hive.ql.io.RCFile$Reader.getCurrentRow(RCFile.java:1434) org.apache.hadoop.hive.ql.io.RCFileRecordReader.next(RCFileRecordReader.java:88) org.apache.hadoop.hive.ql.io.RCFileRecordReader.next(RCFileRecordReader.java:39)org.apache.hadoop.hive.ql.io.CombineHiveRecordReader.doNext(CombineHiveRecordReader.java:98)org.apache.hadoop.hive.ql.io.CombineHiveRecordReader.doNext(CombineHiveRecordReader.java:42) org.apache.hadoop.hive.ql.io.HiveContextAwareRecordReader.next(HiveContextAwareRecordReader.java:67)
其中,比較明顯的是RCFile,它為了構造行而消耗了不必要的數組移動開銷。其主要是因為RCFile 為了還原行,需要構造RowContainer,順序讀取一行構造RowContainer,然后給其中對應的列進行賦值,因為RCFile早期為了兼容 SequenceFile所以可以合並兩個block,又由於RCFile不知道列在哪個row group結束,所以必須維持數組的當前位置,類似如下格式定義:
Array>
而此數據格式可以改為面向列的序列化和反序列化方式。如:
Map,array,array....>
這種方式的反序列化會避免不必要的數組移動,當然前提是我們必須知道列在哪個row group開始到哪個row group結束。這種方式會提高整體反序列化過程的效率。