James Willis created SPARK-58750:
------------------------------------

             Summary:  RDD checkpoint to S3A fails unrecoverably under 
speculation/task retry
                 Key: SPARK-58750
                 URL: https://issues.apache.org/jira/browse/SPARK-58750
             Project: Spark
          Issue Type: Bug
          Components: Spark Core
    Affects Versions: 4.0.3, 4.1.2, 4.1.1, 4.2.0, 3.5.8, 4.0.2, 3.5.7, 4.0.1, 
4.0.0, 3.5.6, 4.1.0, 3.5.5, 3.5.4, 3.5.3, 3.4.4, 3.4.3, 3.5.2, 3.3.4, 3.5.1, 
3.5.0, 3.4.1, 3.4.0, 3.3.2, 3.4.2, 3.3.3, 3.2.4, 3.2.3, 3.3.1, 3.2.2, 3.3.0, 
3.2.1, 3.2.0, 3.5.9, 4.2.1, 3.5.10
         Environment: I produced this error in Spark 4.0.1 on Kubernetes, 
Hadoop 3.4.1 (S3A), with {{spark.speculation=true}}
            Reporter: James Willis


h2. Summary

{{ReliableCheckpointRDD.writePartitionToCheckpointFile}} assumes HDFS rename 
semantics: it handles {{fs.rename()}} returning {{false}} when the destination 
exists, but does not handle the rename {*}throwing{*}. Since HADOOP-16721 
(Hadoop 3.3.1), S3A deliberately raises {{FileAlreadyExistsException}} when the 
rename destination is an existing file, instead of returning {{{}false{}}}. 
ABFS behaves the same way.

Under speculative execution, two attempts of the same checkpoint task race to 
rename their attempt-temp file onto the same final part file. On HDFS the loser 
gets {{rename() == false}} and Spark correctly treats it as "some other copy of 
this task must've finished before us". On S3A the loser gets an unhandled 
{{{}FileAlreadyExistsException{}}}, which fails the task even though the data 
is already written. Because the destination file now permanently exists, 
{*}every subsequent retry of that task fails on the same rename{*}, so 
{{spark.task.maxFailures}} is always exhausted and the job aborts.

Every Spark release since the default Hadoop dependency moved to 3.3.1+ (Spark 
3.2.0) is affected when checkpointing to S3A or ABFS with speculation enabled. 
The code is unchanged in master.
h2. Observed failure (production, Spark 4.0.1 / Hadoop 3.4.1)
{noformat}
  org.apache.hadoop.fs.FileAlreadyExistsException: Failed to rename
  
s3://<bucket>/<prefix>/spark-checkpoints/44151dc0-c680-446d-8556-f6aef47d73dd/rdd-207/.part-00379-attempt-25364
  to 
s3://<bucket>/<prefix>/spark-checkpoints/44151dc0-c680-446d-8556-f6aef47d73dd/rdd-207/part-00379;
  destination file exists
      at 
org.apache.hadoop.fs.s3a.S3AFileSystem.initiateRename(S3AFileSystem.java:2468)
      at 
org.apache.hadoop.fs.s3a.S3AFileSystem.innerRename(S3AFileSystem.java:2533)
      at 
org.apache.hadoop.fs.s3a.S3AFileSystem.lambda$rename$6(S3AFileSystem.java:2394)
      ...
      at org.apache.hadoop.fs.s3a.S3AFileSystem.rename(S3AFileSystem.java:2392)
      at 
org.apache.spark.rdd.ReliableCheckpointRDD$.writePartitionToCheckpointFile(ReliableCheckpointRDD.scala:229)
      at 
org.apache.spark.rdd.ReliableCheckpointRDD$.$anonfun$writeRDDToCheckpointDirectory$1(ReliableCheckpointRDD.scala:168)
      at org.apache.spark.scheduler.Task.run(Task.scala:147)
      at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:650)
  {noformat}
followed by:
{noformat}
  ERROR TaskSetManager: Task 379 in stage 105.0 failed 4 times; aborting job
  {noformat}
Speculative kills were visible throughout the run; task 379.0 was still running 
when attempt 379.4 hit the already-committed part file.
h2. Root cause

{{writePartitionToCheckpointFile}} (master):
{code:scala}
  if (!fs.rename(tempOutputPath, finalOutputPath)) {
    if (!fs.exists(finalOutputPath)) {
      logInfo(...)
      fs.delete(tempOutputPath, false)
      throw SparkCoreErrors.checkpointFailedToSaveError(ctx.attemptNumber(), 
finalOutputPath)
    } else {
      // Some other copy of this task must've finished before us and renamed it
      ...
    }
  }
  {code}
The dest-exists race is only handled when {{rename()}} reports it by returning 
{{{}false{}}}. The Hadoop FileSystem specification does not guarantee that; 
HADOOP-16721 explicitly changed S3A "away from consistency with HDFS to one 
where applications find out what is wrong", raising 
{{FileAlreadyExistsException}} for a file destination. Structured Streaming's 
{{CheckpointFileManager}} was hardened for divergent rename semantics (e.g. 
SPARK-40826); the RDD checkpoint writer was not.
h2. Reproduction
 # Configure an S3A (or ABFS) checkpoint dir, Hadoop >= 3.3.1, 
{{spark.speculation=true}} (aggressive settings help:
{{{}spark.speculation.multiplier=1.0{}}}, 
{{{}spark.speculation.quantile=0.1{}}})
 # {{sc.setCheckpointDir("s3a://bucket/checkpoints")}}
 # Checkpoint an RDD with skewed/slow partitions so speculative attempts 
launch, e.g. a mapPartitions that sleeps on a subset of partitions; call 
{{rdd.checkpoint()}} then an action
 # A speculative attempt losing the rename race fails the task; all retries of 
that task then fail deterministically and the job aborts

Also reproducible without speculation when a task fails *after* its rename 
commits and the task is retried (e.g. executor lost during post-write 
bookkeeping): the retry finds the destination and dies the same way.
h2. Proposed fix

Fold the exception into the existing false-return path, which already re-checks 
the destination defensively:
{code:scala}
  val renamed = try {
    fs.rename(tempOutputPath, finalOutputPath)
  } catch {
    case _: FileAlreadyExistsException => false
  }
  if (!renamed) {
    // existing logic: if finalOutputPath exists, another attempt committed — 
clean up temp and
    // succeed; otherwise delete temp and throw checkpointFailedToSaveError
  }
  {code}
Semantics are unchanged relative to HDFS: whichever attempt commits the part 
file wins, exactly as the winner-takes-all rename behaves today on filesystems 
that return {{{}false{}}}. I can submit a PR with this change and a test.
h2. Workaround

None good: speculation cannot be disabled per-job, so users must disable it 
application-wide, checkpoint to HDFS-semantics storage, or replace 
{{checkpoint()}} with a manual write/read-back through a file-source write 
(which goes through the output commit coordinator and is speculation-safe).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to