Skip to content

Commit 42fb798

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

1 file changed

Lines changed: 60 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: 60 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,56 @@ 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+
* (along with GBK downstream) ensures that records for a specific sharded destination
479+
* (table, shard) are grouped together. This allows downstream transforms to maintain stable
480+
* {@code StreamConnection} sessions for each destination.
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 control:</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}.
488+
* </ul>
489+
*
490+
* <p>The output structure is {@code KV<KV<DestT, Integer>, KV<DestT, Payload>>}. Downstream,
491+
* {@link Redistribute#byKey()} uses this composite key to partition the data, ensuring the runner
492+
* effectively balances load while respecting the per-destination parallelism limits configured
493+
* here.
494+
*/
495+
private static class AddShardKeyFn<DestT, ElemT>
496+
extends DoFn<
497+
KV<DestT, StorageApiWritePayload>,
498+
KV<KV<DestT, Integer>, KV<DestT, StorageApiWritePayload>>> {
499+
private final int numShards;
500+
private int shardNumber = 0;
501+
502+
public AddShardKeyFn(int numShards) {
503+
this.numShards = Math.max(1, numShards);
504+
}
505+
506+
@Setup
507+
public void setup() {
508+
shardNumber = ThreadLocalRandom.current().nextInt(numShards);
509+
}
510+
511+
@ProcessElement
512+
public void processElement(
513+
@Element KV<DestT, StorageApiWritePayload> element,
514+
OutputReceiver<KV<KV<DestT, Integer>, KV<DestT, StorageApiWritePayload>>> outputReceiver) {
515+
outputReceiver.output(KV.of(KV.of(element.getKey(), ++shardNumber % numShards), element));
516+
}
517+
}
518+
460519
private static class ConvertInsertErrorToBadRecord
461520
extends DoFn<BigQueryStorageApiInsertError, BadRecord> {
462521

0 commit comments

Comments
 (0)