I. 概要
この記事では、ソース コードから開始し、Spark が Hadoop のいくつかの OutputFormat を呼び出してファイル出力を実現する方法について説明します。ここでは、saveAsTextFile ( path )、saveAsHadoopFile ( path ) など、作業で一般的に使用されるいくつかの演算子について説明します。
2. Sparkのソースコード分析
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.RDD関数のペアリング
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 の 2 番目のメソッドを入力し、次に 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 関数: 具体的にファイルを書き込む方法です。まず、各パーティションで 1 つのファイルのみが生成され、設定した OutputFormat で使用される RecordWriter の write メソッドを呼び出してファイルを書き込むことがわかります。自分で書きたい 書き込み内容を定義するには、RecordWriter クラスをカスタマイズする必要があります。
3、TextOutputFormat と MultipleTextOutputFormat
1.テキスト出力形式
このクラスは直接コピーして、必要に応じてコードを少し変更できます。次に、このクラスを書き直す方法を確認するための要件を見ていきます。
①ファイルのエンコード形式がUTF-8以外のエンコード形式である、または改行文字が「\n」ではない: この2つは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));
}
}
四、MultipleTextOutputFormat
これは、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 演算子を使用することに加えて、残りの部分を、ペアRddFunctionクラス。
2. Spark は、ローカル ファイルを書き込むときに .crc ファイルを生成しますが、HDFS に書き込むときは生成しません。