如何使saveAsTextFile不将输出分成多个文件?


77

在Spark中使用Scala时,每当我使用来转储结果时saveAsTextFile,似乎会将输出分成多个部分。我只是将参数(路径)传递给它。

val year = sc.textFile("apat63_99.txt").map(_.split(",")(1)).flatMap(_.split(",")).map((_,1)).reduceByKey((_+_)).map(_.swap)
year.saveAsTextFile("year")
  1. 输出数量是否与其使用的减速器数量相对应?
  2. 这是否意味着输出已压缩?
  3. 我知道我可以使用bash将输出组合在一起,但是是否可以选择将输出存储在单个文本文件中而不拆分?我看了一下API文档,但是关于它并没有说太多。

2
如果大文件中只使用一个文件,通常是个坏习惯。
samthebest 2014年

如果输出是一个已排序的文件,那么最佳实践是什么?将其保留为文件的集合,并使许多输出文件名成为某种索引(例如,第一个文件名为“ aa”,中间文件名为“ fg”,最后一个文件“ zzy”)?
Rdesmond

通常情况下,繁重的火花作业只会产生在hdfs上产生的非常小的输出(聚合,kpi,受欢迎程度...),但很可能会被与大数据无关的应用程序使用。在这种情况下,拥有一个命名良好的单个文件以进行传输和使用更为干净和容易。
Xavier Guihot

Answers:


99

之所以将其保存为多个文件,是因为计算是分布式的。如果输出足够小,以至于您认为可以将其安装在一台机器上,则可以使用以下命令结束程序

val arr = year.collect()

然后将结果数组另存为文件。另一种方法是使用自定义分区程序partitionBy,并使其一切都进入一个分区,尽管这是不可取的,因为您不会得到任何并行化。

如果您需要与文件一起保存,则saveAsTextFile可以使用coalesce(1,true).saveAsTextFile()。这基本上意味着进行计算,然后合并到1个分区。您还可以使用shuffle参数设置为truerepartition(1)的包装器coalesce。查看RDD.scala的来源是我弄清楚了大部分内容的方法,您应该看看。


2
如何将数组另存为文本文件?没有数组的saveAsTextFile函数。仅用于RDD。
user2773013 2014年

5
@ user2773013很好的方法就是我建议coalescepartition方法,但是如果仅在1个节点上存储在hdfs上确实没有意义,这就是为什么使用collect确实是正确的方法
aaronman 2014年

非常有用的答案。...在我读过的教程中没有看到partitionBy或合并...

34

对于使用较大数据集的用户

  • rdd.collect()在这种情况下不应该使用它,因为它将在驱动程序中以的形式收集所有数据Array,这是获取内存的最简单方法。

  • rdd.coalesce(1).saveAsTextFile() 也不应使用上游级的并行性,因为上游级的并行性将丢失,无法在存储数据的单个节点上执行。

  • rdd.coalesce(1, shuffle = true).saveAsTextFile() 是最好的简单选择,因为它将保持并行处理上游任务,然后仅对一个节点执行洗牌(这rdd.repartition(1).saveAsTextFile()是确切的同义词)。

  • rdd.saveAsSingleTextFile()如下提供的如下所示,它还允许将rdd存储在具有指定名称的一个文件中同时保留的并行性rdd.coalesce(1, shuffle = true).saveAsTextFile()


可能不方便的rdd.coalesce(1, shuffle = true).saveAsTextFile("path/to/file.txt")是,它实际上生成的文件的路径为path/to/file.txt/part-00000and而不是path/to/file.txt

以下解决方案rdd.saveAsSingleTextFile("path/to/file.txt")实际上将产生一个路径为的文件path/to/file.txt

package com.whatever.package

import org.apache.spark.rdd.RDD
import org.apache.hadoop.fs.{FileSystem, FileUtil, Path}
import org.apache.hadoop.io.compress.CompressionCodec

object SparkHelper {

  // This is an implicit class so that saveAsSingleTextFile can be attached to
  // SparkContext and be called like this: sc.saveAsSingleTextFile
  implicit class RDDExtensions(val rdd: RDD[String]) extends AnyVal {

    def saveAsSingleTextFile(path: String): Unit =
      saveAsSingleTextFileInternal(path, None)

    def saveAsSingleTextFile(path: String, codec: Class[_ <: CompressionCodec]): Unit =
      saveAsSingleTextFileInternal(path, Some(codec))

    private def saveAsSingleTextFileInternal(
        path: String, codec: Option[Class[_ <: CompressionCodec]]
    ): Unit = {

      // The interface with hdfs:
      val hdfs = FileSystem.get(rdd.sparkContext.hadoopConfiguration)

      // Classic saveAsTextFile in a temporary folder:
      hdfs.delete(new Path(s"$path.tmp"), true) // to make sure it's not there already
      codec match {
        case Some(codec) => rdd.saveAsTextFile(s"$path.tmp", codec)
        case None        => rdd.saveAsTextFile(s"$path.tmp")
      }

      // Merge the folder of resulting part-xxxxx into one file:
      hdfs.delete(new Path(path), true) // to make sure it's not there already
      FileUtil.copyMerge(
        hdfs, new Path(s"$path.tmp"),
        hdfs, new Path(path),
        true, rdd.sparkContext.hadoopConfiguration, null
      )
      // Working with Hadoop 3?: https://stackoverflow.com/a/50545815/9297144

      hdfs.delete(new Path(s"$path.tmp"), true)
    }
  }
}

