web-dev-qa-db-fra.com

Écrire dans plusieurs sorties par clé Spark - un travail Spark

Comment écrire sur plusieurs sorties en fonction de la clé en utilisant Spark dans un seul travail.

Connexes: Écriture sur plusieurs sorties avec la clé Scalding Hadoop, un travail MapReduce

Par exemple.

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

s'assurerait que cat prefix/1 est

a
b

et cat prefix/2 serait

c

EDIT: J'ai récemment ajouté une nouvelle réponse incluant les importations complètes, le proxénète et le codec de compression, voir https://stackoverflow.com/a/46118044/1586965 , qui peut être utile en plus des réponses précédentes.

58
samthebest

Cela inclut le codec tel que demandé, les importations nécessaires et le proxénète tel que demandé.

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

Une différence subtile avec l'OP est qu'il préfixera <keyName>= aux noms de répertoire. Par exemple.

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

Donnerait:

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

prefix/my_number=1/part-00000 contiendrait les lignes a et b, et prefix/my_number=2/part-00000 contiendrait la ligne c.

Et

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

Donnerait:

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

Il devrait être clair comment éditer pour parquet

Enfin, ci-dessous, vous trouverez un exemple pour Dataset, qui est peut-être plus pratique que d’utiliser 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

Si vous utilisez Spark 1.4+, cela devient beaucoup plus facile grâce à l’API DataFrame API . (Les DataFrames ont été introduits dans Spark 1.3, mais partitionBy(), dont nous avons besoin, était introduit dans 1.4 .)

Si vous commencez avec un RDD, vous devrez d'abord le convertir en un DataFrame:

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

En Python, ce même code est:

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

Une fois que vous avez un DataFrame, l'écriture sur plusieurs sorties en fonction d'une clé particulière est simple. De plus, et c'est la beauté de l'API DataFrame, le code est à peu près le même en Python, Scala, Java et R:

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

Et vous pouvez facilement utiliser d'autres formats de sortie si vous voulez:

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

Dans chacun de ces exemples, Spark créera un sous-répertoire pour chacune des clés sur lesquelles nous avons partitionné le DataFrame:

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

Je le ferais comme ça, qui est évolutif

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

Je viens de voir une réponse similaire ci-dessus, mais en réalité nous n’avons pas besoin de partitions personnalisées. MultipleTextOutputFormat créera un fichier pour chaque clé. Il est normal que plusieurs enregistrements avec les mêmes clés entrent dans la même partition. 

new HashPartitioner (num), où num correspond au numéro de la partition souhaitée. Si vous avez un grand nombre de clés différentes, vous pouvez définir le nombre sur grand. Dans ce cas, chaque partition n'ouvrira pas trop de gestionnaires de fichiers hdfs.

79
zhang zhan

Si vous avez potentiellement plusieurs valeurs pour une clé donnée, je pense que la solution évolutive consiste à écrire un fichier par clé par partition. Malheureusement, il n’existe pas de support intégré dans Spark, mais nous pouvons créer quelque chose.

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

(Remplacez PrintWriter par votre choix d’utilisation du système de fichiers distribué.)

Cela effectue un seul passage sur le RDD et ne fait aucun mélange. Il vous donne un répertoire par clé, avec un nombre de fichiers à l'intérieur.

15
Daniel Darabos

J'ai un besoin similaire et j'ai trouvé un moyen. Mais il a un inconvénient (qui ne pose pas de problème pour mon cas): vous devez re-partitionner vos données avec une partition par fichier de sortie.

Pour partitionner de cette manière, il est généralement nécessaire de connaître au préalable le nombre de fichiers que le travail va générer et de trouver une fonction qui mappera chaque clé sur chaque partition.

Commençons par créer notre classe basée sur 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
  }
}

Avec cette classe, Spark obtiendra une clé d'une partition (la première/dernière, je suppose) et nommera le fichier avec cette clé. Il n'est donc pas bon de mélanger plusieurs clés sur la même partition.

