[Flink] Select bounded-source split assignment by estimated size - #39874
[Flink] Select bounded-source split assignment by estimated size#39874pkuzmickas wants to merge 3 commits into
Conversation
f362fb0 to
f110427
Compare
|
Run Java PreCommit |
|
Assigning reviewers: R: @shunping for label website. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
|
Hey @Abacn thank you for the super helpful review of the other PR. I'm working on testing your proposed improvements. Could you also please review this when you get a chance? 🙏 |
Abacn
left a comment
There was a problem hiding this comment.
Thanks! Since it's tested working the approach sounds good to me. Had a few initial comments mainly on code base simplifications.
| } | ||
| long estimatedBytesPerReader = estimatedSizeBytes / sourceParallelism; | ||
| long thresholdBytes = FlinkSourceSplitUtils.mebibytesToBytes(thresholdMb); | ||
| FlinkSourceSplitAssignmentMode selectedMode = |
There was a problem hiding this comment.
Reader may return estimatedBytes as 0 if it does not support estimation. This is common in both Beam builtin and user IO Connectors. To make the option truely opt-in I understand the condition should read (thresholdBytes <=0 || estimatedBytesPerReader >= thresholdBytes)
| + "and lazy assignment for sources at or above it. Any negative value always uses " | ||
| + "static assignment.") | ||
| @Default.Long(0) | ||
| Long getLazySourceSplitAssignmentMinSizeMbPerReader(); |
There was a problem hiding this comment.
A few comments
-
We can leave it opt-in for now, however ideally the default option should be good enough for most common use cases, provide a reasonably performant outcome. I understand now it's expected to set this to some positive values to benefit from it.
-
The naming is pretty long. In addition, I don't see "lazySplit" as a term anywhere in documentation. Not sure how it ended up with this naming.
Consider "getSourceStaticSplitThresholdMb" and just note (1) positive sizes below this threshold uses static split (2) it's per reader in the description string (3) note in description this is for DataStream mode only.
There was a problem hiding this comment.
Makes sense, using the name you suggested and modified the description 👍
We can leave it opt-in for now, however ideally the default option should be good enough for most common use cases, provide a reasonably performant outcome
We tested several thresholds against different production workflows, but did not find one that performed well enough across them to make a good general default unfortunately at this time :(
| private static final int SOURCE_PARALLELISM = 2; | ||
| private static final int REQUESTED_SPLITS = 4; | ||
|
|
||
| @Test |
There was a problem hiding this comment.
Thanks for adding thorough test coverage! However, adding 600+ lines of test code introduces maintenance overhead and duplication. We can cut ~300+ lines while keeping 100% of the effective test coverage by simplifying:
- Consolidate threshold decision tests: testSmallBoundedSource..., testLargeBoundedSource..., testUnknownEstimates..., testDefaultConfiguration..., testEmptyBoundedSource..., and testConfigurationCanForce... all test selectAssignmentMode. These 6 methods can be collapsed into a single parameterized or table-driven test using assertAssignmentMode.
- Deduplicate restore tests: testStaticRestoreReturnsPendingSplitsToOriginalOwnersAtSameParallelism is redundant with testRestoreRepartitionsStaticSplitsForNewParallelismWithoutEstimating (rescale already covers the same-parallelism case).
- Remove out-of-scope test & mock: testStaticAssignmentRespectsFileInputSplitMaxSize and TestFileBasedSource test fileInputSplitMaxSizeMB, which is an existing option not introduced by this PR.
- Deduplicate early reader registration tests: testSignalsNoMoreSplitsToEarlyReaderWithoutAssignment and testSizeBasedSelectionAssignsStaticSplitsToEarlyReaders test the same registration race condition.
- Trim custom mock classes: Removing TestFileBasedSource and simplifying TestEstimatedSizeBoundedSource (e.g. dropping failEstimation/createFailing) to eliminate mock boilerplate.
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.beam.runners.flink.translation.wrappers.streaming.io.source; |
There was a problem hiding this comment.
THis thin enum can be put as an internal class of FlinkSourceEnumeratorState
| import java.util.List; | ||
|
|
||
| /** Checkpoint state shared by the lazy and static source split assignment strategies. */ | ||
| public final class FlinkSourceEnumeratorState<T> implements Serializable { |
There was a problem hiding this comment.
Since there is a dedicate FlinkSourceEnumeratorStateSerializer, why still need implements java.io.Serializable? If it's needed it suggests the Serializer isn't effective in some serialization paths
There was a problem hiding this comment.
FlinkSourceEnumeratorStateSerializer handles checkpoint-format versioning and upgrades the previous map-based state. For the new version, the checkpoint data (the assignment mode and list of pending splits) still uses the existing Java object serialization, so FlinkSourceEnumeratorState needs to implement Serializable, as the previous state did. I avoided adding field-by-field encoding to the new serializer for simplicity. Please let me know if you had another approach in mind.
| import org.apache.beam.sdk.options.PipelineOptions; | ||
|
|
||
| /** Shared Beam source sizing and splitting helpers. */ | ||
| final class FlinkSourceSplitUtils { |
There was a problem hiding this comment.
It's only used by FlinkSourceSplitEnumerator. Consider just put helper methods there.
In general XxxUtils classes are discouraged for specific helper methods (admittedly it exists everywhere in code base)
There was a problem hiding this comment.
These helpers are also used by LazyFlinkSourceSplitEnumerator and SizeBasedFlinkSourceSplitEnumerator. Moving them into FlinkSourceSplitEnumerator would make the lazy and size based implementations depend on the static enumerator for shared source splitting logic. I kept them in a neutral helper for that reason, but I’m happy to use a different approach if you prefer.
pkuzmickas
left a comment
There was a problem hiding this comment.
Made most of the changes you asked for with a few comments. Thanks!! 🙏
| import java.util.List; | ||
|
|
||
| /** Checkpoint state shared by the lazy and static source split assignment strategies. */ | ||
| public final class FlinkSourceEnumeratorState<T> implements Serializable { |
There was a problem hiding this comment.
FlinkSourceEnumeratorStateSerializer handles checkpoint-format versioning and upgrades the previous map-based state. For the new version, the checkpoint data (the assignment mode and list of pending splits) still uses the existing Java object serialization, so FlinkSourceEnumeratorState needs to implement Serializable, as the previous state did. I avoided adding field-by-field encoding to the new serializer for simplicity. Please let me know if you had another approach in mind.
| import org.apache.beam.sdk.options.PipelineOptions; | ||
|
|
||
| /** Shared Beam source sizing and splitting helpers. */ | ||
| final class FlinkSourceSplitUtils { |
There was a problem hiding this comment.
These helpers are also used by LazyFlinkSourceSplitEnumerator and SizeBasedFlinkSourceSplitEnumerator. Moving them into FlinkSourceSplitEnumerator would make the lazy and size based implementations depend on the static enumerator for shared source splitting logic. I kept them in a neutral helper for that reason, but I’m happy to use a different approach if you prefer.
| } | ||
| long estimatedBytesPerReader = estimatedSizeBytes / sourceParallelism; | ||
| long thresholdBytes = FlinkSourceSplitUtils.mebibytesToBytes(thresholdMb); | ||
| FlinkSourceSplitAssignmentMode selectedMode = |
| + "and lazy assignment for sources at or above it. Any negative value always uses " | ||
| + "static assignment.") | ||
| @Default.Long(0) | ||
| Long getLazySourceSplitAssignmentMinSizeMbPerReader(); |
There was a problem hiding this comment.
Makes sense, using the name you suggested and modified the description 👍
We can leave it opt-in for now, however ideally the default option should be good enough for most common use cases, provide a reasonably performant outcome
We tested several thresholds against different production workflows, but did not find one that performed well enough across them to make a good general default unfortunately at this time :(
Summary
Add an opt-in, size-based split-assignment strategy for bounded sources in the Flink DataStream runner.
The default remains lazy pull-based assignment. When users configure a positive threshold, the runner assigns sources estimated below that size per reader statically and keeps larger sources lazy. A negative value forces static assignment.
Fixes #39873.
Why
Moving production workloads from the Flink 1 DataSet runner to the Flink 2 DataStream runner caused large performance regressions for sources that emit inexpensive descriptors for expensive downstream work.
The DataStream runner currently sends all bounded sources to the lazy enumerator. That enumerator gives the next split to whichever reader requests it. This balances sources whose splits contain expensive I/O, but fast-starting readers can claim most descriptors before their peers start. A pointwise downstream edge then preserves the skew during the expensive work.
Static assignment fixes descriptor sources but can slow direct file readers, where dynamic work sharing compensates for different file sizes. The option therefore remains disabled by default and lets each pipeline choose a threshold.
Performance
These anonymized production measurements used matching input partitions and isolated outputs.
The measured descriptor and file-reader examples were approximately 4 GiB and 8 GiB per reader, respectively. This supports a threshold near 6 GiB for those pipelines, but estimated bytes are only a proxy for split cost; Beam does not select a global threshold.
Configuration
lazySourceSplitAssignmentMinSizeMbPerReader(Python:lazy_source_split_assignment_min_size_mb_per_reader) controls bounded-source assignment:0(default): always use the existing lazy assignment.Implementation
Compatibility
Existing pipelines keep lazy bounded-source assignment because the option defaults to
0. Existing checkpoints retain their previous assignment behavior. Estimation failures continue to fail the job, matching the current runner.Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:
CHANGES.mdwith noteworthy changes.See the Contributor Guide for more tips on making the review process smoother.