(轉載)淺析Hadoop檔案格式
Hadoop 中的檔案格式
1 SequenceFile
SequenceFile是Hadoop API 提供的一種二進位制檔案,它將資料以<key,value>的形式序列化到檔案中。這種二進位制檔案內部使用Hadoop 的標準的Writable 介面實現序列化和反序列化。它與Hadoop API中的MapFile 是互相相容的。Hive 中的SequenceFile 繼承自Hadoop API 的SequenceFile,不過它的key為空,使用value 存放實際的值, 這樣是為了避免MR 在執行map 階段的排序過程。如果你用Java API 編寫SequenceFile,並讓Hive 讀取的話,請確保使用value欄位存放資料,否則你需要自定義讀取這種SequenceFile 的InputFormat class 和OutputFormat class。
圖1:Sequencefile 檔案結構
2 RCFile
RCFile是Hive推出的一種專門面向列的資料格式。 它遵循“先按列劃分,再垂直劃分”的設計理念。當查詢過程中,針對它並不關心的列時,它會在IO上跳過這些列。需要說明的是,RCFile在map階段從遠端拷貝仍然是拷貝整個資料塊,並且拷貝到本地目錄後RCFile並不是真正直接跳過不需要的列,並跳到需要讀取的列, 而是通過掃描每一個row group的頭部定義來實現的,但是在整個HDFS Block 級別的頭部並沒有定義每個列從哪個row group起始到哪個row group結束。所以在讀取所有列的情況下,RCFile的效能反而沒有SequenceFile高。
圖2:RCFile 檔案結構
3 Avro
Avro是一種用於支援資料密集型的二進位制檔案格式。它的檔案格式更為緊湊,若要讀取大量資料時,Avro能夠提供更好的序列化和反序列化效能。並且Avro資料檔案天生是帶Schema定義的,所以它不需要開發者在API 級別實現自己的Writable物件。最近多個Hadoop 子專案都支援Avro 資料格式,如Pig 、Hive、Flume、Sqoop和Hcatalog。
圖3:Avro MR 檔案格式
4. 文字格式
除上面提到的3種二進位制格式之外,文字格式的資料也是Hadoop中經常碰到的。如TextFile 、XML和JSON。 文字格式除了會佔用更多磁碟資源外,對它的解析開銷一般會比二進位制格式高几十倍以上,尤其是XML 和JSON,它們的解析開銷比Textfile 還要大,因此強烈不建議在生產系統中使用這些格式進行儲存。 如果需要輸出這些格式,請在客戶端做相應的轉換操作。 文字格式經常會用於日誌收集,資料庫匯入,Hive預設配置也是使用文字格式,而且常常容易忘了壓縮,所以請確保使用了正確的格式。另外文字格式的一個缺點是它不具備型別和模式,比如銷售金額、利潤這類數值資料或者日期時間型別的資料,如果使用文字格式儲存,由於它們本身的字串型別的長短不一,或者含有負數,導致MR沒有辦法排序,所以往往需要將它們預處理成含有模式的二進位制格式,這又導致了不必要的預處理步驟的開銷和儲存資源的浪費。
5. 外部格式
Hadoop實際上支援任意檔案格式,只要能夠實現對應的RecordWriter和RecordReader即可。其中資料庫格式也是會經常儲存在Hadoop中,比如Hbase,Mysql,Cassandra,MongoDB。 這些格式一般是為了避免大量的資料移動和快速裝載的需求而用的。他們的序列化和反序列化都是由這些資料庫格式的客戶端完成,並且檔案的儲存位置和資料佈局(Data Layout)不由Hadoop控制,他們的檔案切分也不是按HDFS的塊大小(blocksize)進行切割。
檔案儲存大小比較與分析
我們選取一個TPC-H標準測試來說明不同的檔案格式在儲存上的開銷。因為此資料是公開的,所以讀者如果對此結果感興趣,也可以對照後面的實驗自行做一遍。Orders 表文本格式的原始大小為1.62G。 我們將其裝載進Hadoop 並使用Hive 將其轉化成以上幾種格式,在同一種LZO 壓縮模式下測試形成的檔案的大小。
Orders_text1 |
1732690045 |
1.61G |
非壓縮 |
TextFile |
Orders_tex2 |
772681211 |
736M |
LZO壓縮 |
TextFile |
Orders_seq1 |
1935513587 |
1.80G |
非壓縮 |
SequenceFile |
Orders_seq2 |
822048201 |
783M |
LZO壓縮 |
SequenceFile |
Orders_rcfile1 |
1648746355 |
1.53G |
非壓縮 |
RCFile |
Orders_rcfile2 |
686927221 |
655M |
LZO壓縮 |
RCFile |
Orders_avro_table1 |
1568359334 |
1.46G |
非壓縮 |
Avro |
Orders_avro_table2 |
652962989 |
622M |
LZO壓縮 |
Avro |
表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 的開銷:
rank |
self |
accum |
count |
trace |
method |
20 |
0.48% |
79.64% |
65 |
315554 |
org.apache.hadoop.hive.ql.io.RCFile$Reader.getCurrentRow |
28 |
0.24% |
82.07% |
32 |
315292 |
org.apache.hadoop.hive.serde2.columnar.ColumnarStruct.init |
55 |
0.10% |
85.98% |
14 |
315788 |
org.apache.hadoop.hive.ql.io.RCFileRecordReader.getPos |
56 |
0.10% |
86.08% |
14 |
315797 |
org.apache.hadoop.hive.ql.io.RCFileRecordReader.next |
表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<RowContainer extends List<Object>>
而此資料格式可以改為面向列的序列化和反序列化方式。如:
Map<array<col1Type>,array<col2Type>,array<col3Type>....>
這種方式的反序列化會避免不必要的陣列移動,當然前提是我們必須知道列在哪個row group開始到哪個row group結束。這種方式會提高整體反序列化過程的效率。
關於Hadoop檔案格式的思考
1 高效壓縮
Hadoop目前尚未出現針對資料特性的高效編碼(Encoding)和解碼(Decoding)資料格式。尤其是支援Run Length Encoding、Bitmap 這些極為高效演算法的資料格式。HIVE-2065 討論過使用更加高效的壓縮形式,但是對於如何選取列的順序沒有結論。關於列順序選擇可以看Daniel Lemire的一篇論文 《Reordering Columns for Smaller Indexes》[1]。作者同時也是Hive 0.8中引入的bitmap 壓縮演算法基礎庫的作者。該論文的結論是:當某個表需要選取多個列進行壓縮時,需要根據列的選擇性(selectivity)進行升序排列,即唯一值越少的列排得越靠前。 事實上這個結論也是Vertica多年來使用的資料格式。其他跟壓縮有關的還有HIVE-2604和HIVE-2600。
2 基於列和塊的序列化和反序列化
不論排序後的結果是不是真的需要,目前Hadoop的整體框架都需要不斷根據資料key進行排序。除了上面提到的基於列的排序,序列化和反序列化之外,Hadoop的檔案格式應該支援某種基於塊(Block) 級別的排序和序列化及反序列化方式,只有當資料滿足需要時才進行這些操作。來自Google Tenzing論文中曾將它作為MR 的優化手段提到過。
“Block Shuffle:正常來說,MR 在Shuffle 的時候使用基於行的編碼和解碼。為了逐個處理每一行,資料必須先排序。然而,當排序不是必要的時候這種方式並不高效,我們在基於行的shuffle基礎上實現了一種基於block的shuffle方式,每一次處理大概1M的壓縮block,通過把整個block當成一行,我們能夠避免MR框架上的基於行的序列化和反序列化消耗,這種方式比基於行的shuffle 快上3倍以上。”
3 資料過濾(Skip List)
除常見的分割槽和索引之外,使用排序之後的塊(Block)間隔也是常見列資料庫中使用的過濾資料的方法。Google Tenzing同樣描述了一種叫做ColumnIO 的資料格式,ColumnIO在頭部定義該Block的最大值和最小值,在進行資料判斷的時候,如果當前Block的頭部資訊裡面描述的範圍中不包含當前需要處理的內容,則會直接跳過該塊。Hive社群裡曾討論過如何跳過不需要的塊 ,可是因為沒有排序所以一直沒有較好的實現方式。包括RCFile格式,Hive的index 機制裡面目前還沒有一個高效的根據頭部元資料就可以跳過塊的實現方式。
4 延遲物化
真正好的列資料庫,都應該可以支援直接在壓縮資料之上不需要通過解壓和排序就能夠直接操作塊。通過這種方式可以極大的降低MR 框架或者行式資料庫中先解壓,再反序列化,然後再排序所帶來的開銷。Google Tenzing裡面描述的Block Shuffle 也屬於延遲物化的一種。更好的延遲物化可以直接在壓縮資料上進行操作,並且可以做內部迴圈, 此方面在論文《Integrating Compression and Execution in Column-Oriented Database System》[5]的5.2 章節有描述。 不過考慮到它跟UDF 整合也有關係,所以,它會不會將檔案介面變得過於複雜也是一件有爭議的事情。
5 與Hadoop框架整合
無論文字亦或是二進位制格式,都只是最終的儲存格式。Hadoop執行時產生的中間資料卻沒有辦法控制。包括一個MR Job在map和reduce之間產生的資料或者DAG Job上游reduce 和下游map之間的資料,尤其是中間格式並不是列格式,這會產生不必要的IO和CPU 開銷。比如map 階段產生的spill,reduce 階段需要先copy 再sort-merge。如果這種中間格式也是面向列的,然後將一個大塊切成若干小塊,並在頭部加上每個小塊的最大最小值索引,就可以避免大量sort-mege操作中解壓—反序列化—排序—合併(Merge)的開銷,從而縮短任務的執行時間。
其他檔案格式
Hadoop社群也曾有對其他檔案格式的研究。比如,IBM 研究過面向列的資料格式並發表論文《Column-Oriented Storage Techniques for MapReduce》[4],其中特別提到IBM 的CIF(Column InputFormat)檔案格式在序列化和反序列化的IO消耗上比RCFile 的消耗要小20倍。裡面提到的將列分散在不同的HDFS Block 塊上的實現方式RCFile 也有考慮過,但是最後因為重組行的消耗可能會因分散在遠端機器上產生的延遲而最終放棄了這種實現。此外,最近Avro也在實現一種面向列的資料格式,不過目前Hive 與Avro 整合尚未全部完成。有興趣的讀者可以關注avro-806 和hive-895。
總結
Hadoop 可以與各種系統相容的前提是Hadoop MR 框架本身能夠支援多種資料格式的讀寫。但如果要提升其效能,Hadoop 需要一種高效的面向列的基於整個MR 框架整合的資料格式。尤其是高效壓縮,塊重組(block shuffle),資料過濾(skip list)等高階功能,它們是列資料庫相比MR 框架在檔案格式上有優勢的地方。相信隨著社群的發展以及Hadoop 的逐步成熟,未來會有更高效且統一的資料格式出現。
參考資料
[1]壓縮列順序選擇 http://lemire.me/en/ Reordering Columns for Smaller Indexes 論文地址