-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-8029] Robust shuffle writer #9610
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
b2a90c6
9f0d2f9
55485a9
6deccff
f0c2a5d
d0b937f
35bd469
71b12bf
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 |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| import java.io.FileInputStream; | ||
| import java.io.FileOutputStream; | ||
| import java.io.IOException; | ||
| import java.util.UUID; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| import scala.None$; | ||
|
|
@@ -155,9 +156,20 @@ public void write(Iterator<Product2<K, V>> records) throws IOException { | |
| writer.commitAndClose(); | ||
| } | ||
|
|
||
| partitionLengths = | ||
| writePartitionedFile(shuffleBlockResolver.getDataFile(shuffleId, mapId)); | ||
| shuffleBlockResolver.writeIndexFile(shuffleId, mapId, partitionLengths); | ||
| File output = shuffleBlockResolver.getDataFile(shuffleId, mapId); | ||
| final File tmp = new File(output.getAbsolutePath() + "." + UUID.randomUUID()); | ||
| partitionLengths = writePartitionedFile(tmp); | ||
| if (!output.exists()) { | ||
|
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. I dont' think you can do this and still support SPARK-4085 -- regenerating the output if one of the shuffle files goes completely missing. Because if the index file goes missing, and the data file is still there, with this logic you'll always never regenerate the shuffle output. But maybe SPARK-4085 is not worth it ...
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. Good point, we should check both. |
||
| shuffleBlockResolver.writeIndexFile(shuffleId, mapId, partitionLengths); | ||
| if (output.exists()) { | ||
| output.delete(); | ||
| } | ||
| if (!tmp.renameTo(output)) { | ||
| throw new IOException("fail to rename data file " + tmp + " to " + output); | ||
| } | ||
| } else { | ||
| tmp.delete(); | ||
| } | ||
| mapStatus = MapStatus$.MODULE$.apply(blockManager.shuffleServerId(), partitionLengths); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
|
|
||
| package org.apache.spark.shuffle | ||
|
|
||
| import java.io.File | ||
| import java.util.UUID | ||
| import java.util.concurrent.ConcurrentLinkedQueue | ||
|
|
||
| import scala.collection.JavaConverters._ | ||
|
|
@@ -84,17 +86,8 @@ private[spark] class FileShuffleBlockResolver(conf: SparkConf) | |
| Array.tabulate[DiskBlockObjectWriter](numReducers) { bucketId => | ||
| val blockId = ShuffleBlockId(shuffleId, mapId, bucketId) | ||
| val blockFile = blockManager.diskBlockManager.getFile(blockId) | ||
| // Because of previous failures, the shuffle file may already exist on this machine. | ||
| // If so, remove it. | ||
| if (blockFile.exists) { | ||
| if (blockFile.delete()) { | ||
| logInfo(s"Removed existing shuffle file $blockFile") | ||
| } else { | ||
| logWarning(s"Failed to remove existing shuffle file $blockFile") | ||
| } | ||
| } | ||
| blockManager.getDiskWriter(blockId, blockFile, serializerInstance, bufferSize, | ||
| writeMetrics) | ||
| val tmp = new File(blockFile.getAbsolutePath + "." + UUID.randomUUID()) | ||
|
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. why not use your new method |
||
| blockManager.getDiskWriter(blockId, tmp, serializerInstance, bufferSize, writeMetrics) | ||
| } | ||
| } | ||
| // Creating the file to write to and creating a disk writer both involve interacting with | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| package org.apache.spark.shuffle | ||
|
|
||
| import java.io._ | ||
| import java.util.UUID | ||
|
|
||
| import com.google.common.io.ByteStreams | ||
|
|
||
|
|
@@ -81,7 +82,8 @@ private[spark] class IndexShuffleBlockResolver(conf: SparkConf) extends ShuffleB | |
| * */ | ||
| def writeIndexFile(shuffleId: Int, mapId: Int, lengths: Array[Long]): Unit = { | ||
| val indexFile = getIndexFile(shuffleId, mapId) | ||
| val out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(indexFile))) | ||
| val tmp = new File(indexFile.getAbsolutePath + "." + UUID.randomUUID()) | ||
| val out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))) | ||
| Utils.tryWithSafeFinally { | ||
| // We take in lengths of each block, need to convert it to offsets. | ||
| var offset = 0L | ||
|
|
@@ -93,6 +95,10 @@ private[spark] class IndexShuffleBlockResolver(conf: SparkConf) extends ShuffleB | |
| } { | ||
| out.close() | ||
| } | ||
| indexFile.deleteOnExit() | ||
| if (!tmp.renameTo(indexFile)) { | ||
| throw new IOException(s"fail to rename index file $tmp to $indexFile") | ||
|
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. this will just kill the task, right? both tasks are actually just fine, and in fact the overall job should continue if one of them succeeds. But instead this will lead to the task getting retried, and potentially continuing to fail up to 4 times, though its actually finished successfully from another taskset? You could handle this in scheduler, but that would add some complexity.
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. There is very little chance that the two concurrent task will call
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. Can you test for this? I think the worry was about different TaskSets attempting the same map stage. Imagine that attempt 1 of the stage successfully completes a task, and sends back a map output status, but that status gets ignored because that stage attempt got cancelled. Attempt 2 might then fail to send a new status for it. There seem to be two ways to fix it if this problem can actually occur -- either add MapOutputStatuses even from failed task sets or mark this new task as successful if a file exists.
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. On Thu, Nov 12, 2015 at 8:30 AM, Matei Zaharia notifications@github.com
|
||
| } | ||
| } | ||
|
|
||
| override def getBlockData(blockId: ShuffleBlockId): ManagedBuffer = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
|
|
||
| package org.apache.spark.shuffle.hash | ||
|
|
||
| import java.io.IOException | ||
|
|
||
| import org.apache.spark._ | ||
| import org.apache.spark.executor.ShuffleWriteMetrics | ||
| import org.apache.spark.scheduler.MapStatus | ||
|
|
@@ -106,6 +108,19 @@ private[spark] class HashShuffleWriter[K, V]( | |
| writer.commitAndClose() | ||
| writer.fileSegment().length | ||
| } | ||
| // rename all shuffle files to final paths | ||
| shuffle.writers.zip(sizes).foreach { case (writer: DiskBlockObjectWriter, size: Long) => | ||
| if (size > 0) { | ||
| val output = blockManager.diskBlockManager.getFile(writer.blockId) | ||
| if (output.exists()) { | ||
| writer.file.delete() | ||
| } else { | ||
| if (!writer.file.renameTo(output)) { | ||
| throw new IOException(s"fail to rename ${writer.file} to $output") | ||
|
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. same problem here on partially existing shuffle output. Also, the
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. I think these partitions are independent, they should be OK whenever it's generated in different attempt, or that's the basic idea of how RDD works (could be re-run and got the same result). If not, for example, the items in RDD is random are generated randomly, then it also does not matter if it's different across attempts.
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. The
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. yeah I suppose it all depends on what the model is for non-deterministic data. The reduce tasks can read data from a mix attempts, but I guess that is OK (we can't completely prevent it in any case). There is also the problem of returning the right mapstatus here, but it doesn't matter as much in this case -- you will at least return some set of non-empty blocks that is consistent with the shuffle data on disk, even if the sizes can be arbitrarily wrong. Also I know its super-rare, but there is a race between I also find it a weird that this is neither first or last attempt wins -- the first attempt to get to each output file wins, but it can be a mix of attempts. again I'd include a comment explaining the logic |
||
| } | ||
| } | ||
| } | ||
| } | ||
| MapStatus(blockManager.shuffleServerId, sizes) | ||
| } | ||
|
|
||
|
|
||
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.
good point about creating the tmp files in the same dir as the dest to make sure we can do the rename ... I had taken that for granted.