Skip to content

Flink source split strategy - #40433

Draft
jto wants to merge 11 commits into
apache:masterfrom
jto:flink-source-split-strategy
Draft

jto wants to merge 11 commits into
apache:masterfrom
jto:flink-source-split-strategy

Conversation

@jto

@jto jto commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Improves how the Flink runner splits bounded sources in batch pipelines, so that work is evenly distributed over the source readers. The same logic is shared by the DataSet (SourceInputFormat) and DataStream (FlinkSource enumerators) translations, in a new BoundedSourceSplitter.

Motivation

The runner used to call BoundedSource.split(estimatedSize / parallelism) once and use whatever came back. Beam does not require sources to split in one pass, and many don't honour the desired size: for example a BigQuery export returns one split per exported file, whatever size is requested. The result was often too few splits for the readers, or splits of very uneven size. Combined with lazy split assignment, a few readers could end up with much more work than the others.

What changes

After the initial split() call, the splitter refines the splits in a few bounded passes:

  1. Re-split oversized splits. Splits much larger than estimatedSize / (2 x parallelism) are split again, so each reader gets at least two splits, and sources returning splits far larger than requested (e.g. BigQuery exports) are cut down.
  2. Balance sizes. If split sizes still vary a lot (coefficient of variation above 0.5), the larger splits are re-split towards the mean size, in up to three rounds.
  3. Balance the split count. If round-robin assignment would give the busiest reader more than 10% above the average bytes, the largest splits are halved.
  4. Order for round-robin. Splits are sorted by size in a "snake" order, so that assigning split i to reader i % parallelism pairs large splits with small ones.

Steps 2 to 4 need per-split size estimates and are skipped when they are not available. Re-splitting never goes below min(64 MiB, estimatedSize / (2 x parallelism)): small splits mostly add fixed per-split overhead, but small inputs, which may feed transforms that generate a lot more data, are still spread over all readers. fileInputSplitMaxSizeMB is still honoured as a cap.

The DataStream enumerators now pass the actual reader count (currentParallelism()) to the splitter, separately from numSplits (which may be maxParallelism), so ordering and balancing match how splits are actually assigned.

Split assignment: static by default in batch

  • sourceStaticSplitThresholdMb no longer has a default. When unset, batch pipelines use static assignment and streaming pipelines keep lazy assignment. Explicit values behave as before: 0 = lazy, negative = static, positive = size-based.
  • DataSet pipelines get a new StaticInputSplitAssigner (split i goes to task i % parallelism, like the DataStream enumerator), and fall back to Flink's DefaultInputSplitAssigner when lazy assignment is configured.
  • The lazy enumerator hands out the largest splits first, in constant time per assignment.

This is a behaviour change for batch pipelines that relied on the previous lazy default. They can restore it with --sourceStaticSplitThresholdMb=0.

Observability

Each step logs its effect at INFO, in human-readable units: split count, size min/mean/max and coefficient of variation, and the busiest reader's load relative to the average. For example:

Re-split 0 of 374596 splits larger than 192.00 MiB towards 128.00 MiB: ...
Split count balancing for 3500 readers: skipped, 374596 splits already give the busiest reader 1.003 times the average bytes
Ordered 374596 splits for round-robin over 3500 readers: busiest reader load / average went from 1.020 to 1.003

Results

On a production-sized batch job (45 TiB of Avro, parallelism 3500, DataStream translation), comparing the source stage before and after this change, per subtask:

records out (max / mean) records out CV duration (max / mean) duration CV
before 1.38 0.142 1.27 0.133
after 1.00 0.001 1.13 0.036

On a BigQuery export read (714 files for 3750 readers), the export files are now re-split into 7854 splits, so all readers get work, with the busiest reader at 1.017x the average bytes. Before, about 80% of the readers had nothing to read.

Trade-offs

  • The refinement calls getEstimatedSizeBytes() on every split, and split() again on some of them. For file-based sources this is in-memory work once the file pattern has been expanded, but it may be slower for sources whose size estimates are remote calls.
  • Static assignment cannot adapt to readers that are slower for reasons other than data volume (node, network, storage). Lazy assignment remains available for that.