可以这样使用:

import com.whatever.package.SparkHelper.RDDExtensions

rdd.saveAsSingleTextFile("path/to/file.txt")
// Or if the produced file is to be compressed:
import org.apache.hadoop.io.compress.GzipCodec
rdd.saveAsSingleTextFile("path/to/file.txt.gz", classOf[GzipCodec])

此代码段:

  • 首先,将rdd与一起存储rdd.saveAsTextFile("path/to/file.txt")在一个临时文件夹中path/to/file.txt.tmp,就像我们不想将数据存储在一个文件中一样(这使上游任务的处理保持并行)

  • 然后,仅使用hadoop文件系统api进行不同输出文件的mergeFileUtil.copyMerge()),以创建最终的单个输出文件path/to/file.txt


22

您可以先打电话coalesce(1),然后再打电话saveAsTextFile()-但如果您有很多数据,这可能不是一个好主意。就像在Hadoop中一样,每个拆分生成单独的文件,以便让单独的映射器和化简器写入不同的文件。如果您只有很少的数据,则只有一个输出文件是一个好主意,在这种情况下,您也可以执行collect(),如@aaronman所说。


Nice并没有想到coalesce要比使用分区程序更干净,也就是说,我仍然认为如果您的目标是将其保存到一个文件collect中可能是正确的方法
aaronman 2014年

1
这可行。但是,如果使用合并,则意味着您仅使用1个reducer。这样会不会减慢速度,因为只使用了1个减速器?
user2773013 2014年

1
是的,但这就是您要的。Spark每个分区输出一个文件。另一方面,为什么还要关心文件数量?在Spark中读取文件时,您只需指定父目录即可,所有分区都作为一个RDD读取
David

1
不要coalesce(1)取悦,除非您知道自己在做什么
gsamaras's

4

正如其他人提到的那样,您可以收集或合并数据集以强制Spark生成单个文件。但这也限制了可以并行处理数据集的Spark任务的数量。我更喜欢让它在输出HDFS目录中创建一百个文件,然后用于hadoop fs -getmerge /hdfs/dir /local/file.txt将结果提取到本地文件系统中的单个文件中。当然,当您的输出是相对较小的报告时,这才最有意义。


2

您可以致电repartition()并按照以下方式操作:

val year = sc.textFile("apat63_99.txt").map(_.split(",")(1)).flatMap(_.split(",")).map((_,1)).reduceByKey((_+_)).map(_.swap)

var repartitioned = year.repartition(1)
repartitioned.saveAsTextFile("C:/Users/TheBhaskarDas/Desktop/wc_spark00")

在此处输入图片说明


1

您将能够在Spark的下一版本中执行此操作,在当前版本1.0.0中,除非您以某种方式手动执行此操作(例如,如您提到的那样,通过bash脚本调用),否则是不可能的。


2
下一个版本的Spark在这里,但操作方法并不明显:(
CiprianTomoiagă17年

1

我还想提及一下,该文档明确指出,在使用数量很少的分区进行合并时,用户应格外小心。这可能导致上游分区继承此数量的分区。

除非确实需要,否则我不建议您使用Coalesce(1)。


1

在Spark 1.6.1中,格式如下所示。它会创建一个输出文件,如果输出足够小,则最好使用该文件。基本上,它的作用是返回一个新的RDD,并将其缩减为numPartitions分区。例如,对于numPartitions = 1,这可能导致您的计算在少于您希望的节点上进行(例如,在numPartitions = 1的情况下为一个节点)

pair_result.coalesce(1).saveAsTextFile("/app/data/")

0

这是我输出单个文件的答案。我刚刚添加coalesce(1)

val year = sc.textFile("apat63_99.txt")
              .map(_.split(",")(1))
              .flatMap(_.split(","))
              .map((_,1))
              .reduceByKey((_+_)).map(_.swap)
year.saveAsTextFile("year")

码:

year.coalesce(1).saveAsTextFile("year")
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.