淺析Hadoop文件格式
Hadoop 作為MR 的開(kāi)源實(shí)現(xiàn),一直以動(dòng)態(tài)運(yùn)行解析文件格式并獲得比MPP數(shù)據(jù)庫(kù)快上幾倍的裝載速度為優(yōu)勢(shì)。不過(guò),MPP數(shù)據(jù)庫(kù)社區(qū)也一直批評(píng)Hadoop由于文件格式并非為特定目的而建,因此序列化和反序列化的成本過(guò)高[7]。本文介紹Hadoop目前已有的幾種文件格式,分析其特點(diǎn)、開(kāi)銷及使用場(chǎng)景。希望加深讀者對(duì)Hadoop文件格式及其影響性能的因素的理解。
Hadoop 中的文件格式
1 SequenceFile
SequenceFile是Hadoop API 提供的一種二進(jìn)制文件,它將數(shù)據(jù)以<key,value>的形式序列化到文件中。這種二進(jìn)制文件內(nèi)部使用Hadoop 的標(biāo)準(zhǔn)的Writable 接口實(shí)現(xiàn)序列化和反序列化。它與Hadoop API中的MapFile 是互相兼容的。Hive 中的SequenceFile 繼承自Hadoop API 的SequenceFile,不過(guò)它的key為空,使用value 存放實(shí)際的值, 這樣是為了避免MR 在運(yùn)行map 階段的排序過(guò)程。如果你用Java API 編寫(xiě)SequenceFile,并讓Hive 讀取的話,請(qǐng)確保使用value字段存放數(shù)據(jù),否則你需要自定義讀取這種SequenceFile 的InputFormat class 和OutputFormat class。
圖1:Sequencefile 文件結(jié)構(gòu)
2 RCFile
RCFile是Hive推出的一種專門面向列的數(shù)據(jù)格式。 它遵循“先按列劃分,再垂直劃分”的設(shè)計(jì)理念。當(dāng)查詢過(guò)程中,針對(duì)它并不關(guān)心的列時(shí),它會(huì)在IO上跳過(guò)這些列。需要說(shuō)明的是,RCFile在map階段從遠(yuǎn)端拷貝仍然是拷貝整個(gè)數(shù)據(jù)塊,并且拷貝到本地目錄后RCFile并不是真正直接跳過(guò)不需要的列,并跳到需要讀取的列, 而是通過(guò)掃描每一個(gè)row group的頭部定義來(lái)實(shí)現(xiàn)的,但是在整個(gè)HDFS Block 級(jí)別的頭部并沒(méi)有定義每個(gè)列從哪個(gè)row group起始到哪個(gè)row group結(jié)束。所以在讀取所有列的情況下,RCFile的性能反而沒(méi)有SequenceFile高。
圖2:RCFile 文件結(jié)構(gòu)
3 Avro
Avro是一種用于支持?jǐn)?shù)據(jù)密集型的二進(jìn)制文件格式。它的文件格式更為緊湊,若要讀取大量數(shù)據(jù)時(shí),Avro能夠提供更好的序列化和反序列化性能。并且Avro數(shù)據(jù)文件天生是帶Schema定義的,所以它不需要開(kāi)發(fā)者在API 級(jí)別實(shí)現(xiàn)自己的Writable對(duì)象。最近多個(gè)Hadoop 子項(xiàng)目都支持Avro 數(shù)據(jù)格式,如Pig 、Hive、Flume、Sqoop和Hcatalog。
圖3:Avro MR 文件格式
4. 文本格式
除上面提到的3種二進(jìn)制格式之外,文本格式的數(shù)據(jù)也是Hadoop中經(jīng)常碰到的。如TextFile 、XML和JSON。 文本格式除了會(huì)占用更多磁盤資源外,對(duì)它的解析開(kāi)銷一般會(huì)比二進(jìn)制格式高幾十倍以上,尤其是XML 和JSON,它們的解析開(kāi)銷比Textfile 還要大,因此強(qiáng)烈不建議在生產(chǎn)系統(tǒng)中使用這些格式進(jìn)行儲(chǔ)存。 如果需要輸出這些格式,請(qǐng)?jiān)诳蛻舳俗鱿鄳?yīng)的轉(zhuǎn)換操作。 文本格式經(jīng)常會(huì)用于日志收集,數(shù)據(jù)庫(kù)導(dǎo)入,Hive默認(rèn)配置也是使用文本格式,而且常常容易忘了壓縮,所以請(qǐng)確保使用了正確的格式。另外文本格式的一個(gè)缺點(diǎn)是它不具備類型和模式,比如銷售金額、利潤(rùn)這類數(shù)值數(shù)據(jù)或者日期時(shí)間類型的數(shù)據(jù),如果使用文本格式保存,由于它們本身的字符串類型的長(zhǎng)短不一,或者含有負(fù)數(shù),導(dǎo)致MR沒(méi)有辦法排序,所以往往需要將它們預(yù)處理成含有模式的二進(jìn)制格式,這又導(dǎo)致了不必要的預(yù)處理步驟的開(kāi)銷和儲(chǔ)存資源的浪費(fèi)。
5. 外部格式
Hadoop實(shí)際上支持任意文件格式,只要能夠?qū)崿F(xiàn)對(duì)應(yīng)的RecordWriter和RecordReader即可。其中數(shù)據(jù)庫(kù)格式也是會(huì)經(jīng)常儲(chǔ)存在Hadoop中,比如Hbase,Mysql,Cassandra,MongoDB。 這些格式一般是為了避免大量的數(shù)據(jù)移動(dòng)和快速裝載的需求而用的。他們的序列化和反序列化都是由這些數(shù)據(jù)庫(kù)格式的客戶端完成,并且文件的儲(chǔ)存位置和數(shù)據(jù)布局(Data Layout)不由Hadoop控制,他們的文件切分也不是按HDFS的塊大?。╞locksize)進(jìn)行切割。
文件存儲(chǔ)大小比較與分析
我們選取一個(gè)TPC-H標(biāo)準(zhǔn)測(cè)試來(lái)說(shuō)明不同的文件格式在存儲(chǔ)上的開(kāi)銷。因?yàn)榇藬?shù)據(jù)是公開(kāi)的,所以讀者如果對(duì)此結(jié)果感興趣,也可以對(duì)照后面的實(shí)驗(yàn)自行做一遍。Orders 表文本格式的原始大小為1.62G。 我們將其裝載進(jìn)Hadoop 并使用Hive 將其轉(zhuǎn)化成以上幾種格式,在同一種LZO 壓縮模式下測(cè)試形成的文件的大小。
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:不同格式文件大小對(duì)比
從上述實(shí)驗(yàn)結(jié)果可以看到,SequenceFile無(wú)論在壓縮和非壓縮的情況下都比原始純文本TextFile大,其中非壓縮模式下大11%, 壓縮模式下大6.4%。這跟SequenceFile的文件格式的定義有關(guān): SequenceFile在文件頭中定義了其元數(shù)據(jù),元數(shù)據(jù)的大小會(huì)根據(jù)壓縮模式的不同略有不同。一般情況下,壓縮都是選取block 級(jí)別進(jìn)行的,每一個(gè)block都包含key的長(zhǎng)度和value的長(zhǎng)度,另外每4K字節(jié)會(huì)有一個(gè)sync-marker的標(biāo)記。對(duì)于TextFile文件格式來(lái)說(shuō)不同列之間只需要用一個(gè)行間隔符來(lái)切分,所以TextFile文件格式比SequenceFile文件格式要小。但是TextFile 文件格式不定義列的長(zhǎng)度,所以它必須逐個(gè)字符判斷每個(gè)字符是不是分隔符和行結(jié)束符。因此TextFile 的反序列化開(kāi)銷會(huì)比其他二進(jìn)制的文件格式高幾十倍以上。
RCFile文件格式同樣也會(huì)保存每個(gè)列的每個(gè)字段的長(zhǎng)度。但是它是連續(xù)儲(chǔ)存在頭部元數(shù)據(jù)塊中,它儲(chǔ)存實(shí)際數(shù)據(jù)值也是連續(xù)的。另外RCFile 會(huì)每隔一定塊大小重寫(xiě)一次頭部的元數(shù)據(jù)塊(稱為row group,由hive.io.rcfile.record.buffer.size控制,其默認(rèn)大小為4M),這種做法對(duì)于新出現(xiàn)的列是必須的,但是如果是重復(fù)的列則不需要。RCFile 本來(lái)應(yīng)該會(huì)比SequenceFile 文件大,但是RCFile 在定義頭部時(shí)對(duì)于字段長(zhǎng)度使用了Run Length Encoding進(jìn)行壓縮,所以RCFile 比SequenceFile又小一些。Run length Encoding針對(duì)固定長(zhǎng)度的數(shù)據(jù)格式有非常高的壓縮效率,比如Integer、Double和Long等占固定長(zhǎng)度的數(shù)據(jù)類型。在此提一個(gè)特例——Hive 0.8引入的TimeStamp 時(shí)間類型,如果其格式不包括毫秒,可表示為”YYYY-MM-DD HH:MM:SS”,那么就是固定長(zhǎng)度占8個(gè)字節(jié)。如果帶毫秒,則表示為”YYYY-MM-DD HH:MM:SS.fffffffff”,后面毫秒的部分則是可變的。
Avro文件格式也按group進(jìn)行劃分。但是它會(huì)在頭部定義整個(gè)數(shù)據(jù)的模式(Schema), 而不像RCFile那樣每隔一個(gè)row group就定義列的類型,并且重復(fù)多次。另外,Avro在使用部分類型的時(shí)候會(huì)使用更小的數(shù)據(jù)類型,比如Short或者Byte類型,所以Avro的數(shù)據(jù)塊比RCFile 的文件格式塊更小。
序列化與反序列化開(kāi)銷分析
我們可以使用Java的profile工具來(lái)查看Hadoop 運(yùn)行時(shí)任務(wù)的CPU和內(nèi)存開(kāi)銷。以下是在Hive 命令行中的設(shè)置:
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
當(dāng)map task 運(yùn)行結(jié)束后,它產(chǎn)生的日志會(huì)寫(xiě)在$logs/userlogs/job-
我們運(yùn)行一個(gè)簡(jiǎn)單的SQL語(yǔ)句來(lái)觀察RCFile 格式在序列化和反序列化上的開(kāi)銷:
hive> select O_CUSTKEY,O_ORDERSTATUS from orders_rc2 where O_ORDERSTATUS='P';
其中的O_CUSTKEY列為integer類型,O_ORDERSTATUS為String類型。在日志輸出的最后會(huì)包含內(nèi)存和CPU 的消耗。
下表是一次CPU 的開(kāi)銷:
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的開(kāi)銷
其中第五列可以對(duì)照上面的Track信息查看到底調(diào)用了哪些函數(shù)。比如CPU消耗排名20的函數(shù)對(duì)應(yīng)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,它為了構(gòu)造行而消耗了不必要的數(shù)組移動(dòng)開(kāi)銷。其主要是因?yàn)镽CFile 為了還原行,需要構(gòu)造RowContainer,順序讀取一行構(gòu)造RowContainer,然后給其中對(duì)應(yīng)的列進(jìn)行賦值,因?yàn)镽CFile早期為了兼容SequenceFile所以可以合并兩個(gè)block,又由于RCFile不知道列在哪個(gè)row group結(jié)束,所以必須維持?jǐn)?shù)組的當(dāng)前位置,類似如下格式定義:
Array<RowContainer extends List<Object>>
而此數(shù)據(jù)格式可以改為面向列的序列化和反序列化方式。如:
Map<array<col1Type>,array<col2Type>,array<col3Type>....>
這種方式的反序列化會(huì)避免不必要的數(shù)組移動(dòng),當(dāng)然前提是我們必須知道列在哪個(gè)row group開(kāi)始到哪個(gè)row group結(jié)束。這種方式會(huì)提高整體反序列化過(guò)程的效率。
關(guān)于Hadoop文件格式的思考
1 高效壓縮
Hadoop目前尚未出現(xiàn)針對(duì)數(shù)據(jù)特性的高效編碼(Encoding)和解碼(Decoding)數(shù)據(jù)格式。尤其是支持Run Length Encoding、Bitmap 這些極為高效算法的數(shù)據(jù)格式。HIVE-2065 討論過(guò)使用更加高效的壓縮形式,但是對(duì)于如何選取列的順序沒(méi)有結(jié)論。關(guān)于列順序選擇可以看Daniel Lemire的一篇論文 《Reordering Columns for Smaller Indexes》[1]。作者同時(shí)也是Hive 0.8中引入的bitmap 壓縮算法基礎(chǔ)庫(kù)的作者。該論文的結(jié)論是:當(dāng)某個(gè)表需要選取多個(gè)列進(jìn)行壓縮時(shí),需要根據(jù)列的選擇性(selectivity)進(jìn)行升序排列,即唯一值越少的列排得越靠前。 事實(shí)上這個(gè)結(jié)論也是Vertica多年來(lái)使用的數(shù)據(jù)格式。其他跟壓縮有關(guān)的還有HIVE-2604和HIVE-2600。
2 基于列和塊的序列化和反序列化
不論排序后的結(jié)果是不是真的需要,目前Hadoop的整體框架都需要不斷根據(jù)數(shù)據(jù)key進(jìn)行排序。除了上面提到的基于列的排序,序列化和反序列化之外,Hadoop的文件格式應(yīng)該支持某種基于塊(Block) 級(jí)別的排序和序列化及反序列化方式,只有當(dāng)數(shù)據(jù)滿足需要時(shí)才進(jìn)行這些操作。來(lái)自Google Tenzing論文中曾將它作為MR 的優(yōu)化手段提到過(guò)。
“Block Shuffle:正常來(lái)說(shuō),MR 在Shuffle 的時(shí)候使用基于行的編碼和解碼。為了逐個(gè)處理每一行,數(shù)據(jù)必須先排序。然而,當(dāng)排序不是必要的時(shí)候這種方式并不高效,我們?cè)诨谛械膕huffle基礎(chǔ)上實(shí)現(xiàn)了一種基于block的shuffle方式,每一次處理大概1M的壓縮block,通過(guò)把整個(gè)block當(dāng)成一行,我們能夠避免MR框架上的基于行的序列化和反序列化消耗,這種方式比基于行的shuffle 快上3倍以上。”
3 數(shù)據(jù)過(guò)濾(Skip List)
除常見(jiàn)的分區(qū)和索引之外,使用排序之后的塊(Block)間隔也是常見(jiàn)列數(shù)據(jù)庫(kù)中使用的過(guò)濾數(shù)據(jù)的方法。Google Tenzing同樣描述了一種叫做ColumnIO 的數(shù)據(jù)格式,ColumnIO在頭部定義該Block的最大值和最小值,在進(jìn)行數(shù)據(jù)判斷的時(shí)候,如果當(dāng)前Block的頭部信息里面描述的范圍中不包含當(dāng)前需要處理的內(nèi)容,則會(huì)直接跳過(guò)該塊。Hive社區(qū)里曾討論過(guò)如何跳過(guò)不需要的塊 ,可是因?yàn)闆](méi)有排序所以一直沒(méi)有較好的實(shí)現(xiàn)方式。包括RCFile格式,Hive的index 機(jī)制里面目前還沒(méi)有一個(gè)高效的根據(jù)頭部元數(shù)據(jù)就可以跳過(guò)塊的實(shí)現(xiàn)方式。
4 延遲物化
真正好的列數(shù)據(jù)庫(kù),都應(yīng)該可以支持直接在壓縮數(shù)據(jù)之上不需要通過(guò)解壓和排序就能夠直接操作塊。通過(guò)這種方式可以極大的降低MR 框架或者行式數(shù)據(jù)庫(kù)中先解壓,再反序列化,然后再排序所帶來(lái)的開(kāi)銷。Google Tenzing里面描述的Block Shuffle 也屬于延遲物化的一種。更好的延遲物化可以直接在壓縮數(shù)據(jù)上進(jìn)行操作,并且可以做內(nèi)部循環(huán), 此方面在論文《Integrating Compression and Execution in Column-Oriented Database System》[5]的5.2 章節(jié)有描述。 不過(guò)考慮到它跟UDF 集成也有關(guān)系,所以,它會(huì)不會(huì)將文件接口變得過(guò)于復(fù)雜也是一件有爭(zhēng)議的事情。
5 與Hadoop框架集成
無(wú)論文本亦或是二進(jìn)制格式,都只是最終的儲(chǔ)存格式。Hadoop運(yùn)行時(shí)產(chǎn)生的中間數(shù)據(jù)卻沒(méi)有辦法控制。包括一個(gè)MR Job在map和reduce之間產(chǎn)生的數(shù)據(jù)或者DAG Job上游reduce 和下游map之間的數(shù)據(jù),尤其是中間格式并不是列格式,這會(huì)產(chǎn)生不必要的IO和CPU 開(kāi)銷。比如map 階段產(chǎn)生的spill,reduce 階段需要先copy 再sort-merge。如果這種中間格式也是面向列的,然后將一個(gè)大塊切成若干小塊,并在頭部加上每個(gè)小塊的最大最小值索引,就可以避免大量sort-mege操作中解壓—反序列化—排序—合并(Merge)的開(kāi)銷,從而縮短任務(wù)的運(yùn)行時(shí)間。
其他文件格式
Hadoop社區(qū)也曾有對(duì)其他文件格式的研究。比如,IBM 研究過(guò)面向列的數(shù)據(jù)格式并發(fā)表論文《Column-Oriented Storage Techniques for MapReduce》[4],其中特別提到IBM 的CIF(Column InputFormat)文件格式在序列化和反序列化的IO消耗上比RCFile 的消耗要小20倍。里面提到的將列分散在不同的HDFS Block 塊上的實(shí)現(xiàn)方式RCFile 也有考慮過(guò),但是最后因?yàn)橹亟M行的消耗可能會(huì)因分散在遠(yuǎn)程機(jī)器上產(chǎn)生的延遲而最終放棄了這種實(shí)現(xiàn)。此外,最近Avro也在實(shí)現(xiàn)一種面向列的數(shù)據(jù)格式,不過(guò)目前Hive 與Avro 集成尚未全部完成。有興趣的讀者可以關(guān)注avro-806 和hive-895。
總結(jié)
Hadoop 可以與各種系統(tǒng)兼容的前提是Hadoop MR 框架本身能夠支持多種數(shù)據(jù)格式的讀寫(xiě)。但如果要提升其性能,Hadoop 需要一種高效的面向列的基于整個(gè)MR 框架集成的數(shù)據(jù)格式。尤其是高效壓縮,塊重組(block shuffle),數(shù)據(jù)過(guò)濾(skip list)等高級(jí)功能,它們是列數(shù)據(jù)庫(kù)相比MR 框架在文件格式上有優(yōu)勢(shì)的地方。相信隨著社區(qū)的發(fā)展以及Hadoop 的逐步成熟,未來(lái)會(huì)有更高效且統(tǒng)一的數(shù)據(jù)格式出現(xiàn)。
【編輯推薦】