Spark自定义输出文件

一、概述

       本文将通过源码出发讲述spark如何调用hadoop几种OutputFormat,从而实现的文件输出,这里将讲述几种工作中常使用的算子,例如:saveAsTextFile(path) 、saveAsHadoopFile(path)

二、spark源码分析

       saveAsTextFile(path)底层调用也是saveAsHadoopFile(path),所以这里主要是讲述后者的源码;这一步也将带你认识到可以自定义的内容;

1.main

def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("")
    val sc = new SparkContext(conf)
    //禁用success文件
    sc.hadoopConfiguration.set("mapreduce.fileoutputcommitter.marksuccessfuljobs", "false")
    val value: RDD[(String,Int)] = sc.parallelize(List(
      ("1",1), ("1",1), ("2",1), ("2",1),("2",1),
    ))
    value1
      .saveAsHadoopFile("C:\\Users\\Desktop\\learn\\spark_program_test\\definedFileName"
                        ,classOf[String]
                        ,classOf[String]
                        ,classOf[TextOutputFormat[String,String]])
    sc.stop()
  }

2.PairRDDFunctions

def saveAsHadoopFile[F <: OutputFormat[K, V]](
      path: String)(implicit fm: ClassTag[F]): Unit = self.withScope {
    saveAsHadoopFile(path, keyClass, valueClass, fm.runtimeClass.asInstanceOf[Class[F]])
  }
def saveAsHadoopFile(
      path: String,
      keyClass: Class[_],
      valueClass: Class[_],
      outputFormatClass: Class[_ <: OutputFormat[_, _]],
      conf: JobConf = new JobConf(self.context.hadoopConfiguration),
      codec: Option[Class[_ <: CompressionCodec]] = None): Unit = self.withScope {
    // Rename this as hadoopConf internally to avoid shadowing (see SPARK-2038).
    val hadoopConf = conf
    hadoopConf.setOutputKeyClass(keyClass)
    hadoopConf.setOutputValueClass(valueClass)
    conf.setOutputFormat(outputFormatClass)
    for (c <- codec) {
      hadoopConf.setCompressMapOutput(true)
      hadoopConf.set("mapreduce.output.fileoutputformat.compress", "true")
      hadoopConf.setMapOutputCompressorClass(c)
      hadoopConf.set("mapreduce.output.fileoutputformat.compress.codec", c.getCanonicalName)
      hadoopConf.set("mapreduce.output.fileoutputformat.compress.type",
        CompressionType.BLOCK.toString)
    }

    // Use configured output committer if already set
    if (conf.getOutputCommitter == null) {
      hadoopConf.setOutputCommitter(classOf[FileOutputCommitter])
    }

    // When speculation is on and output committer class name contains "Direct", we should warn
    // users that they may loss data if they are using a direct output committer.
    val speculationEnabled = self.conf.getBoolean("spark.speculation", false)
    val outputCommitterClass = hadoopConf.get("mapred.output.committer.class", "")
    if (speculationEnabled && outputCommitterClass.contains("Direct")) {
      val warningMessage =
        s"$outputCommitterClass may be an output committer that writes data directly to " +
          "the final location. Because speculation is enabled, this output committer may " +
          "cause data loss (see the case in SPARK-10063). If possible, please use an output " +
          "committer that does not have this behavior (e.g. FileOutputCommitter)."
      logWarning(warningMessage)
    }

    FileOutputFormat.setOutputPath(hadoopConf,
      SparkHadoopWriterUtils.createPathFromString(path, hadoopConf))
    saveAsHadoopDataset(hadoopConf)
  }

       这里指定了OutputFormat为TextOutputFormat,如果不指定也是默认TextOutputFormat;进入到PairRDDFunctions第二个方法,其中saveAsHadoopDataset(hadoopConf)再进去;