Testing

  • New BoundedSourceSplitterTest: re-splitting of oversized splits, size balancing, split count balancing by bytes per reader, the minimum split size, small inputs, round-robin ordering for the actual reader count, unsplittable sources, assignment mode resolution and size formatting.
  • SourceInputFormatTest: static assignment by default and lazy assignment when requested.
  • FlinkSourceSplitEnumeratorTest updated for the new default.

Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md

GitHub Actions Tests Status (on master branch)

Build python source distribution and wheels
Python tests
Java tests
Go tests

See CI.md for more information about GitHub Actions CI or the workflows README for a list of workflows and how to trigger them.

jto and others added 11 commits October 2, 2026 16:21
Share bounded source splitting between the DataSet and DataStream
translations in BoundedSourceSplitter. After the initial split, the
splitter re-splits when there are fewer splits than readers, re-splits
oversized splits when split sizes vary a lot, rounds the split count up
to a multiple of the parallelism, and orders splits so round-robin
assignment balances readers. The file input max split size is preserved.

Batch pipelines now use static split assignment by default, in both
DataSet (new StaticInputSplitAssigner) and DataStream. Lazy assignment
is used when explicitly requested via sourceStaticSplitThresholdMb, and
stays the default for streaming pipelines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the count-based re-split (uniform factor per split) with a
single pass that re-splits only the splits much larger than the desired
split size, as some sources (e.g. BigQuery) return splits way larger
than requested. Share the per-split re-split logic with size balancing
and express balancing without loops and breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite roundToMultipleOfParallelism without break/continue, using a
priority queue and a halve() helper. Stop halving once the split count
is within 10% of the parallelism of a multiple of it, since re-splitting
introduces some skew anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DataStream translation passes maxParallelism as numSplits when it is
set, while the static enumerator assigns split i to reader
i % currentParallelism. Pass the enumerator's current parallelism to the
splitter separately, so rounding and round-robin ordering match the
actual assignment. numSplits still drives the desired split size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Instead of accepting split counts within 10% of the parallelism of a
multiple of it, halve splits until round-robin assignment gives the
busiest reader at most 10% more splits than the average. This rejects
counts like 1.1x the parallelism (busiest reader does ~1.8x the
average) while accepting any count once readers get many splits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lazy enumerator hands out the largest splits first by taking them
from the front of its pending splits. Use an ArrayDeque instead of
ArrayList.remove(0), which made assigning n splits O(n^2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixed cost of opening and reading a split dominates for small
splits, and byte ranges smaller than a file's blocks may read nothing.
Re-splitting of oversized splits, size balancing and halving no longer
create splits smaller than min(64 MiB, estimatedSize / parallelism), so
small inputs, which may feed transforms generating a lot more data, are
still spread over all readers. The initial split is not affected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With static round-robin assignment, a single split per reader passes
any size difference straight to the slowest reader, while two splits
per reader let the ordering pair large splits with small ones.

Re-split splits larger than estimatedSize / (2 x parallelism), and lower
the minimum split size to min(64 MiB, estimatedSize / (2 x parallelism))
so small inputs also get two splits per reader. The initial split
request is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Log each re-split, size balancing round, split count balancing and
round-robin ordering with the split count, size min/mean/max and
coefficient of variation, and the busiest reader's load relative to the
average.

Also document that fileInputSplitMaxSizeMB is rarely needed now that
bounded sources are re-split by default to even out uneven file sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An uneven split count is not a problem when split sizes differ: the
round-robin ordering gives the readers with more splits the smaller
ones. Skip halving when the ordered splits already give the busiest
reader at most 10% more bytes than the average, instead of judging by
split count. On a real BigQuery export read, this avoids thousands of
pointless halve attempts reporting a misleading 1.43 count imbalance
for a 1.017 byte imbalance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Format sizes in the splitter logs with binary units and two decimals
(e.g. 1.71 MiB, 12.51 GiB) instead of raw byte counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant