如何使saveAsTextFile NOT分割输出到多个文件?

时间:2014-06-23 16:52:54

标签: scala apache-spark

在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文档,但对此没有太多说明。

8 个答案:

答案 0 :(得分:95)

将其保存为多个文件的原因是因为计算是分布式的。如果输出足够小,以至于您认为可以将其放在一台机器上,那么您可以使用

结束程序
val arr = year.collect()

然后将生成的数组保存为文件,另一种方法是使用自定义分区程序partitionBy,并将其设置为所有内容都转到一个分区,虽然这是不可取的,因为你不会得到任何并行化。

如果您要求使用saveAsTextFile保存文件,则可以使用coalesce(1,true).saveAsTextFile()。这基本上意味着计算然后合并到1分区。你也可以使用repartition(1),它只是coalesce的包装器,shuffle参数设置为true。查看RDD.scala的来源是我如何找出这些东西的大部分内容,你应该看看。

答案 1 :(得分:21)

您可以拨打coalesce(1),然后拨打saveAsTextFile() - 但如果您拥有大量数据,则可能不是一个好主意。生成每个拆分的单独文件就像在Hadoop中一样,以便让单独的映射器和Reducer写入不同的文件。如果您的数据非常少,那么只有一个输出文件是个好主意,在这种情况下,您也可以使用collect(),就像@aaronman所说的那样。

答案 2 :(得分:21)

对于那些使用更大的数据集的人:

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

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

  • rdd.coalesce(1, shuffle = true).saveAsTextFile() 是最简单的选项,因为它会使上游任务的处理保持并行,然后只对一个节点执行shuffle(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-00000而不是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.saveAsTextFile("path/to/file.txt")的rdd存储在临时文件夹path/to/file.txt.tmp中,好像我们不想将数据存储在一个文件中(这使得上游任务的处理保持并行)

  • 然后,只使用hadoop file system api,我们继续使用不同输出文件的mergeFileUtil.copyMerge())来创建最终输出单个文件{{1} }。

答案 3 :(得分:4)

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

答案 4 :(得分:1)

您将能够在下一版本的Spark中执行此操作,在当前版本1.0.0中,除非您以某种方式手动执行此操作,例如,如您所提到的,使用bash脚本调用。

答案 5 :(得分:1)

我还想提一下,文档清楚地指出用户在使用真正的少量分区调用coalesce时应该小心。这可能导致上游分区继承此数量的分区。

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

答案 6 :(得分:1)

在Spark 1.6.1中,格式如下所示。它创建一个单独的输出文件。如果输出足够小以便处理,最好使用它。它的作用基本上是它返回一个新的RDD,它被缩减为numPartitions分区。如果你做了一个激烈的合并,例如到numPartitions = 1,这可能导致您的计算发生在比您喜欢的节点更少的节点上(例如,在numPartitions = 1的情况下,一个节点)

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

答案 7 :(得分: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")