it-swarm.com.de

Schreiben Sie mehrere Ausgaben mit einem Spark-Schlüssel - einem Spark-Job

Wie können Sie mit Spark in einem einzigen Job auf mehrere Ausgaben zugreifen, die vom Schlüssel abhängen.

Verwandt: Schreiben Sie mehrere Ausgaben mit der Taste Verbrühungshadoop, einem MapReduce-Job

Z.B.

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

würde sicherstellen, dass cat prefix/1 ist

a
b

und cat prefix/2 wäre

c

BEARBEITEN: Ich habe vor kurzem eine neue Antwort hinzugefügt, die vollständige Importe, Pimp- und Kompressionscodecs enthält, siehe https://stackoverflow.com/a/46118044/1586965 , die zusätzlich zu den vorherigen Antworten hilfreich sein können.

58
samthebest

Dazu gehören der gewünschte Codec, die erforderlichen Importe und der Zuhälter.

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")

Ein geringfügiger Unterschied zum OP besteht darin, dass den Verzeichnisnamen <keyName>= vorangestellt wird. Z.B.

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

Würde geben:

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

wobei prefix/my_number=1/part-00000 die Zeilen a und b und prefix/my_number=2/part-00000 die Zeile c enthalten würde.

Und

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

Würde geben:

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

Es sollte klar sein, wie man parquet bearbeitet. 

Schließlich ist unten ein Beispiel für Dataset, das vielleicht schöner ist als die Verwendung von 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)
  }
}
6
samthebest

Wenn Sie Spark 1.4+ verwenden, ist dies dank der DataFrame-API wesentlich einfacher geworden. (DataFrames wurden in Spark 1.3 eingeführt, aber partitionBy(), das wir brauchen, wurde in 1.4 eingeführt.)

Wenn Sie mit einer RDD beginnen, müssen Sie sie zuerst in einen DataFrame konvertieren:

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

In Python lautet derselbe Code:

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

Sobald Sie einen DataFrame haben, ist das Schreiben in mehrere Ausgaben auf Grundlage eines bestimmten Schlüssels einfach. Was mehr ist - und das ist die Schönheit der DataFrame-API - der Code ist in Python, Scala, Java und R ziemlich gleich:

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

Sie können auch andere Ausgabeformate verwenden, wenn Sie möchten:

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

In jedem dieser Beispiele erstellt Spark ein Unterverzeichnis für jeden der Schlüssel, für die wir den DataFrame partitioniert haben:

people/
  _SUCCESS
  number=1/
    part-abcd
    part-efgh
  number=2/
    part-abcd
    part-efgh
101
Nick Chammas

Ich würde es so machen, das ist skalierbar

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()
  }
}

Ich habe oben bereits eine ähnliche Antwort gesehen, aber eigentlich brauchen wir keine benutzerdefinierten Partitionen. Das MultipleTextOutputFormat erstellt für jeden Schlüssel eine Datei. Es ist in Ordnung, dass mehrere Datensätze mit denselben Schlüsseln in dieselbe Partition fallen. 

new HashPartitioner (num), wobei die Nummer die gewünschte Partitionsnummer ist. Falls Sie eine große Anzahl verschiedener Tasten haben, können Sie die Anzahl groß einstellen. In diesem Fall werden mit jeder Partition nicht zu viele HDFS-Dateibehandler geöffnet.

79
zhang zhan

Wenn Sie potenziell viele Werte für einen bestimmten Schlüssel haben, denke ich, dass die skalierbare Lösung darin besteht, eine Datei pro Schlüssel pro Partition zu schreiben. Leider gibt es in Spark keine integrierte Unterstützung dafür, aber wir können etwas herausfinden.

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)
}

(Ersetzen Sie PrintWriter durch den von Ihnen gewählten Betrieb des verteilten Dateisystems.)

Dadurch wird die RDD einmalig durchlaufen, und es erfolgt kein Shuffle. Es gibt Ihnen ein Verzeichnis pro Schlüssel mit einer Anzahl von Dateien in jedem.

15
Daniel Darabos

Ich habe ein ähnliches Bedürfnis und habe einen Weg gefunden. Aber es hat einen Nachteil (was für meinen Fall kein Problem ist): Sie müssen Ihre Daten mit einer Partition pro Ausgabedatei neu partitionieren.

Um auf diese Weise zu partitionieren, muss im Allgemeinen vorher bekannt sein, wie viele Dateien der Job ausgeben soll, und es wird eine Funktion gefunden, mit der jeder Schlüssel jeder Partition zugeordnet wird.

Zuerst erstellen wir unsere auf MultipleTextOutputFormat basierende Klasse:

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
  }
}

Mit dieser Klasse erhält Spark einen Schlüssel von einer Partition (der erste/letzte, denke ich) und benennt die Datei mit diesem Schlüssel. Daher ist es nicht gut, mehrere Schlüssel auf derselben Partition zu mischen.