Pour votre exemple, vous aurez besoin d'un partitionneur personnalisé. Cela fera le travail:

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

Maintenant mettons tout ensemble:

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

Cela générera 3 fichiers sous le préfixe (nommés 1, 2 et 7), traitant tout en un seul passage.

Comme vous pouvez le constater, vous devez avoir quelques connaissances sur vos clés pour pouvoir utiliser cette solution.

Pour moi, c'était plus facile, car j'avais besoin d'un fichier de sortie pour chaque hachage de clé et que le nombre de fichiers était sous mon contrôle, je pouvais donc utiliser le stock HashPartitioner pour faire l'affaire.

4
douglaz

J'avais besoin de la même chose en Java. Publier ma traduction de La réponse Scala de Zhang Zhan aux utilisateurs de l'API Spark Java:

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

J'ai eu un cas d'utilisation similaire où j'ai divisé le fichier d'entrée sur Hadoop HDFS en plusieurs fichiers basés sur une clé (1 fichier par clé). Voici mon code scala pour spark

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

J'ai regroupé les enregistrements en fonction de la clé. Les valeurs de chaque clé sont écrites dans un fichier séparé.

3
shanmuga

saveAsText () et saveAsHadoop (...) sont implémentés sur la base des données RDD, en particulier par la méthode: PairRDD.saveAsHadoopDataset qui prend les données de PairRdd où elles sont exécutées . Deux options sont possibles: Si vos données sont de taille relativement petite, vous pouvez gagner du temps d’implémentation en les regroupant sur le RDD, en créant un nouveau RDD à partir de chaque collection et en utilisant ce RDD pour écrire les données. Quelque chose comme ça:

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}

Notez que cela ne fonctionnera pas pour les grands ensembles de données car la matérialisation de l'itérateur à v.toSeq pourrait ne pas tenir dans la mémoire.

L’autre option que je vois, et celle que je recommanderais dans ce cas-ci est la suivante: lancez le vôtre, en appelant directement l’API hadoop/hdfs.

Voici une discussion que j'ai commencée en cherchant cette question: Comment créer des RDD à partir d'un autre RDD?

3
maasg

bonne nouvelle pour l'utilisateur python si vous avez plusieurs colonnes et que vous voulez sauvegarder toutes les autres colonnes non partitionnées au format CSV, ce qui échouera si vous utilisez la méthode "text" comme le suggère Nick Chammas.

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

le message d'erreur est "AnalysisException: la source de données u'Text ne prend en charge qu'une seule colonne et vous avez 2 colonnes; '"

Dans spark 2.0.0 (mon environnement de test est l'étincelle 2.0.0 de hdp), le paquetage "com.databricks.spark.csv" est maintenant intégré et nous permet d'enregistrer le fichier texte partitionné par une seule colonne, voir l'exemple:

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

Dans mon environnement d'allumage 1.6.1, le code n'a généré aucune erreur, mais il n'y a qu'un seul fichier généré. ce n'est pas partitionné par deux dossiers.

J'espère que cela peut aider.

1
dalin qin

J'ai eu un cas d'utilisation similaire. Je l'ai résolu en Java en écrivant deux classes personnalisées implémentant MultipleTextOutputFormat et RecordWriter

Mon entrée était un JavaPairRDD<String, List<String>> et je voulais le stocker dans un fichier nommé par sa clé, avec toutes les lignes contenues dans sa valeur.

Voici le code de mon implémentation 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);
    }
} 

Voici le code de mon implémentation 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();
    }
}

La plupart du code est exactement le même que dans FileOutputFormat. La seule différence est ces quelques lignes

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

Ces lignes m'ont permis d'écrire chaque ligne de mon entrée List<String> sur le fichier. Le premier argument de la fonction write est défini sur null afin d'éviter d'écrire la clé sur chaque ligne.

Pour finir, il me suffit de faire cet appel pour écrire mes fichiers

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