簡體   English   中英

通過鍵Spark寫入多個輸出-一個Spark作業

[英]Write to multiple outputs by key Spark - one Spark job

如何在單個Job中使用Spark寫入取決於鍵的多個輸出。

相關: 通過鍵擴展Hadoop(一個MapReduce作業)寫入多個輸出

例如

sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
.writeAsMultiple(prefix, compressionCodecOption)

將確保cat prefix/1

a
b

cat prefix/2將是

c

編輯:我最近添加了一個新的答案,其中包括完整的導入,pimp和壓縮編解碼器,請參閱https://stackoverflow.com/a/46118044/1586965 ,除了較早的答案外,它可能還會有所幫助。

如果使用Spark 1.4+,這要歸功於DataFrame API ,它變得非常容易。 (DataFrames是在Spark 1.3中引入的,而我們所需的partitionBy()在1.4引入的 。)

如果從RDD開始,則首先需要將其轉換為DataFrame:

val people_rdd = sc.parallelize(Seq((1, "alice"), (1, "bob"), (2, "charlie")))
val people_df = people_rdd.toDF("number", "name")

在Python中,相同的代碼是:

people_rdd = sc.parallelize([(1, "alice"), (1, "bob"), (2, "charlie")])
people_df = people_rdd.toDF(["number", "name"])

一旦有了DataFrame,就可以根據特定鍵寫入多個輸出,這很簡單。 更重要的是-這就是DataFrame API的優點-Python,Scala,Java和R中的代碼幾乎相同:

people_df.write.partitionBy("number").text("people")

如果需要,您可以輕松使用其他輸出格式:

people_df.write.partitionBy("number").json("people-json")
people_df.write.partitionBy("number").parquet("people-parquet")

在上述每個示例中,Spark將為我們對DataFrame進行分區的每個鍵創建一個子目錄:

people/
  _SUCCESS
  number=1/
    part-abcd
    part-efgh
  number=2/
    part-abcd
    part-efgh

我會這樣做,可擴展

import org.apache.hadoop.io.NullWritable

import org.apache.spark._
import org.apache.spark.SparkContext._

import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat

class RDDMultipleTextOutputFormat extends MultipleTextOutputFormat[Any, Any] {
  override def generateActualKey(key: Any, value: Any): Any = 
    NullWritable.get()

  override def generateFileNameForKeyValue(key: Any, value: Any, name: String): String = 
    key.asInstanceOf[String]
}

object Split {
  def main(args: Array[String]) {
    val conf = new SparkConf().setAppName("Split" + args(1))
    val sc = new SparkContext(conf)
    sc.textFile("input/path")
    .map(a => (k, v)) // Your own implementation
    .partitionBy(new HashPartitioner(num))
    .saveAsHadoopFile("output/path", classOf[String], classOf[String],
      classOf[RDDMultipleTextOutputFormat])
    spark.stop()
  }
}

剛剛在上面看到了類似的答案,但實際上我們不需要自定義分區。 MultipleTextOutputFormat將為每個鍵創建文件。 可以將具有相同鍵的多個記錄放入同一分區。

新的HashPartitioner(num),其中num是所需的分區號。 如果您有大量不同的鍵,可以將數字設置為big。 在這種情況下,每個分區將不會打開太多的hdfs文件處理程序。

如果給定鍵可能有很多值,我認為可伸縮的解決方案是每個分區的每個鍵寫一個文件。 不幸的是,Spark中沒有對此的內置支持,但是我們可以提高一些。

sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
  .mapPartitionsWithIndex { (p, it) =>
    val outputs = new MultiWriter(p.toString)
    for ((k, v) <- it) {
      outputs.write(k.toString, v)
    }
    outputs.close
    Nil.iterator
  }
  .foreach((x: Nothing) => ()) // To trigger the job.

// This one is Local, but you could write one for HDFS
class MultiWriter(suffix: String) {
  private val writers = collection.mutable.Map[String, java.io.PrintWriter]()
  def write(key: String, value: Any) = {
    if (!writers.contains(key)) {
      val f = new java.io.File("output/" + key + "/" + suffix)
      f.getParentFile.mkdirs
      writers(key) = new java.io.PrintWriter(f)
    }
    writers(key).println(value)
  }
  def close = writers.values.foreach(_.close)
}

