gudladona opened a new pull request, #20533:
URL: https://github.com/apache/hudi/pull/20533

   ### Describe the issue this Pull Request addresses
   
   Closes #20489
   
   Clustering builds one RDD per clustering group and unions them into a single 
write stage. Spark serializes the whole stage lineage into one task binary that 
every task deserializes. Several objects that do not depend on the group were 
built once per group, so each one was shipped to, deserialized by and held by 
every task, once per group. The task binary and executor memory grew with 
groups × concurrently running tasks.
   
   ### Summary and Changelog
   
   Objects that do not depend on the group are built once per clustering run 
and shared by every group. Java serialization writes a shared instance once per 
stream, so each one now costs one copy per task, whatever the plan size. This 
covers items (1)–(5) of #20489. Item (6), writing the group's 
`RDD<InternalRow>` without the DataFrame round-trip, will follow in a separate 
PR, as suggested in the review.
   
   - `MultipleSparkJobExecutionStrategy`
     - Resolves the table schema with meta fields once in `performClustering`, 
instead of once per group. That was also one commit-metadata read on the driver 
per group.
     - Builds the `ReaderContextFactory` and the reader properties once. Each 
`SparkReaderContextFactory` creates three broadcasts, so this also removes 
three broadcasts per group. The non-row-writer path shares its factory and 
reader schema the same way.
     - Adds `getGroupWriteConfig(Option<Long> parquetMaxFileSize)`: one write 
config per run, re-derived if the strategy's write config is replaced 
(`SparkBinaryCopyClusteringExecutionStrategy` replaces it before clustering).
     - Adds `getGroupWriterSchema(...)`: one bulk-insert writer schema per run.
   - `SparkSortAndSizeExecutionStrategy`, 
`SparkSingleFileSortExecutionStrategy`, 
`SparkConsistentBucketClusteringExecutionStrategy`
     - Use the shared write config and writer schema.
     - `withBulkInsertParallelism(numOutputGroups)` is dropped, not moved after 
`withProps`. It was overwritten by `withProps`, and nothing on the clustering 
write paths reads `BULKINSERT_PARALLELISM_VALUE`: both paths pass 
`numOutputGroups` explicitly. Making it effective would only make configs 
differ per group.
   - `HoodieDatasetBulkInsertHelper`
     - New `writerSchema(datasetSchema, writeConfig)` and a `bulkInsert` 
overload that takes the writer schema. The existing `bulkInsert` delegates to 
it unchanged.
   - `SQLConfInjectingRDD`
     - RDDs that capture equal SQL configs share one configs map, keyed by 
content, so a changed conf never gets a stale map.
   - Tests
     - `TestClusteringTaskBinaryPayloads` (new) runs the real 
`SparkSortAndSizeExecutionStrategy` on a 2-group and an 8-group subset of one 
plan. It serializes each write-stage RDD the way the closure serializer does, 
and asserts that group-independent payloads do not grow with the number of 
groups. It fails on master and passes with this change.
     - `TestSQLConfInjectingRDD` (new) checks that the configs map is shared, 
deduplicated in one task stream, never stale after a conf change, and still 
applied to tasks.
   
   `TestClusteringTaskBinaryPayloads`, before → after (2 groups → 8 groups):
   
   | Payload | master | this PR |
   |---|---|---|
   | `HoodieSchema` | 3 → 9 | 2 → 2 |
   | `HoodieWriteConfig` | 3 → 9 | 2 → 2 |
   | `TypedProperties` | 45 → 129 | 31 → 31 |
   | `ReaderContextFactory` | 2 → 8 | 1 → 1 |
   | `StructType` | 6 → 24 | 3 → 3 |
   | `SQLConfInjectingRDD` configs maps | 2 → 8 | 1 → 1 |
   | Task binary growth per extra group | +73,013 B | +10,523 B |
   
   The remaining growth per group is the group's own lineage: its RDDs, 
partitions and file slices, plus the per-group `RDDScanExec` that (6) addresses.
   
   ### Impact
   
   Performance: the write-stage task binary, task deserialization CPU and 
executor heap no longer grow with the number of clustering groups for these 
payloads. On a production table (Hudi 1.1.1, 4,000-group plans, 4 write tasks 
per executor), the same changes in our fork took the task binary from 287.8 MiB 
to 7.6 MiB and the executor live heap from 6.9–7.5 GB to 0.6–1.2 GB. The GC 
share of task time fell from 18.9% to 1.4%. That fork also includes (6).
   
   API: `runClusteringForGroupAsync` and `runClusteringForGroupAsyncAsRow` 
(protected, in `MultipleSparkJobExecutionStrategy`) take the shared state as 
extra parameters. Custom execution strategies that call or override them need 
to pass it through. No in-repo subclass overrides them. Public APIs and configs 
are unchanged.
   
   ### Risk Level
   
   Low. The change shares immutable or read-only instances that were previously 
copied per group:
   - `HoodieSchema` and `StructType` are immutable.
   - The write config is not mutated after it is built; nothing on the 
bulk-insert paths calls `setValue` on it.
   - Each task still deserializes its own copy, so nothing is shared across 
tasks at runtime.
   
   Verified locally:
   - Spark 3.5 / Java 11: `TestClusteringTaskBinaryPayloads`, 
`TestSQLConfInjectingRDD`, `TestSparkSortAndSizeClustering`, 
`TestSparkConsistentBucketClustering`, 
`TestHoodieSparkMergeOnReadTableClustering` and 
`TestSparkBinaryCopyClusteringAndValidationMeta` pass. On my machine, a network 
proxy breaks calls to the embedded timeline server, so the suites that use it 
were run with it disabled and direct markers. That is the same setting 
`TestClusteringTaskBinaryPayloads` uses.
   - Spark 4.2 / Java 17 / Scala 2.13: builds, and 
`TestClusteringTaskBinaryPayloads` and `TestSQLConfInjectingRDD` pass.
   
   ### Documentation Update
   
   none
   
   ### Contributor's checklist
   
   - [x] Read through [contributor's 
guide](https://hudi.apache.org/contribute/how-to-contribute)
   - [x] Enough context is provided in the sections above
   - [x] Adequate tests were added if applicable
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to