Spark RCFile的那些“坑”

RCFile在平臺的應用場景中多數用於存儲需要“長期留存”的數據文件,在我們的實踐過程中,RCFile的數據壓縮比通常可以達到8 : 1或者10 : 1,特別適用於存儲用戶通過Hive(MapReduce)分析的結果。目前平臺的計算引擎正逐步由Hadoop MapReduce遷移至Spark,存儲方面我們依然想利用RCFile的優勢,但是具體實踐中遇到那麼幾個“坑”。
數據分析師使用PySpark構建Spark分析程序,源數據是按行存儲的文本文件(可能有壓縮),結果數據爲Python list,list的元素類型爲tuple,而tuple的元素類型爲unicode(Python2,爲了保持大家對數據認知的一致性,源數據是文本,我們要求用戶的處理結果也爲文本),可以看出list實際可以理解爲一張表(Table),list的元素tuple爲行(Row),tuple的元素爲列(Column),因此能夠很好的利用RCFile的列式存儲特性。
RCFile擴展自Hadoop InputFormat、OutputFormat、Writable:
org.apache.hadoop.hive.ql.io.RCFileInputFormat
org.apache.hadoop.hive.ql.io.RCFileOutputFormat
org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable
注意:RCFile的使用需要依賴於Hive的Jar。
使用RCFileOutputFormat時我們需要處理tuple => BytesRefArrayWritable(Object[] => BytesRefArrayWritable)的數據類型轉換,使用RCFileInputFormat時我們需要處理BytesRefArrayWritable => tuple(BytesRefArrayWritable => Object[])的數據類型轉換,也就是說我們需要擴展兩個Converter:
ObjectArrayToBytesRefArrayWritableConverter:用於Object[] => BytesRefArrayWritable的數據類型轉換;
BytesRefArrayWritableToObjectArrayConverter:用於BytesRefArrayWritable => Object[]的數據類型轉換;
注:有關PySpark Converter的相關原理可以參考 http://diptech.sinaapp.com/?p=125 ,在此我們只介紹具體的實現細節。
(1)ObjectArrayToBytesRefArrayWritableConverter;
convert的參數類型爲Object[],返回值類型爲BytesRefArrayWritable。
(2)BytesRefArrayWritableToObjectArrayConverter;
convert的參數類型爲BytesRefArrayWritable,返回值類型爲Object[]。
1. 模擬數據(用戶分析結果),將其以RCFile的形式保存至HDFS;
我們模擬的數據爲三行三列,數據類型均爲文本,需要注意的是RCFile在保存數據時需要通過Hadoop Configuration指定“列數”,否則會出現以下異常:
此外RCFileOutputFormat RecordWriter會丟棄“key”:
因此“key”可以是任意值,只要兼容Hadoop Writable即可,此處我們將“key”處理爲None,並設置keyClass爲org.apache.hadoop.io.NullWritable。
而且運行上述程序之前,還需要將com.sina.dip.spark.converter.ObjectArrayToBytesRefArrayWritableConverter編譯打包爲獨立的Jar:rcfile.jar,運行命令如下:
spark-submit --jars rcfile.jar 1.5.1/examples/app/spark_app_save_data_to_rcfile.py
出乎意料,異常信息出現:
引發異常的代碼並不是我們自定義擴展的ObjectArrayToBytesRefArrayWritableConverter,而是RCFileOutputFormat,怎麼可能,這不是官方提供的代碼麼?根據異常堆棧可知,RCFileOutputFormat第79行(不同版本的Hive可能代碼行數不同)代碼出現空指針異常:
該行可能引發空指針異常的唯一原因就是outputPath == null,而outputPath的值由方法getWorkOutputPath計算而得:
其中JobContext.TASK_OUTPUT_DIR的值爲mapreduce.task.output.dir。
熟悉Hadoop的同學可能已經想到,方法getWorkOutputPath是用來計算Map或Reduce Task臨時輸出目錄的,JobContext.TASK_OUTPUT_DIR屬性也是以前綴“mapreduce”開頭的,“ Spark運行時是不會爲該屬性設置值的 ”,所以outputPath == null,那麼我們應該如何計算outputPath呢?
困惑之餘,我們聯想到當初調研Spark時是以文本爲基礎進行功能測試的,也就是說在Spark中使用TextInputFormat、TextOutputFormat是沒有任何問題的,果斷參考一下TextOutputFormat是如何實現的?
FileOutputFormat是一個基礎類,這意味着我們可以重寫RCFileOutputFormat getRecordWriter,使用FileOutputFormat.getTaskOutputPath替換getWorkOutputPath:
可以看出,重寫後的getRecordWriter僅僅是改變了outputPath的計算方式,其它邏輯並沒有改變,我們將重寫後的類命名爲com.sina.dip.spark.output.DipRCFileOutputFormat,並將其一併編譯打包爲獨立的Jar:rcfile.jar。
重新修改Spark代碼:
我們作出了兩個地方的修改:
(1)parallelize numSlices:1,考慮到模擬的數據量比較小,爲了便於查看結果,我們將“分區數”設置爲1,這樣最終僅有一個數據文件;
(2)outputFormatClass:com.sina.dip.spark.output.DipRCFileOutputFormat;
再次運行命令:
spark-submit --jars rcfile.jar 1.5.1/examples/app/spark_app_save_data_to_rcfile.py
程序執行結果之後,我們通過HDFS FS命令查看目錄:hdfs://dip.dev.cdh5:8020/user/yurun/rcfile/:
數據文件已成功生成,爲了確認寫入的正確性,我們通過Hive RCFileCat命令查看文件:hdfs://dip.dev.cdh5:8020/user/yurun/rcfile/part-00000:
可見寫入文件的數據與我們模擬的數據是一致的。
2. 讀取上一步寫入的數據;
運行上述程序之前,還需要將com.sina.dip.spark.converter.BytesRefArrayWritableToObjectArrayConverter編譯打包爲獨立的Jar:rcfile.jar,運行命令如下:
spark-submit --jars rcfile.jar 1.5.1/examples/app/spark_app_read_data_from_rcfile.py
輸出結果:
我們使用Hive原生的RCFileInputFormat,以及我們自己擴展的BytesRefArrayWritableToObjectArrayConverter正確完成了RCFile數據的讀取,實際上pair[0]可以理解爲“行數”(注意keyClass的設置),通常情況下沒有實際意義,可以選擇忽略。
綜上所述,Spark(PySpark)使用RCFile的過程中會遇到三個“坑”:
(1)需要重寫RCFileOutputFormat getRecordWriter;
(2)需要擴展Converter支持tuple(Object[]) => BytesRefArrayWritable的數據類型轉換;
(3)需要擴展Converter支持BytesRefArrayWritable => tuple (Object[])的數據類型轉換。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章