web-dev-qa-db-ja.com

キーSpark-1つのSparkジョブによる複数の出力への書き込み

単一のジョブでSparkを使用して、キーに依存する複数の出力に書き込むにはどうすればよいですか。

関連: キーScalding Hadoop、1つの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 を参照してください。以前の回答。

60
samthebest

これには、要求されたコーデック、必要なインポート、要求されたポン引きが含まれます。

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との微妙な違いの1つは、<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の例を示します。これは、おそらくタプルを使用するよりも優れています。

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

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
107
Nick Chammas

スケーラブルなこのようにします

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は、各キーのファイルを作成します。同じキーを持つ複数のレコードが同じパーティションに分類されることは問題ありません。

new HashPartitioner(num)。numは必要なパーティション番号です。多数の異なるキーがある場合は、numberをbigに設定できます。この場合、各パーティションは多くのhdfsファイルハンドラーを開きません。

81
zhang zhan

特定のキーに多数の値がある可能性がある場合、スケーラブルなソリューションはパーティションごとのキーごとに1つのファイルを書き出すことだと思います。残念ながら、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を1回通過し、シャッフルは実行されません。キーごとに1つのディレクトリがあり、それぞれに多数のファイルがあります。

16
Daniel Darabos

同様のニーズがあり、方法を見つけました。ただし、1つの欠点があります(私の場合は問題ではありません)。出力ファイルごとに1つのパーティションでデータを再パーティションする必要があります。

この方法でパーティションを作成するには、通常、ジョブが出力するファイルの数を事前に把握し、各キーを各パーティションにマップする関数を見つける必要があります。

まず、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つのファイルが生成され、すべてが1つのパスで処理されます。

ご覧のとおり、このソリューションを使用するにはキーに関する知識が必要です。

私にとっては、キーハッシュごとに1つの出力ファイルが必要であり、ファイルの数が自分の管理下にあったので簡単でした。したがって、ストックHashPartitionerを使用してトリックを行うことができました。

4
douglaz

Javaでも同じことが必要でした。 Zhang ZhanのScala answer の翻訳をSpark Java AP​​Iユーザーに投稿:

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

Hadoop HDFS上の入力ファイルをキーに基づいて複数のファイルに分割する同様のユースケースがありました(キーごとに1ファイル)。これがスパークの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))

キーに基づいてレコードをグループ化しました。各キーの値は個別のファイルに書き込まれます。

3
shanmuga

saveAsText()およびsaveAsHadoop(...)は、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}

v.toSeqでのイテレータの実体化がメモリに収まらない可能性があるため、大規模なデータセットでは機能しないことに注意してください。

私が見る他のオプション、そして実際このケースでお勧めするオプションは、hadoop/hdfs apiを直接呼び出すことにより、独自のオプションをロールバックすることです。

この質問の調査中に始めたディスカッションは次のとおりです。 別のRDDからRDDを作成する方法

3
maasg

同様のユースケースがありました。 MultipleTextOutputFormatRecordWriterを実装する2つのカスタムクラスを記述することにより、Javaで解決しました。

私の入力は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);
1
jeanr

pythonユーザーにとっては朗報です。複数の列があり、csv形式でパーティション分割されていない他のすべての列を保存したい場合は、ニックテキストの提案として "text"メソッドを使用すると失敗します。

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

エラーメッセージは「AnalysisException:u'Text data source support only single column、and you have 2 columns .;」

spark 2.0.0(私のテスト環境はhdpのspark 2.0.0)パッケージ "com.databricks.spark.csv"が統合され、テキストファイルを分割して保存できるようになりました1列だけで、打撃の例を参照してください:

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環境では、コードはエラーをスローしませんでしたが、生成されるファイルは1つだけです。 2つのフォルダーに分割されていません。

これが役立つことを願っています。

1
dalin qin