Für Ihr Beispiel benötigen Sie einen benutzerdefinierten Partitionierer. Dies wird die Arbeit erledigen:

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
  }
}

Jetzt lass uns alles zusammenfügen:

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]])

Dadurch werden 3 Dateien unter dem Präfix (1, 2 und 7) generiert, die alles in einem Durchgang verarbeiten.

Wie Sie sehen, benötigen Sie Kenntnisse über Ihre Schlüssel, um diese Lösung verwenden zu können.

Für mich war es einfacher, weil ich für jeden Schlüsselhash eine Ausgabedatei benötigte und die Anzahl der Dateien unter meiner Kontrolle stand, sodass ich den Hash-Part-Hasher verwenden konnte, um den Trick auszuführen.

4
douglaz

Ich brauchte dasselbe in Java. Meine Übersetzung der Zhang Zhans Scala-Antwort veröffentlichen an die Benutzer von 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();
    }
}
4
Thamme Gowda

Ich hatte einen ähnlichen Anwendungsfall, bei dem ich die Eingabedatei auf Hadoop HDFS in mehrere Dateien auf Basis eines Schlüssels aufteilte (1 Datei pro Schlüssel). Hier ist mein Scala-Code für Funken

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))

Ich habe die Datensätze nach Schlüssel gruppiert. Die Werte für jeden Schlüssel werden in eine separate Datei geschrieben.

3
shanmuga

saveAsText () und saveAsHadoop (...) werden basierend auf den RDD-Daten implementiert, und zwar durch die Methode: PairRDD.saveAsHadoopDataset die die Daten von der PairRdd übernimmt, wo sie ausgeführt werden . Ich sehe zwei mögliche Optionen: Wenn Ihre Daten relativ klein sind, können Sie Implementierungszeit einsparen, indem Sie über die RDD gruppieren, eine neue RDD aus jeder Sammlung erstellen und diese RDD zum Schreiben der Daten verwenden. Etwas wie das:

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}

Beachten Sie, dass dies für große Datensätze nicht funktioniert, da die Materialisierung des Iterators bei v.toSeq möglicherweise nicht in den Speicher passt.

Die andere Option, die ich sehe, und eigentlich die, die ich in diesem Fall empfehlen würde, ist: Rollen Sie Ihre eigene, indem Sie die hadoop/hdfs-API direkt aufrufen.

Hier ist eine Diskussion, die ich bei der Erforschung dieser Frage begann: Wie erstelle ich RDDs aus einer anderen RDD?

3
maasg

gute Nachrichten für Python-Benutzer, wenn Sie mehrere Spalten haben und alle anderen Spalten speichern möchten, die nicht im CSV-Format partitioniert sind. Dies schlägt fehl, wenn Sie die "Text" -Methode als Vorschlag von Nick Chammas verwenden.

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

fehlermeldung ist "AnalysisException: u'Text-Datenquelle unterstützt nur eine einzelne Spalte und Sie haben 2 Spalten."

In spark 2.0.0 (meine Testumgebung ist hdp spark 2.0.0) ist jetzt das Paket "com.databricks.spark.csv" integriert, mit dem wir Textdateien speichern können, die mit nur einer Spalte partitioniert sind. Siehe Beispiel blow:

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")

[[email protected] 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

[[email protected] 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
[[email protected] people]# cat number\=2/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,charlie

In meiner Spark 1.6.1-Umgebung hat der Code keinen Fehler ausgegeben, es wird jedoch nur eine Datei generiert. Es ist nicht durch zwei Ordner partitioniert.

Hoffe das kann helfen.

1
dalin qin

Ich hatte einen ähnlichen Anwendungsfall. Ich löste es in Java, indem ich zwei benutzerdefinierte Klassen schrieb, die MultipleTextOutputFormat und RecordWriter implementieren. 

Meine Eingabe war JavaPairRDD<String, List<String>> und ich wollte es in einer Datei speichern, die nach ihrem Schlüssel benannt wurde, wobei alle Zeilen in seinem Wert enthalten waren.

Hier ist der Code für meine MultipleTextOutputFormat-Implementierung

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);
    }
} 

Hier ist der Code für meine RecordWriter-Implementierung. 

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();
    }
}

Der größte Teil des Codes ist identisch mit FileOutputFormat. Der einzige Unterschied besteht in diesen wenigen Zeilen

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

Diese Zeilen erlaubten mir, jede Zeile meiner Eingabe List<String> in die Datei zu schreiben. Das erste Argument der write-Funktion ist auf null gesetzt, um zu vermeiden, dass der Schlüssel in jede Zeile geschrieben wird.

Zum Abschluss brauche ich nur diesen Aufruf, um meine Dateien zu schreiben

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