Spark輸出自定義檔案目錄踩坑(Java)

Hiway發表於2019-09-19

最近專案中,使用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方法的異同,我在查資料的過程中也發現了很多延伸的知識點,不過這些應該就是另一篇部落格了。

相關文章