Skip to content

Commit c4d81ee

Browse files
committed
Improve sharding for bounded pcolleciton, to better control concurrent connections. this will keep elements for same destination close to each other and shard them. For single table write it's same behaviour, for dynamic destination it will improve reduce amount of connections used
1 parent 3d68e9d commit c4d81ee

1 file changed

Lines changed: 56 additions & 1 deletion

File tree

  • sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiLoads.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.beam.sdk.coders.Coder;
3131
import org.apache.beam.sdk.coders.KvCoder;
3232
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition;
33+
import org.apache.beam.sdk.options.StreamingOptions;
3334
import org.apache.beam.sdk.schemas.NoSuchSchemaException;
3435
import org.apache.beam.sdk.transforms.DoFn;
3536
import org.apache.beam.sdk.transforms.Flatten;
@@ -38,6 +39,7 @@
3839
import org.apache.beam.sdk.transforms.ParDo;
3940
import org.apache.beam.sdk.transforms.Redistribute;
4041
import org.apache.beam.sdk.transforms.SerializableFunction;
42+
import org.apache.beam.sdk.transforms.Values;
4143
import org.apache.beam.sdk.transforms.errorhandling.BadRecord;
4244
import org.apache.beam.sdk.transforms.errorhandling.BadRecordRouter;
4345
import org.apache.beam.sdk.transforms.errorhandling.BadRecordRouter.ThrowingBadRecordRouter;
@@ -379,12 +381,19 @@ public WriteResult expandUntriggered(
379381
PCollection<KV<DestinationT, StorageApiWritePayload>> successfulConvertedRows =
380382
convertMessagesResult.get(successfulConvertedRowsTag);
381383

382-
if (numShards > 0) {
384+
boolean streaming = input.getPipeline().getOptions().as(StreamingOptions.class).isStreaming();
385+
if (numShards > 0 && streaming) {
383386
successfulConvertedRows =
384387
successfulConvertedRows.apply(
385388
"ResdistibuteNumShards",
386389
Redistribute.<KV<DestinationT, StorageApiWritePayload>>arbitrarily()
387390
.withNumBuckets(numShards));
391+
} else if (numShards > 0 && !streaming) {
392+
successfulConvertedRows =
393+
successfulConvertedRows
394+
.apply("AddKeyWithSideInputs", ParDo.of(new AddShardKeyFn<>(numShards)))
395+
.apply("RedistributeNumShards", Redistribute.byKey())
396+
.apply("Remove shard", Values.create());
388397
}
389398

390399
PCollectionTuple writeRecordsResult =
@@ -457,6 +466,52 @@ private void addErrorCollections(
457466
}
458467
}
459468

469+
/**
470+
* A {@link DoFn} that applies a composite sharding key to incoming records to optimize BigQuery
471+
* Storage API throughput.
472+
*
473+
* <p>This transform manages the balance between connection count (resource overhead) and
474+
* processing parallelism by distributing data across {@code numShards} buckets:
475+
*
476+
* <ul>
477+
* <li><b>Data Affinity:</b> By using a composite key {@code KV<DestT, Integer>}, this transform
478+
* ensures that all records for a specific destination (table) are grouped into specific
479+
* shard buckets. This allows downstream transforms to maintain stable {@code
480+
* StreamConnection} sessions for each destination, minimizing connection thrashing.
481+
* <li><b>Parallel Throughput:</b> By appending a pseudo-random integer shard index, this
482+
* transform allows the runner to distribute the records for a single destination across up
483+
* to {@code numShards} parallel streams, parallelizing the write throughput of "hot"
484+
* (high-volume) destinations.
485+
* <li><b>Concurrency Scaling:</b> The {@code numShards} parameter acts as the parallelism
486+
* multiplier per destination. The total potential concurrency across the pipeline is {@code
487+
* numShards * total_destinations}, allowing users to scale write throughput by increasing
488+
* {@code numShards} for bottlenecked tables.
489+
* </ul>
490+
*
491+
* <p>The output structure is {@code KV<KV<DestT, Integer>, KV<DestT, Payload>>}. Downstream,
492+
* {@link Redistribute#byKey()} uses this composite key to partition the data, ensuring the runner
493+
* effectively balances load while respecting the per-destination parallelism limits configured
494+
* here.
495+
*/
496+
private static class AddShardKeyFn<DestT, ElemT>
497+
extends DoFn<
498+
KV<DestT, StorageApiWritePayload>,
499+
KV<KV<DestT, Integer>, KV<DestT, StorageApiWritePayload>>> {
500+
private final int shardBound;
501+
502+
public AddShardKeyFn(int numShards) {
503+
this.shardBound = Math.max(1, numShards);
504+
}
505+
506+
@ProcessElement
507+
public void processElement(
508+
@Element KV<DestT, StorageApiWritePayload> element,
509+
OutputReceiver<KV<KV<DestT, Integer>, KV<DestT, StorageApiWritePayload>>> outputReceiver) {
510+
int shard = ThreadLocalRandom.current().nextInt(shardBound);
511+
outputReceiver.output(KV.of(KV.of(element.getKey(), shard), element));
512+
}
513+
}
514+
460515
private static class ConvertInsertErrorToBadRecord
461516
extends DoFn<BigQueryStorageApiInsertError, BadRecord> {
462517

0 commit comments

Comments
 (0)