|
30 | 30 | import org.apache.beam.sdk.coders.Coder; |
31 | 31 | import org.apache.beam.sdk.coders.KvCoder; |
32 | 32 | import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition; |
| 33 | +import org.apache.beam.sdk.options.StreamingOptions; |
33 | 34 | import org.apache.beam.sdk.schemas.NoSuchSchemaException; |
34 | 35 | import org.apache.beam.sdk.transforms.DoFn; |
35 | 36 | import org.apache.beam.sdk.transforms.Flatten; |
|
38 | 39 | import org.apache.beam.sdk.transforms.ParDo; |
39 | 40 | import org.apache.beam.sdk.transforms.Redistribute; |
40 | 41 | import org.apache.beam.sdk.transforms.SerializableFunction; |
| 42 | +import org.apache.beam.sdk.transforms.Values; |
41 | 43 | import org.apache.beam.sdk.transforms.errorhandling.BadRecord; |
42 | 44 | import org.apache.beam.sdk.transforms.errorhandling.BadRecordRouter; |
43 | 45 | import org.apache.beam.sdk.transforms.errorhandling.BadRecordRouter.ThrowingBadRecordRouter; |
@@ -379,12 +381,19 @@ public WriteResult expandUntriggered( |
379 | 381 | PCollection<KV<DestinationT, StorageApiWritePayload>> successfulConvertedRows = |
380 | 382 | convertMessagesResult.get(successfulConvertedRowsTag); |
381 | 383 |
|
382 | | - if (numShards > 0) { |
| 384 | + boolean streaming = input.getPipeline().getOptions().as(StreamingOptions.class).isStreaming(); |
| 385 | + if (numShards > 0 && streaming) { |
383 | 386 | successfulConvertedRows = |
384 | 387 | successfulConvertedRows.apply( |
385 | 388 | "ResdistibuteNumShards", |
386 | 389 | Redistribute.<KV<DestinationT, StorageApiWritePayload>>arbitrarily() |
387 | 390 | .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()); |
388 | 397 | } |
389 | 398 |
|
390 | 399 | PCollectionTuple writeRecordsResult = |
@@ -457,6 +466,52 @@ private void addErrorCollections( |
457 | 466 | } |
458 | 467 | } |
459 | 468 |
|
| 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 | + |
460 | 515 | private static class ConvertInsertErrorToBadRecord |
461 | 516 | extends DoFn<BigQueryStorageApiInsertError, BadRecord> { |
462 | 517 |
|
|
0 commit comments