(將PrintWriter替換為您選擇的分布式文件系統操作。)

這樣就可以在RDD上進行一次遍歷,而不會進行隨機播放。 它為每個鍵提供一個目錄,每個目錄中包含許多文件。

這包括請求的編解碼器,必要的導入和請求的皮條客。

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext

// TODO Need a macro to generate for each Tuple length, or perhaps can use shapeless
implicit class PimpedRDD[T1, T2](rdd: RDD[(T1, T2)]) {
  def writeAsMultiple(prefix: String, codec: String,
                      keyName: String = "key")
                     (implicit sqlContext: SQLContext): Unit = {
    import sqlContext.implicits._

    rdd.toDF(keyName, "_2").write.partitionBy(keyName)
    .format("text").option("codec", codec).save(prefix)
  }
}

val myRdd = sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
myRdd.writeAsMultiple("prefix", "org.apache.hadoop.io.compress.GzipCodec")

與OP的一個細微差別是它將<keyName>=作為目錄名稱的前綴。 例如

myRdd.writeAsMultiple("prefix", "org.apache.hadoop.io.compress.GzipCodec")

將給出:

prefix/key=1/part-00000
prefix/key=2/part-00000

其中prefix/my_number=1/part-00000將包含行ab ,而prefix/my_number=2/part-00000將包含行c

myRdd.writeAsMultiple("prefix", "org.apache.hadoop.io.compress.GzipCodec", "foo")

將給出:

prefix/foo=1/part-00000
prefix/foo=2/part-00000

應該清楚如何編輯parquet

最后,下面是Dataset的示例,它也許比使用Tuples更好。

implicit class PimpedDataset[T](dataset: Dataset[T]) {
  def writeAsMultiple(prefix: String, codec: String, field: String): Unit = {
    dataset.write.partitionBy(field)
    .format("text").option("codec", codec).save(prefix)
  }
}

我有類似的需求,找到了一種方法。 但這有一個缺點(對我而言,這不是問題):您需要使用每個輸出文件一個分區對數據進行重新分區。

要以這種方式進行分區,通常需要事先知道作業將輸出多少文件,並找到將每個鍵映射到每個分區的函數。

首先,讓我們創建基於MultipleTextOutputFormat的類:

import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat

class KeyBasedOutput[T >: Null, V <: AnyRef] extends MultipleTextOutputFormat[T , V] {
  override def generateFileNameForKeyValue(key: T, value: V, leaf: String) = {
    key.toString
  }
  override protected def generateActualKey(key: T, value: V) = {
    null
  }
}

有了這個類,Spark將從一個分區(我想是第一個/最后一個)中獲取一個密鑰,並用該密鑰命名文件,因此在同一分區上混合多個密鑰是不好的。

對於您的示例,您將需要一個自定義分區程序。 這將完成工作:

import org.apache.spark.Partitioner

class IdentityIntPartitioner(maxKey: Int) extends Partitioner {
  def numPartitions = maxKey

  def getPartition(key: Any): Int = key match {
    case i: Int if i < maxKey => i
  }
}

現在,讓我們將所有內容放在一起:

val rdd = sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c"), (7, "d"), (7, "e")))

// You need to know the max number of partitions (files) beforehand
// In this case we want one partition per key and we have 3 keys,
// with the biggest key being 7, so 10 will be large enough
val partitioner = new IdentityIntPartitioner(10)

val prefix = "hdfs://.../prefix"

val partitionedRDD = rdd.partitionBy(partitioner)

partitionedRDD.saveAsHadoopFile(prefix,
    classOf[Integer], classOf[String], classOf[KeyBasedOutput[Integer, String]])

這將在前綴(名稱分別為1、2和7)下生成3個文件,一次處理所有文件。

如您所見,您需要有關密鑰的一些知識才能使用此解決方案。

對我來說,這很容易,因為每個密鑰哈希都需要一個輸出文件,並且文件的數量在我的控制之下,所以我可以使用常規的HashPartitioner來完成此任務。

