-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-18191][CORE] Port RDD API to use commit protocol #15769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a0426c8
e017e1e
5e12850
4e72745
eb74e59
09d5ed9
e5a60ff
86f1951
cfcd823
243b8ba
9380f91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark | ||
|
|
||
| import java.util.Date | ||
|
|
||
| import org.apache.hadoop.conf.Configuration | ||
| import org.apache.hadoop.mapreduce._ | ||
| import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.internal.io.HadoopMapReduceCommitProtocol | ||
| import org.apache.spark.util.SerializableConfiguration | ||
|
|
||
| /** | ||
| * Internal helper class that saves an RDD using a Hadoop OutputFormat | ||
| * (from the newer mapreduce API, not the old mapred API). | ||
| * | ||
| * Saves the RDD using a JobConf, which should contain an output key class, an output value class, | ||
| * a filename to write to, etc, exactly like in a Hadoop MapReduce job. | ||
| * | ||
| * Use a [[HadoopMapReduceCommitProtocol]] to handle output commit, which, unlike Hadoop's | ||
| * OutputCommitter, is serializable. | ||
| */ | ||
| private[spark] | ||
| class SparkNewHadoopWriter( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move this into internal/io |
||
| jobConf: Configuration, | ||
| committer: HadoopMapReduceCommitProtocol) extends Logging with Serializable { | ||
|
|
||
| private val now = new Date() | ||
| private val conf = new SerializableConfiguration(jobConf) | ||
|
|
||
| private val jobtrackerID = SparkHadoopWriter.createJobTrackerID(new Date()) | ||
| private var jobId = 0 | ||
| private var splitId = 0 | ||
| private var attemptId = 0 | ||
|
|
||
| @transient private var writer: RecordWriter[AnyRef, AnyRef] = null | ||
| @transient private var jobContext: JobContext = null | ||
| @transient private var taskContext: TaskAttemptContext = null | ||
|
|
||
| def setupJob(): Unit = { | ||
| // Committer setup a job | ||
| committer.setupJob(getJobContext) | ||
| } | ||
|
|
||
| def setupTask(context: TaskContext): Unit = { | ||
| // Set jobID/taskID | ||
| jobId = context.stageId | ||
| splitId = context.partitionId | ||
| attemptId = (context.taskAttemptId % Int.MaxValue).toInt | ||
| // Committer setup a task | ||
| committer.setupTask(getTaskContext(context)) | ||
| } | ||
|
|
||
| def write(context: TaskContext, key: AnyRef, value: AnyRef): Unit = { | ||
| getWriter(context).write(key, value) | ||
| } | ||
|
|
||
| def abortTask(context: TaskContext): Unit = { | ||
| // Close writer | ||
| getWriter(context).close(getTaskContext(context)) | ||
| // Committer abort a task | ||
| committer.abortTask(getTaskContext(context)) | ||
| } | ||
|
|
||
| def commitTask(context: TaskContext): Unit = { | ||
| // Close writer | ||
| getWriter(context).close(getTaskContext(context)) | ||
| // Committer commit a task | ||
| committer.commitTask(getTaskContext(context)) | ||
| } | ||
|
|
||
| def abortJob(): Unit = { | ||
| committer.abortJob(getJobContext) | ||
| } | ||
|
|
||
| def commitJob() { | ||
| committer.commitJob(getJobContext, Seq.empty) | ||
| } | ||
|
|
||
| // ********* Private Functions ********* | ||
|
|
||
| /* | ||
| * Generate jobContext. Since jobContext is transient, it may be null after serialization. | ||
| */ | ||
| private def getJobContext(): JobContext = { | ||
| if (jobContext == null) { | ||
| val jobAttemptId = new TaskAttemptID(jobtrackerID, jobId, TaskType.MAP, 0, 0) | ||
| jobContext = new TaskAttemptContextImpl(conf.value, jobAttemptId) | ||
| } | ||
| jobContext | ||
| } | ||
|
|
||
| /* | ||
| * Generate taskContext. Since taskContext is transient, it may be null after serialization. | ||
| */ | ||
| private def getTaskContext(context: TaskContext): TaskAttemptContext = { | ||
| if (taskContext == null) { | ||
| val attemptId = new TaskAttemptID(jobtrackerID, jobId, TaskType.REDUCE, splitId, | ||
| context.attemptNumber) | ||
| taskContext = new TaskAttemptContextImpl(conf.value, attemptId) | ||
| } | ||
| taskContext | ||
| } | ||
|
|
||
| /* | ||
| * Generate writer. Since writer is transient, it may be null after serialization. | ||
| */ | ||
| private def getWriter(context: TaskContext): RecordWriter[AnyRef, AnyRef] = { | ||
| if (writer == null) { | ||
| val format = getJobContext.getOutputFormatClass.newInstance | ||
| writer = format.getRecordWriter(getTaskContext(context)) | ||
| .asInstanceOf[RecordWriter[AnyRef, AnyRef]] | ||
| } | ||
| writer | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,19 +21,21 @@ import java.nio.ByteBuffer | |
| import java.text.SimpleDateFormat | ||
| import java.util.{Date, HashMap => JHashMap, Locale} | ||
|
|
||
| import org.apache.spark.internal.io.{HadoopMapReduceCommitProtocol, FileCommitProtocol} | ||
|
|
||
| import scala.collection.{mutable, Map} | ||
| import scala.collection.JavaConverters._ | ||
| import scala.collection.mutable.ArrayBuffer | ||
| import scala.reflect.ClassTag | ||
| import scala.util.DynamicVariable | ||
|
|
||
| import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus | ||
| import org.apache.hadoop.conf.{Configurable, Configuration} | ||
| import org.apache.hadoop.conf.Configuration | ||
| import org.apache.hadoop.fs.FileSystem | ||
| import org.apache.hadoop.io.SequenceFile.CompressionType | ||
| import org.apache.hadoop.io.compress.CompressionCodec | ||
| import org.apache.hadoop.mapred.{FileOutputCommitter, FileOutputFormat, JobConf, OutputFormat} | ||
| import org.apache.hadoop.mapreduce.{Job => NewAPIHadoopJob, OutputFormat => NewOutputFormat, RecordWriter => NewRecordWriter, TaskAttemptID, TaskType} | ||
| import org.apache.hadoop.mapreduce.{Job => NewAPIHadoopJob, OutputFormat => NewOutputFormat, TaskAttemptID, TaskType} | ||
| import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl | ||
|
|
||
| import org.apache.spark._ | ||
|
|
@@ -1092,37 +1094,38 @@ class PairRDDFunctions[K, V](self: RDD[(K, V)]) | |
| jobFormat.checkOutputSpecs(job) | ||
| } | ||
|
|
||
| // Instantiate writer | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you think you can move most of the logic from saveAsNewAPIHadoopDataset into SparkNewHadoopWriter? It'd be similar to how FileFormatWriter works, but much simpler because there is no dynamic partition insert. |
||
| val committer = FileCommitProtocol.instantiate( | ||
| className = classOf[HadoopMapReduceCommitProtocol].getName, | ||
| jobId = stageId.toString, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this how we determine the old job id? i thought it had some date in it too
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact I'm not sure what value should be assigned to |
||
| outputPath = jobConfiguration.get("mapred.output.dir"), | ||
| isAppend = false | ||
| ).asInstanceOf[HadoopMapReduceCommitProtocol] | ||
| val writer = new SparkNewHadoopWriter(hadoopConf, committer) | ||
|
|
||
| val writeShard = (context: TaskContext, iter: Iterator[(K, V)]) => { | ||
| val config = wrappedConf.value | ||
| /* "reduce task" <split #> <attempt # = spark task #> */ | ||
| val attemptId = new TaskAttemptID(jobtrackerID, stageId, TaskType.REDUCE, context.partitionId, | ||
| context.attemptNumber) | ||
| val hadoopContext = new TaskAttemptContextImpl(config, attemptId) | ||
| val format = outfmt.newInstance | ||
| format match { | ||
| case c: Configurable => c.setConf(config) | ||
| case _ => () | ||
| } | ||
| val committer = format.getOutputCommitter(hadoopContext) | ||
| committer.setupTask(hadoopContext) | ||
| writer.setupTask(context) | ||
|
|
||
| val outputMetricsAndBytesWrittenCallback: Option[(OutputMetrics, () => Long)] = | ||
| initHadoopOutputMetrics(context) | ||
|
|
||
| val writer = format.getRecordWriter(hadoopContext).asInstanceOf[NewRecordWriter[K, V]] | ||
| require(writer != null, "Unable to obtain RecordWriter") | ||
| var recordsWritten = 0L | ||
| Utils.tryWithSafeFinallyAndFailureCallbacks { | ||
| while (iter.hasNext) { | ||
| val pair = iter.next() | ||
| writer.write(pair._1, pair._2) | ||
| writer.write(context, pair._1.asInstanceOf[AnyRef], pair._2.asInstanceOf[AnyRef]) | ||
|
|
||
| // Update bytes written metric every few records | ||
| maybeUpdateOutputMetrics(outputMetricsAndBytesWrittenCallback, recordsWritten) | ||
| recordsWritten += 1 | ||
| } | ||
| }(finallyBlock = writer.close(hadoopContext)) | ||
| committer.commitTask(hadoopContext) | ||
|
|
||
| writer.commitTask(context) | ||
| }(catchBlock = { | ||
| writer.abortTask(context) | ||
| writer.abortJob() | ||
| }) | ||
| outputMetricsAndBytesWrittenCallback.foreach { case (om, callback) => | ||
| om.setBytesWritten(callback()) | ||
| om.setRecordsWritten(recordsWritten) | ||
|
|
@@ -1147,9 +1150,9 @@ class PairRDDFunctions[K, V](self: RDD[(K, V)]) | |
| logWarning(warningMessage) | ||
| } | ||
|
|
||
| jobCommitter.setupJob(jobTaskContext) | ||
| writer.setupJob() | ||
| self.context.runJob(self, writeShard) | ||
| jobCommitter.commitJob(jobTaskContext) | ||
| writer.commitJob() | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -509,21 +509,6 @@ class PairRDDFunctionsSuite extends SparkFunSuite with SharedSparkContext { | |
| (2, ArrayBuffer(1)))) | ||
| } | ||
|
|
||
| test("saveNewAPIHadoopFile should call setConf if format is configurable") { | ||
| val pairs = sc.parallelize(Array((new Integer(1), new Integer(1)))) | ||
|
|
||
| // No error, non-configurable formats still work | ||
| pairs.saveAsNewAPIHadoopFile[NewFakeFormat]("ignored") | ||
|
|
||
| /* | ||
| Check that configurable formats get configured: | ||
| ConfigTestFormat throws an exception if we try to write | ||
| to it when setConf hasn't been called first. | ||
| Assertion is in ConfigTestFormat.getRecordWriter. | ||
| */ | ||
| pairs.saveAsNewAPIHadoopFile[ConfigTestFormat]("ignored") | ||
| } | ||
|
|
||
| test("saveAsHadoopFile should respect configured output committers") { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic is no longer needed. |
||
| val pairs = sc.parallelize(Array((new Integer(1), new Integer(1)))) | ||
| val conf = new JobConf() | ||
|
|
@@ -725,8 +710,7 @@ class PairRDDFunctionsSuite extends SparkFunSuite with SharedSparkContext { | |
| } | ||
|
|
||
| /* | ||
| These classes are fakes for testing | ||
| "saveNewAPIHadoopFile should call setConf if format is configurable". | ||
| These classes are fakes for testing saveAsHadoopFile/saveNewAPIHadoopFile. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment have been out-of-date for a while. |
||
| Unfortunately, they have to be top level classes, and not defined in | ||
| the test method, because otherwise Scala won't generate no-args constructors | ||
| and the test will therefore throw InstantiationException when saveAsNewAPIHadoopFile | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to generate jobTrackerID seprately in
SparkNewHadoopWriter