Spark 사용자 정의 출력 파일

I. 개요

이 기사는 소스 코드부터 시작하여 파일 출력을 달성하기 위해 Spark가 Hadoop의 여러 OutputFormat을 호출하는 방법을 설명합니다. 여기서는 saveAsTextFile ( path ) , saveAsHadoopFile ( path )        과 같이 작업에서 일반적으로 사용되는 여러 연산자에 대해 설명합니다.

2. 스파크 소스코드 분석

       saveAsTextFile ( path ) 의 기본 호출 도 saveAsHadoopFile ( path )이므로 여기서는 주로 후자의 소스 코드를 설명합니다. 이 단계는 또한 사용자 정의할 수 있는 콘텐츠를 실현하는 데 도움이 됩니다.

1.메인

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.PairRDD함수

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의 쓰기 메소드를 호출합니다. 직접 작성하고 싶습니다. 작성 내용을 정의하려면 RecordWriter 클래스를 사용자 정의해야 합니다.

3, TextOutputFormat 및 MultipleTextOutputFormat

1.텍스트 출력 형식

       이 클래스는 직접 복사할 수 있으며 필요에 따라 코드를 약간 변경할 수 있습니다.다음으로 요구 사항을 살펴보고 이 클래스를 다시 작성하는 방법을 살펴보겠습니다.

       ①파일 인코딩 형식이 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");
            }
        }
。。。

       여기서 인코딩 형식과 줄바꿈이 수정됩니다.

       ②키/값 구분 기호는 하드 코딩되거나(재작성 시) 기본에서 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])+"";

。。。
}

       ④ 키/값 쓰기 로직 수정: 여기서는 수정하지 않았습니다. 비즈니스 로직에 따라 수정할 수 있습니다.

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

넷, 다중 텍스트 출력 형식

       hadoop에서 제공하는 간단한 커스텀 파일명, 커스텀 출력 키/값 데이터이지만 작성되는 최종 파일은 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 연산자를 사용하는 것 외에 saveAsHadoopDataset 메소드의 소스 코드를 수정하여 나머지 부분을 수정해야 합니다. ParRddFunction 클래스;

                  2. Spark는 로컬 파일을 쓸 때 .crc 파일을 생성하지만 HDFS에 쓸 때는 생성하지 않습니다.

추천

출처blog.csdn.net/m0_64640191/article/details/129726358