def saveAsHadoopDataset(conf: JobConf): Unit = self.withScope {
    // Rename this as hadoopConf internally to avoid shadowing (see SPARK-2038).
    val hadoopConf = conf
    val outputFormatInstance = hadoopConf.getOutputFormat
    val keyClass = hadoopConf.getOutputKeyClass
    val valueClass = hadoopConf.getOutputValueClass
    if (outputFormatInstance == null) {
      throw new SparkException("Output format class not set")
    }
    if (keyClass == null) {
      throw new SparkException("Output key class not set")
    }
    if (valueClass == null) {
      throw new SparkException("Output value class not set")
    }
    SparkHadoopUtil.get.addCredentials(hadoopConf)

    logDebug("Saving as hadoop file of type (" + keyClass.getSimpleName + ", " +
      valueClass.getSimpleName + ")")

    if (SparkHadoopWriterUtils.isOutputSpecValidationEnabled(self.conf)) {
      // FileOutputFormat ignores the filesystem parameter
      val ignoredFs = FileSystem.get(hadoopConf)
      hadoopConf.getOutputFormat.checkOutputSpecs(ignoredFs, hadoopConf)
    }

    val writer = new SparkHadoopWriter(hadoopConf)
    writer.preSetup()

    val writeToFile = (context: TaskContext, iter: Iterator[(K, V)]) => {
      // Hadoop wants a 32-bit task attempt ID, so if ours is bigger than Int.MaxValue, roll it
      // around by taking a mod. We expect that no task will be attempted 2 billion times.
      val taskAttemptId = (context.taskAttemptId % Int.MaxValue).toInt

      val (outputMetrics, callback) = SparkHadoopWriterUtils.initHadoopOutputMetrics(context)

      writer.setup(context.stageId, context.partitionId, taskAttemptId)
      writer.open()
      var recordsWritten = 0L

      Utils.tryWithSafeFinallyAndFailureCallbacks {
        while (iter.hasNext) {
          val record = iter.next()
          writer.write(record._1.asInstanceOf[AnyRef], record._2.asInstanceOf[AnyRef])

          // Update bytes written metric every few records
          SparkHadoopWriterUtils.maybeUpdateOutputMetrics(outputMetrics, callback, recordsWritten)
          recordsWritten += 1
        }
      }(finallyBlock = writer.close())
      writer.commit()
      outputMetrics.setBytesWritten(callback())
      outputMetrics.setRecordsWritten(recordsWritten)
    }

    self.context.runJob(self, writeToFile)
    writer.commitJob()
  }

       到达这里就是写入文件的主要逻辑了:

       ①writer.open():他是SparkHadoopWriter的方法;首先这里会初始化文件名(诸如part-0000),然后传入你所设置的OutputFormat类的getRecordWriter返回的RecordWriter中,所以如果想要自定义文件名,从这里看来可以重写getRecordWriter方法,后面会讲解TextOutputFormat和MultipleTextOutputFormat如何重写的getRecordWriter;

def open() {
    val numfmt = NumberFormat.getInstance(Locale.US)
    numfmt.setMinimumIntegerDigits(5)
    numfmt.setGroupingUsed(false)

    val outputName = "part-"  + numfmt.format(splitID)
    val path = FileOutputFormat.getOutputPath(conf.value)
    val fs: FileSystem = {
      if (path != null) {
        path.getFileSystem(conf.value)
      } else {
        FileSystem.get(conf.value)
      }
    }
    getOutputCommitter().setupTask(getTaskContext())
    writer = getOutputFormat().getRecordWriter(fs, conf.value, outputName, Reporter.NULL)
  }

       ②writeToFile函数:这是一块是具体如何写入文件的,首先可以看出这里每个分区只会生成一个文件,然后是调用你所设置的OutputFormat所使用的RecordWriter的write方法写入文件;如果要自定义写入内容,也就要自定义RecordWriter类;

三、TextOutputFormat & MultipleTextOutputFormat

1.TextoutputFormat

       这个类可以直接复制,然后根据自己的需求稍微改点代码就行了,接下来我就从需求出来来看看怎么重写这个类;

       ①文件编码格式为UTF-8以外的编码格式,或者换行符不为 '\n':因为在TextoutputFormat的LineRecoderWriter中这两个是写死的,所以需要重写TextoutputFormat类,只需要复制整个类代码就行,然后修改你需要改的地方,例如:

public class MyOutput<K,V> extends FileOutputFormat<K, V> {
    protected static class LineRecordWriter<K, V>
            implements RecordWriter<K, V> {
        private static final String utf8 = "GBK";
        private static final byte[] newline;
        static {
            try {
                newline = "\r\n".getBytes(utf8);
            } catch (UnsupportedEncodingException uee) {
                throw new IllegalArgumentException("can't find " + utf8 + " encoding");
            }
        }
。。。

       这里就修改了编码格式和换行符

       ②key/value的分隔符,这个可以写死(重写的时候),也可以在main中改hadoopconf配置

def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("")
    val sc = new SparkContext(conf)
    //修改输出文件的key/value分隔符
    sc.hadoopConfiguration.set("mapreduce.output.textoutputformat.separator",",")

       ③修改文件名

@Override
    public RecordWriter<K, V> getRecordWriter(FileSystem ignored, JobConf job, String name, Progressable progress) throws IOException {
        //重写的类加上的,这个可以自定义
        name = Integer.parseInt(name.split("-")[1])+"";

。。。
}

       ④修改key/value写入逻辑:这里我没有做修改,可以根据业务逻辑修改;

  public synchronized void write(K key, V value)
                throws IOException {

            boolean nullKey = key == null || key instanceof NullWritable;
            boolean nullValue = value == null || value instanceof NullWritable;
            if (nullKey && nullValue) {
                return;
            }
            if (!nullKey) {
                writeObject(key);
            }
            if (!(nullKey || nullValue)) {
                out.write(keyValueSeparator);
            }
            if (!nullValue) {
                writeObject(value);
            }
            out.write(newline);
        }
//PairRddFunction中传过来的时候,key/value都转换为了Anyval,所以这里会走else
private void writeObject(Object o) throws IOException {
            if (o instanceof Text) {
                Text to = (Text) o;
                out.write(to.getBytes(), 0, to.getLength());
            } else {
                out.write(o.toString().getBytes(utf8));
            }
        }

四、MultipleTextOutputFormat

       这是hadoop提供的简易的自定义文件名,自定义输出key/value数据,但是最终写入文件还是TextOutputFormat的LineRcorderWriter,也就是说文件编码格式、换行符无法自定义。

  //修改生成的分区文件名,每个分区传入的name不同诸如:Part-0001,优先级低于generateFileNameForKeyValue
  protected String generateLeafFileName(String name) {
    return name;
  }

  //key,value不用解释,这里的name是generateLeafFileName返回的name,如果没有generateLeafFileName则是Part—0001,需要注意的是,由于是多分区写文件,如果不同分区生成文件名同样的文件,将会被覆盖,如果仅用key,必须保证相同key在同一分区,key+name则可以保证不会被覆盖,但是可能文件生成太多
  protected String generateFileNameForKeyValue(K key, V value, String name) {
    return name;
  }

//实际写入key
  protected K generateActualKey(K key, V value) {
    return key;
  }
  
  //实际写入的value
  protected V generateActualValue(K key, V value) {
    return value;
  }
  
  //这个方法决定了最终写入文件的RecorderWriter,是getRecordWriter方法调用的,实际上mutipleOutputFormat,重写的RcordWriter(内部类的形式),只是使得name,key,value可以自定义
  abstract protected RecordWriter<K, V> getBaseRecordWriter(FileSystem fs,
      JobConf job, String name, Progressable arg3) throws IOException;

       备注:1.如果业务逻辑保函需要按照规定的文件大小或者条数切分,除了用foreachPartition算子,剩下的就必须修改PairRddFunction类的saveAsHadoopDataset方法的源码了;

                  2.spark写本地文件会生成.crc文件,写入hdfs不会生成;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Spark文件时避免小文件过多的一种方法是使用"Coalesce"操作来合并小文件。 Coalesce操作可以将多个小文件合并为一个大文件,这样就可以减少小文件的数量。 例如,假设有一个RDD包含了100个小文件,你可以使用以下代码来使用Coalesce操作将它们合并为10个文件: ``` rdd.coalesce(10).saveAsTextFile(outputPath) ``` 需要注意的是,Coalesce操作只能用于减少文件的数量,不能用于增加文件的数量。如果你想增加文件的数量,可以使用"Repartition"操作。 另外,你也可以在Spark作业的配置中设置"spark.sql.shuffle.partitions"参数来控制Spark文件时生成的文件数量。这个参数用于指定Spark文件时生成的文件数量,默认值是200。 例如,你可以在Spark作业的配置中设置"spark.sql.shuffle.partitions"参数为100,这样Spark文件时就会生成100个文件。 ``` val conf = new SparkConf().set("spark.sql.shuffle.partitions", "100") val spark = SparkSession.builder.config(conf).getOrCreate() ``` 还有一种方法是使用自定义的"Partitioner"来控制文件的数量。你可以通过实现"org.apache.spark.Partitioner"接口,并将其传递给"saveAsHadoopFile"或"saveAsNewAPIHadoopFile"方法来实现这种方法。 例 ### 回答2: 在Spark中,可以采用以下几种方法来避免产生过多的小文件: 1. 合并小文件:对于产生的小文件,可以选择将其合并成一个较大的文件。可以通过使用`coalesce`或`repartition`方法将数据重新分区,减少输出文件的数量。 2. 增加分区数:通过增加输出数据的分区数,可以将数据均匀地分布到更多的分区上,从而减少每个分区中的数据量,避免产生过多的小文件。可以在写文件之前使用`repartition`或`repartitionByRange`方法进行数据重分区。 3. 控制输出文件的大小:可以设置输出文件的最大大小,当达到指定大小时,自动创建新的输出文件。可以通过设置`spark.hadoop.mapreduce.output.fileoutputformat.maxfilesize`参数来控制输出文件的大小。 4. 合并输出文件:可以通过使用`FileUtil`类的`copyMerge`方法将多个小文件合并成一个大文件。这个方法将在Hadoop分布式文件系统上执行文件合并操作。 5. 使用输出格式类:可以使用输出格式类来指定输出数据的格式,例如`TextOutputFormat`和`ParquetOutputFormat`等。这些输出格式类提供了对输出文件的更好控制,可以通过设置参数来控制输出文件的大小和数量。 需要注意的是,在使用以上方法时,需要根据具体的场景和需求来选择合适的方案。同时,也需要权衡时间和空间的消耗,以及对作业性能的影响。 ### 回答3: 在Spark中,为了避免生成过多的小文件,可以采取以下几种方法: 1. 合并小文件:将多个小文件合并成一个较大的文件。可以使用`coalesce`或`repartition`方法将RDD或DataFrame的分区数改为较少的数目,从而减少输出的小文件数量。 2. 控制输出分区数:在写入文件时,可以通过设置`writer`的`numPartitions`参数来控制输出文件的分区数。较少的分区数能够减少小文件的数量。 3. 提前聚合:在数据处理过程中,尽量提前进行聚合操作,减少中间结果的数量,从而减少输出的小文件数量。 4. 使用Hive分区:如果数据写入Hive表中,可以合理使用Hive的分区功能。在写入文件之前,将数据按照某个字段进行分区,从而可以有效地避免生成过多的小文件。 5. 合理设置输出文件格式:使用合适的文件格式可以减少小文件的数量。例如,使用`csv`格式时,每个RDD分区将生成一个文件,可改为使用`parquet`等格式。 6. 批量写入:避免使用循环逐条写入数据,可以将数据通过批量方式写入,减少小文件的生成。 总结起来,以上方法可以通过合并文件、控制分区数、提前聚合、合理使用Hive分区、选择合适的输出文件格式和批量写入等方式来避免Spark写入过多的小文件。通过对数据处理和输出的优化,可以减少小文件的数量,提高Spark任务的性能和效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

听箫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值