我在Java中也需要同樣的東西。 將我對Zhang Zhan的Scala答案的翻譯發布給Spark Java API用戶:

import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaSparkContext;
import scala.Tuple2;

import java.util.Arrays;


class RDDMultipleTextOutputFormat<A, B> extends MultipleTextOutputFormat<A, B> {

    @Override
    protected String generateFileNameForKeyValue(A key, B value, String name) {
        return key.toString();
    }
}

public class Main {

    public static void main(String[] args) {
        SparkConf conf = new SparkConf()
                .setAppName("Split Job")
                .setMaster("local");
        JavaSparkContext sc = new JavaSparkContext(conf);
        String[] strings = {"Abcd", "Azlksd", "whhd", "wasc", "aDxa"};
        sc.parallelize(Arrays.asList(strings))
                // The first character of the string is the key
                .mapToPair(s -> new Tuple2<>(s.substring(0,1).toLowerCase(), s))
                .saveAsHadoopFile("output/", String.class, String.class,
                        RDDMultipleTextOutputFormat.class);
        sc.stop();
    }
}

saveRDC數據是基於RDD數據實現的,特別是通過以下方法實現的: PairRDD.saveAsHadoopDataset ,該方法從PairRdd中獲取執行數據。 我看到兩個可能的選擇:如果數據較小,則可以通過對RDD進行分組,從每個集合中創建一個新的RDD並使用該RDD寫入數據來節省一些實現時間。 像這樣:

val byKey = dataRDD.groupByKey().collect()
val rddByKey = byKey.map{case (k,v) => k->sc.makeRDD(v.toSeq)}
val rddByKey.foreach{ case (k,rdd) => rdd.saveAsText(prefix+k}

請注意,它不適用於大型數據集b / c, v.toSeq處的迭代器的v.toSeq可能不適合內存。

我看到的另一個選項(在這種情況下實際上是我推薦的)是:通過直接調用hadoop / hdfs api自己滾動。

這是我在研究此問題時開始的討論: 如何從另一個RDD創建RDD?

我有一個類似的用例,其中我根據一個密鑰(每個密鑰1個文件)將Hadoop HDFS上的輸入文件拆分為多個文件。 這是我的火花的scala代碼

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

val hadoopconf = new Configuration();
val fs = FileSystem.get(hadoopconf);

@serializable object processGroup {
    def apply(groupName:String, records:Iterable[String]): Unit = {
        val outFileStream = fs.create(new Path("/output_dir/"+groupName))
        for( line <- records ) {
                outFileStream.writeUTF(line+"\n")
            }
        outFileStream.close()
    }
}
val infile = sc.textFile("input_file")
val dateGrouped = infile.groupBy( _.split(",")(0))
dateGrouped.foreach( (x) => processGroup(x._1, x._2))

我已經根據密鑰對記錄進行了分組。 每個密鑰的值都寫入單獨的文件。

如果您有多列並且要保存所有其他未按csv格式分區的列,這對於python用戶來說是個好消息,如果您使用Nick的建議使用“文本”方法,則這將失敗。

people_df.write.partitionBy("number").text("people") 

錯誤消息是“ AnalysisException:u'Text數據源僅支持一列,而您有2列。

在spark 2.0.0中(我的測試環境是hdp的spark 2.0.0),軟件包“ com.databricks.spark.csv”現在集成了,它允許我們保存僅一列分區的文本文件,請參見示例打擊:

people_rdd = sc.parallelize([(1,"2016-12-26", "alice"),
                             (1,"2016-12-25", "alice"),
                             (1,"2016-12-25", "tom"), 
                             (1, "2016-12-25","bob"), 
                             (2,"2016-12-26" ,"charlie")])
df = people_rdd.toDF(["number", "date","name"])

df.coalesce(1).write.partitionBy("number").mode("overwrite").format('com.databricks.spark.csv').options(header='false').save("people")

[root@namenode people]# tree
.
├── number=1
│?? └── part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
├── number=2
│?? └── part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
└── _SUCCESS

[root@namenode people]# cat number\=1/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,alice
2016-12-25,alice
2016-12-25,tom
2016-12-25,bob
[root@namenode people]# cat number\=2/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,charlie

在我的Spark 1.6.1環境中,代碼沒有引發任何錯誤,但是僅生成了一個文件。 它沒有被兩個文件夾分區。

希望這會有所幫助。

我有一個類似的用例。 我通過編寫兩個實現MultipleTextOutputFormatRecordWriter自定義類在Java中解決了該RecordWriter

我的輸入是JavaPairRDD<String, List<String>> ,我想將其存儲在以其鍵命名的文件中,其值中包含所有行。

這是我的MultipleTextOutputFormat實現的代碼

class RDDMultipleTextOutputFormat<K, V> extends MultipleTextOutputFormat<K, V> {

    @Override
    protected String generateFileNameForKeyValue(K key, V value, String name) {
        return key.toString(); //The return will be used as file name
    }

    /** The following 4 functions are only for visibility purposes                 
    (they are used in the class MyRecordWriter) **/
    protected String generateLeafFileName(String name) {
        return super.generateLeafFileName(name);
    }

    protected V generateActualValue(K key, V value) {
        return super.generateActualValue(key, value);
    }

    protected String getInputFileBasedOutputFileName(JobConf job,     String name) {
        return super.getInputFileBasedOutputFileName(job, name);
        }

    protected RecordWriter<K, V> getBaseRecordWriter(FileSystem fs, JobConf job, String name, Progressable arg3) throws IOException {
        return super.getBaseRecordWriter(fs, job, name, arg3);
    }

    /** Use my custom RecordWriter **/
    @Override
    RecordWriter<K, V> getRecordWriter(final FileSystem fs, final JobConf job, String name, final Progressable arg3) throws IOException {
    final String myName = this.generateLeafFileName(name);
        return new MyRecordWriter<K, V>(this, fs, job, arg3, myName);
    }
} 

這是我的RecordWriter實現的代碼。

class MyRecordWriter<K, V> implements RecordWriter<K, V> {

    private RDDMultipleTextOutputFormat<K, V> rddMultipleTextOutputFormat;
    private final FileSystem fs;
    private final JobConf job;
    private final Progressable arg3;
    private String myName;

    TreeMap<String, RecordWriter<K, V>> recordWriters = new TreeMap();

    MyRecordWriter(RDDMultipleTextOutputFormat<K, V> rddMultipleTextOutputFormat, FileSystem fs, JobConf job, Progressable arg3, String myName) {
        this.rddMultipleTextOutputFormat = rddMultipleTextOutputFormat;
        this.fs = fs;
        this.job = job;
        this.arg3 = arg3;
        this.myName = myName;
    }

    @Override
    void write(K key, V value) throws IOException {
        String keyBasedPath = rddMultipleTextOutputFormat.generateFileNameForKeyValue(key, value, myName);
        String finalPath = rddMultipleTextOutputFormat.getInputFileBasedOutputFileName(job, keyBasedPath);
        Object actualValue = rddMultipleTextOutputFormat.generateActualValue(key, value);
        RecordWriter rw = this.recordWriters.get(finalPath);
        if(rw == null) {
            rw = rddMultipleTextOutputFormat.getBaseRecordWriter(fs, job, finalPath, arg3);
            this.recordWriters.put(finalPath, rw);
        }
        List<String> lines = (List<String>) actualValue;
        for (String line : lines) {
            rw.write(null, line);
        }
    }

    @Override
    void close(Reporter reporter) throws IOException {
        Iterator keys = this.recordWriters.keySet().iterator();

        while(keys.hasNext()) {
            RecordWriter rw = (RecordWriter)this.recordWriters.get(keys.next());
            rw.close(reporter);
        }

        this.recordWriters.clear();
    }
}

大多數代碼與FileOutputFormat中的代碼完全相同。 唯一的區別是那幾行

List<String> lines = (List<String>) actualValue;
for (String line : lines) {
    rw.write(null, line);
}

這些行允許我將輸入List<String>每一行寫在文件上。 為了避免將密鑰寫在每一行上, write函數的第一個參數設置為null

最后,我只需要執行此調用即可寫入文件

javaPairRDD.saveAsHadoopFile(path, String.class, List.class, RDDMultipleTextOutputFormat.class);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM