最近專案中,使用Spark做離線計算,結果需要輸出一份結果到檔案中儲存,並且需要按Key來放置不同的目錄。因為spark通過saveAsTextFile()方法預設輸出是以part-0000的形式。
解決方法
通過搜尋,很輕易的就能搜尋到使用saveAsHadoopFile()方法可以將檔案輸出到自定義檔案目錄。網上大部分都是scala的寫法,java的具體操作如下:
//首先,構造出一個PariRDD形式的RDD
JavaPairRDD<String, JSONObject> javaPairRDD =xxx
//使用saveAsHadoopFile方法輸出到目標目錄下,方法引數分別為(目標目錄,key的class型別,value的class型別,輸出format類)
javaPairRDD.saveAsHadoopFile("D:\\Test",String.class,JSONObject.class,RDDMultipleTextOutputFormat2.class);
//自定義一個RDDMultipleTextOutputFormat2繼承MultipleTextOutputFormat
public static class RDDMultipleTextOutputFormat2 extends MultipleTextOutputFormat<String, JSONObject> {
@Override
public String generateFileNameForKeyValue(String key, JSONObject value,
String name) {
String object_type = value.getString("object_type");
String object_id = value.getString("object_id");
return object_type + "/" + object_id+".json";
}
}
複製程式碼
最後輸出的結果就是按"D:\Test\object_type\object_id.json
來區分儲存。
新的問題
美滋滋的開啟按我們要求輸出目錄的輸出檔案。結果卻發現,輸出檔案中,並不是僅僅將value寫入了檔案中,同時把key也寫了進去。但是我們的檔案格式是不需要key寫入檔案的。
//輸出檔案內容形式如下
key value
複製程式碼
解決辦法一
遇事不決百度Google,通過搜尋引擎,可以找到我們通過設定rdd的key為NullWritable,使得輸出檔案中不包含key,網上同樣大多數是scala的,下面是java的具體操作:
/首先,構造出一個PariRDD形式的RDD
JavaPairRDD<String, JSONObject> javaPairRDD =xxx
//將PariRDD轉為<NullWritable,T>的形式
JavaPairRDD<NullWritable, JSONObject> nullKeyJavaPairRDD = javaPairRDD.mapToPair(tuple2 -> {
return new Tuple2(NullWritable.get(),tuple2._2);
});
//接下來的操作和上面一樣
nullKeyJavaPairRDD.saveAsHadoopFile("D:\\Test",NullWritable.class,JSONObject.class,RDDMultipleTextOutputFormat.class);
public static class RDDMultipleTextOutputFormat extends MultipleTextOutputFormat<NullWritable, JSONObject> {
@Override
public String generateFileNameForKeyValue(NullWritable key, JSONObject value,
String name) {
String object_type = value.getString("object_type");
String object_id = value.getString("object_id");
return object_type + "/" + object_id+".json";
}
}
複製程式碼
如上方法確實是可以解決問題,但是如果我們需要的輸出目錄與key有關係,想將key使用在自定義目錄中,這就辦不到了。所以這個解決方法還是有缺陷的,僅僅能滿足輸出目錄只與value有關係的情況。
解決辦法二
這裡想到另一個解決思路,往檔案寫內容總是需要一個類的,我們找到這個類,重寫它,把key的輸出去掉,不就可以了。
於是我們跟進我們繼承的MultipleTextOutputFormat
類中
public class MultipleTextOutputFormat<K, V>
extends MultipleOutputFormat<K, V> {
private TextOutputFormat<K, V> theTextOutputFormat = null;
@Override
protected RecordWriter<K, V> getBaseRecordWriter(FileSystem fs, JobConf job,
String name, Progressable arg3) throws IOException {
if (theTextOutputFormat == null) {
theTextOutputFormat = new TextOutputFormat<K, V>();
}
return theTextOutputFormat.getRecordWriter(fs, job, name, arg3);
}
}
複製程式碼
並沒有發現有相關的方法,我們繼續跟進父類MultipleOutputFormat
,在這個類中,我們發現了一個write方法:
public void write(K key, V value) throws IOException {
// get the file name based on the key
String keyBasedPath = generateFileNameForKeyValue(key, value, myName);
// get the file name based on the input file name
String finalPath = getInputFileBasedOutputFileName(myJob, keyBasedPath);
// get the actual key
K actualKey = generateActualKey(key, value);
V actualValue = generateActualValue(key, value);
RecordWriter<K, V> rw = this.recordWriters.get(finalPath);
if (rw == null) {
// if we don't have the record writer yet for the final path, create
// one
// and add it to the cache
rw = getBaseRecordWriter(myFS, myJob, finalPath, myProgressable);
this.recordWriters.put(finalPath, rw);
}
rw.write(actualKey, actualValue);
};
複製程式碼
感覺真相就在眼前了,我們繼續跟進rw.write(actualKey, actualValue);
方法,通過斷點我們可以知道他進入的是TextOutPutFormat #write()
方法:
public synchronized void write(K key, V value)
throws IOException {
boolean nullKey = key == null || key instanceof NullWritable;
boolean nullValue = value == null || value instanceof NullWritable;
if (nullKey && nullValue) {
return;
}
if (!nullKey) {
writeObject(key);
}
if (!(nullKey || nullValue)) {
out.write(keyValueSeparator);
}
if (!nullValue) {
writeObject(value);
}
out.write(newline);
}
複製程式碼
這段程式碼就很簡單了,只要key不是null或者NullWritable類,他就會往檔案裡輸出。這也解釋了為什麼上面方法一中將key轉換為NullWritable類就不會輸出到檔案中了。
瞭解到這裡,我們就很容易得出解決方案,我們只要將傳入write()
方法中的key傳入null就可以了。回到MultipleOutputFormat
類中,我們看到傳入的key是由這個方法獲取的K actualKey = generateActualKey(key, value);
,繼續跟進:
protected K generateActualKey(K key, V value) {
return key;
}
複製程式碼
接下很簡單了,我們在自定義的format類中重寫這個方法,改為返回null即可。
public static class RDDMultipleTextOutputFormat3 extends MultipleTextOutputFormat<String, JSONObject> {
@Override
public String generateFileNameForKeyValue(String key, JSONObject value,
String name) {
String object_id = value.getString("object_id");
return key + "/" + object_id+".json";
}
@Override
public String generateActualKey(String key, JSONObject value) {
return null;
}
}
複製程式碼
總結
其實這次踩坑還是挺簡單的,按部就班的一路跟進就找到了,其中還有不少點是可以延伸的,比如saveAsHadoopFile方法的的底層實現,和傳統的saveAsTextFile方法的異同,我在查資料的過程中也發現了很多延伸的知識點,不過這些應該就是另一篇部落格了。