[ 
https://issues.apache.org/jira/browse/SPARK-58830?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

wanghaihua updated SPARK-58830:
-------------------------------
    Description: 
h3. Problem

{{MapStatus}} is one of the most frequently accessed metadata structures 
throughout the shuffle lifecycle: every mapper produces one, the driver retains 
one for each map task, and every reducer consults it to determine which shuffle 
blocks to fetch.

If a {{MapStatus}} instance—specifically its {{partitionLengths}} or compressed 
representation—is corrupted in memory or in transit between the mapper and 
reducer, a partition containing data on disk may incorrectly appear to have a 
size of zero. Possible causes include transient hardware faults, JVM or memory 
corruption, and serialization bugs.

Shuffle block fetchers intentionally skip zero-length blocks when:

{{MapStatus.getSizeForBlock(reduceId) == 0}}

Consequently, the reducer silently skips the affected block. The valid on-disk 
shuffle data is never read and is effectively lost, with no exception, failed 
stage, or log signal anywhere in the pipeline.

We observed this behavior in production. A job produced fewer output rows than 
an otherwise equivalent rerun, despite there being no errors in the driver or 
executor logs. Comparing the Spark UI SQL metrics from the two runs revealed a 
significant discrepancy between shuffle “records written” and “records read” at 
an {{Exchange}} node in the affected run.

!shuffle-datalost.png|width=685,height=397!

After instrumenting {{MapStatus}} at multiple points along the mapper → driver 
→ reducer path, we found that several non-empty partition entries had changed 
to zero, while the corresponding shuffle data files on disk remained complete 
and correct. The root cause was traced to a hardware fault that silently 
corrupted a {{MapStatus}} object held in the driver’s memory.
h3. Related Existing Safeguards
 * *SPARK-35276* added checksums for shuffle data files, allowing Spark to 
detect disk- or I/O-level corruption when a reducer reads the shuffle bytes. 
However, when the corruption affects metadata rather than data—for example, 
when {{MapStatus}} incorrectly reports a non-empty partition as empty—the 
reducer never attempts to read the block, so the data-checksum path is not 
exercised.
 * *SPARK-40872* handles a related but distinct push-based shuffle scenario in 
which a merged chunk is empty because it was produced by a faulty merge node. 
It recovers by falling back to the original blocks, but it does not cover 
non-empty-to-empty corruption in a mapper’s own {{MapStatus}} outside 
push-based merging.
 * *SPARK-57491* addresses a scenario where two attempts for the same 
partition—such as speculative attempts with non-deterministic shuffle keys—both 
complete their pushes, but only one attempt’s {{MapStatus}} survives. It does 
not address in-memory or in-transit corruption of an individual 
{{{}MapStatus{}}}.

None of these safeguards detects a {{MapStatus}} that was correct when produced 
by the mapper but became corrupted before being consumed by a reducer.
h3. Proposed Solution

Add an integrity checksum to {{{}MapStatus{}}}, computed over the {*}set of 
non-empty partition indices{*}, rather than over the partition sizes themselves.

Partition sizes are already represented using lossy compression or estimation 
in {{CompressedMapStatus}} and {{{}HighlyCompressedMapStatus{}}}. Their exact 
encoded values may therefore legitimately differ while remaining correct. For 
this failure mode, the important invariant is whether each partition is empty 
or non-empty.

The checksum would travel with {{MapStatus}} throughout its normal lifecycle, 
including Java serialization, and would be verified whenever the metadata is 
consumed to plan shuffle block fetches.

A checksum mismatch would indicate that the metadata was corrupted in transit 
or at rest. Spark could then throw a {{{}FetchFailedException{}}}, reusing the 
existing fetch-failure and stage-retry recovery path instead of silently 
producing incomplete results.
h3. Design Notes
 * Store the checksum as an {{{}Option[Int]{}}}, gated by a new internal 
configuration that is disabled by default, allowing the feature to be enabled 
selectively.
 * {{None}} naturally represents both “checksum verification disabled” and “no 
checksum present,” without reserving a sentinel value in the 32-bit checksum 
space. A computed checksum remains valid even when there are no non-empty 
partitions.
 * Make the checksum algorithm configurable, initially supporting {{CRC32}} and 
{{{}Adler32{}}}.
 * Perform verification where {{MapStatus}} is consumed to plan block fetches.
 * On mismatch, throw {{FetchFailedException}} and reuse Spark’s existing 
map-output fetch-failure handling rather than introducing a new recovery 
mechanism.
 * Verification requires {{O(N)}} time per {{{}MapStatus{}}}, where {{N}} is 
the number of reduce partitions, plus a small constant number of bytes per 
{{MapStatus}} for the optional checksum.

We have a working prototype that includes:
 * A new {{MapStatusChecksum}} utility.
 * A {{nonEmptyChecksum: Option[Int]}} field on {{{}MapStatus{}}}.
 * Serialization support for the checksum.
 * Checksum verification integrated into {{{}MapOutputTracker{}}}.

We would like to contribute this implementation upstream.

  was:
h3. Problem

{{MapStatus}} is one of the most frequently accessed metadata structures 
throughout the shuffle lifecycle: every mapper produces one, the driver retains 
one for each map task, and every reducer consults it to determine which shuffle 
blocks to fetch.

If a {{MapStatus}} instance—specifically its {{partitionLengths}} or compressed 
representation—is corrupted in memory or in transit between the mapper and 
reducer, a partition containing data on disk may incorrectly appear to have a 
size of zero. Possible causes include transient hardware faults, JVM or memory 
corruption, and serialization bugs.

Shuffle block fetchers intentionally skip zero-length blocks when:

{{MapStatus.getSizeForBlock(reduceId) == 0}}

Consequently, the reducer silently skips the affected block. The valid on-disk 
shuffle data is never read and is effectively lost, with no exception, failed 
stage, or log signal anywhere in the pipeline.

We observed this behavior in production. A job produced fewer output rows than 
an otherwise equivalent rerun, despite there being no errors in the driver or 
executor logs. Comparing the Spark UI SQL metrics from the two runs revealed a 
significant discrepancy between shuffle “records written” and “records read” at 
an {{Exchange}} node in the affected run.

After instrumenting {{MapStatus}} at multiple points along the mapper → driver 
→ reducer path, we found that several non-empty partition entries had changed 
to zero, while the corresponding shuffle data files on disk remained complete 
and correct. The root cause was traced to a hardware fault that silently 
corrupted a {{MapStatus}} object held in the driver’s memory.
h3. Related Existing Safeguards
 * *SPARK-35276* added checksums for shuffle data files, allowing Spark to 
detect disk- or I/O-level corruption when a reducer reads the shuffle bytes. 
However, when the corruption affects metadata rather than data—for example, 
when {{MapStatus}} incorrectly reports a non-empty partition as empty—the 
reducer never attempts to read the block, so the data-checksum path is not 
exercised.
 * *SPARK-40872* handles a related but distinct push-based shuffle scenario in 
which a merged chunk is empty because it was produced by a faulty merge node. 
It recovers by falling back to the original blocks, but it does not cover 
non-empty-to-empty corruption in a mapper’s own {{MapStatus}} outside 
push-based merging.
 * *SPARK-57491* addresses a scenario where two attempts for the same 
partition—such as speculative attempts with non-deterministic shuffle keys—both 
complete their pushes, but only one attempt’s {{MapStatus}} survives. It does 
not address in-memory or in-transit corruption of an individual 
{{{}MapStatus{}}}.

None of these safeguards detects a {{MapStatus}} that was correct when produced 
by the mapper but became corrupted before being consumed by a reducer.
h3. Proposed Solution

Add an integrity checksum to {{{}MapStatus{}}}, computed over the {*}set of 
non-empty partition indices{*}, rather than over the partition sizes themselves.

Partition sizes are already represented using lossy compression or estimation 
in {{CompressedMapStatus}} and {{{}HighlyCompressedMapStatus{}}}. Their exact 
encoded values may therefore legitimately differ while remaining correct. For 
this failure mode, the important invariant is whether each partition is empty 
or non-empty.

The checksum would travel with {{MapStatus}} throughout its normal lifecycle, 
including Java serialization, and would be verified whenever the metadata is 
consumed to plan shuffle block fetches.

A checksum mismatch would indicate that the metadata was corrupted in transit 
or at rest. Spark could then throw a {{{}FetchFailedException{}}}, reusing the 
existing fetch-failure and stage-retry recovery path instead of silently 
producing incomplete results.
h3. Design Notes
 * Store the checksum as an {{{}Option[Int]{}}}, gated by a new internal 
configuration that is disabled by default, allowing the feature to be enabled 
selectively.
 * {{None}} naturally represents both “checksum verification disabled” and “no 
checksum present,” without reserving a sentinel value in the 32-bit checksum 
space. A computed checksum remains valid even when there are no non-empty 
partitions.
 * Make the checksum algorithm configurable, initially supporting {{CRC32}} and 
{{{}Adler32{}}}.
 * Perform verification where {{MapStatus}} is consumed to plan block fetches.
 * On mismatch, throw {{FetchFailedException}} and reuse Spark’s existing 
map-output fetch-failure handling rather than introducing a new recovery 
mechanism.
 * Verification requires {{O(N)}} time per {{{}MapStatus{}}}, where {{N}} is 
the number of reduce partitions, plus a small constant number of bytes per 
{{MapStatus}} for the optional checksum.

We have a working prototype that includes:
 * A new {{MapStatusChecksum}} utility.
 * A {{nonEmptyChecksum: Option[Int]}} field on {{{}MapStatus{}}}.
 * Serialization support for the checksum.
 * Checksum verification integrated into {{{}MapOutputTracker{}}}.

We would like to contribute this implementation upstream.


> Shuffle can silently drop data when MapStatus metadata is corrupted between 
> mapper and reducer
> ----------------------------------------------------------------------------------------------
>
>                 Key: SPARK-58830
>                 URL: https://issues.apache.org/jira/browse/SPARK-58830
>             Project: Spark
>          Issue Type: Improvement
>          Components: Spark Core
>    Affects Versions: 3.4.4
>            Reporter: wanghaihua
>            Priority: Major
>         Attachments: shuffle-datalost.png
>
>
> h3. Problem
> {{MapStatus}} is one of the most frequently accessed metadata structures 
> throughout the shuffle lifecycle: every mapper produces one, the driver 
> retains one for each map task, and every reducer consults it to determine 
> which shuffle blocks to fetch.
> If a {{MapStatus}} instance—specifically its {{partitionLengths}} or 
> compressed representation—is corrupted in memory or in transit between the 
> mapper and reducer, a partition containing data on disk may incorrectly 
> appear to have a size of zero. Possible causes include transient hardware 
> faults, JVM or memory corruption, and serialization bugs.
> Shuffle block fetchers intentionally skip zero-length blocks when:
> {{MapStatus.getSizeForBlock(reduceId) == 0}}
> Consequently, the reducer silently skips the affected block. The valid 
> on-disk shuffle data is never read and is effectively lost, with no 
> exception, failed stage, or log signal anywhere in the pipeline.
> We observed this behavior in production. A job produced fewer output rows 
> than an otherwise equivalent rerun, despite there being no errors in the 
> driver or executor logs. Comparing the Spark UI SQL metrics from the two runs 
> revealed a significant discrepancy between shuffle “records written” and 
> “records read” at an {{Exchange}} node in the affected run.
> !shuffle-datalost.png|width=685,height=397!
> After instrumenting {{MapStatus}} at multiple points along the mapper → 
> driver → reducer path, we found that several non-empty partition entries had 
> changed to zero, while the corresponding shuffle data files on disk remained 
> complete and correct. The root cause was traced to a hardware fault that 
> silently corrupted a {{MapStatus}} object held in the driver’s memory.
> h3. Related Existing Safeguards
>  * *SPARK-35276* added checksums for shuffle data files, allowing Spark to 
> detect disk- or I/O-level corruption when a reducer reads the shuffle bytes. 
> However, when the corruption affects metadata rather than data—for example, 
> when {{MapStatus}} incorrectly reports a non-empty partition as empty—the 
> reducer never attempts to read the block, so the data-checksum path is not 
> exercised.
>  * *SPARK-40872* handles a related but distinct push-based shuffle scenario 
> in which a merged chunk is empty because it was produced by a faulty merge 
> node. It recovers by falling back to the original blocks, but it does not 
> cover non-empty-to-empty corruption in a mapper’s own {{MapStatus}} outside 
> push-based merging.
>  * *SPARK-57491* addresses a scenario where two attempts for the same 
> partition—such as speculative attempts with non-deterministic shuffle 
> keys—both complete their pushes, but only one attempt’s {{MapStatus}} 
> survives. It does not address in-memory or in-transit corruption of an 
> individual {{{}MapStatus{}}}.
> None of these safeguards detects a {{MapStatus}} that was correct when 
> produced by the mapper but became corrupted before being consumed by a 
> reducer.
> h3. Proposed Solution
> Add an integrity checksum to {{{}MapStatus{}}}, computed over the {*}set of 
> non-empty partition indices{*}, rather than over the partition sizes 
> themselves.
> Partition sizes are already represented using lossy compression or estimation 
> in {{CompressedMapStatus}} and {{{}HighlyCompressedMapStatus{}}}. Their exact 
> encoded values may therefore legitimately differ while remaining correct. For 
> this failure mode, the important invariant is whether each partition is empty 
> or non-empty.
> The checksum would travel with {{MapStatus}} throughout its normal lifecycle, 
> including Java serialization, and would be verified whenever the metadata is 
> consumed to plan shuffle block fetches.
> A checksum mismatch would indicate that the metadata was corrupted in transit 
> or at rest. Spark could then throw a {{{}FetchFailedException{}}}, reusing 
> the existing fetch-failure and stage-retry recovery path instead of silently 
> producing incomplete results.
> h3. Design Notes
>  * Store the checksum as an {{{}Option[Int]{}}}, gated by a new internal 
> configuration that is disabled by default, allowing the feature to be enabled 
> selectively.
>  * {{None}} naturally represents both “checksum verification disabled” and 
> “no checksum present,” without reserving a sentinel value in the 32-bit 
> checksum space. A computed checksum remains valid even when there are no 
> non-empty partitions.
>  * Make the checksum algorithm configurable, initially supporting {{CRC32}} 
> and {{{}Adler32{}}}.
>  * Perform verification where {{MapStatus}} is consumed to plan block fetches.
>  * On mismatch, throw {{FetchFailedException}} and reuse Spark’s existing 
> map-output fetch-failure handling rather than introducing a new recovery 
> mechanism.
>  * Verification requires {{O(N)}} time per {{{}MapStatus{}}}, where {{N}} is 
> the number of reduce partitions, plus a small constant number of bytes per 
> {{MapStatus}} for the optional checksum.
> We have a working prototype that includes:
>  * A new {{MapStatusChecksum}} utility.
>  * A {{nonEmptyChecksum: Option[Int]}} field on {{{}MapStatus{}}}.
>  * Serialization support for the checksum.
>  * Checksum verification integrated into {{{}MapOutputTracker{}}}.
> We would like to contribute this implementation upstream.



--
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