Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/layouts/shortcodes/generated/pipeline_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
<td>Boolean</td>
<td>Forces Flink to register avro classes in kryo serializer.<br /><br />Important: Make sure to include the flink-avro module. Otherwise, nothing will be registered. For backward compatibility, the default value is empty to conform to the behavior of the older version. That is, always register avro with kryo, and if flink-avro is not in the class path, register a dummy serializer. In Flink-2.0, we will set the default value to true.</td>
</tr>
<tr>
<td><h5>pipeline.forward-edge.parallelism-mismatch-mode</h5></td>
<td style="word-wrap: break-word;">REBALANCE</td>
<td><p>Enum</p></td>
<td>Determines how the runtime handles a FORWARD (pointwise) edge whose producer and consumer parallelism no longer match, which can happen when the parallelism of connected operators is changed independently (for example via the AdaptiveScheduler or <code class="highlighter-rouge">pipeline.jobvertex-parallelism-overrides</code>). A FORWARD edge is only valid at equal parallelism; on a mismatch one of the following strategies is applied:<ul><li><code class="highlighter-rouge">REBALANCE</code>: replace the forward partitioner with a rebalance (round-robin) partitioner. This preserves throughput but reorders records, which is safe for append-only streams but corrupts order-sensitive (changelog) streams.</li><li><code class="highlighter-rouge">FAIL</code>: fail the job with an exception instead of silently changing the data distribution.</li><li><code class="highlighter-rouge">KEEP_FORWARD</code>: keep the forward partitioner. Record order is preserved but the records are funneled to a single consumer subtask, so the remaining consumer subtasks stay idle.</li></ul><br /><br />Possible values:<ul><li>"REBALANCE"</li><li>"FAIL"</li><li>"KEEP_FORWARD"</li></ul></td>
</tr>
<tr>
<td><h5>pipeline.generic-types</h5></td>
<td style="word-wrap: break-word;">true</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,68 @@ public enum VertexDescriptionMode {
CASCADING
}

public static final ConfigOption<ForwardEdgeParallelismMismatchMode>
FORWARD_EDGE_PARALLELISM_MISMATCH_MODE =
key("pipeline.forward-edge.parallelism-mismatch-mode")
.enumType(ForwardEdgeParallelismMismatchMode.class)
.defaultValue(ForwardEdgeParallelismMismatchMode.REBALANCE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC REBALANCE breaks changelog mode - should it be the default?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. REBALANCE stays the default to keep the existing behavior, so jobs relying on it today (e.g. append-only jobs rescaled by the AdaptiveScheduler) aren't affected. Changelog jobs can opt into FAIL or KEEP_FORWARD.

.withDescription(
Description.builder()
.text(
"Determines how the runtime handles a FORWARD (pointwise) edge whose"
+ " producer and consumer parallelism no longer match, which can happen"
+ " when the parallelism of connected operators is changed"
+ " independently (for example via the AdaptiveScheduler or %s). A"
+ " FORWARD edge is only valid at equal parallelism; on a mismatch one"
+ " of the following strategies is applied:",
code(PARALLELISM_OVERRIDES.key()))
.list(
text(
"%s: replace the forward partitioner with a rebalance (round-robin)"
+ " partitioner. This preserves throughput but reorders records,"
+ " which is safe for append-only streams but corrupts"
+ " order-sensitive (changelog) streams.",
code(
ForwardEdgeParallelismMismatchMode
.REBALANCE
.name())),
text(
"%s: fail the job with an exception instead of silently changing"
+ " the data distribution.",
code(
ForwardEdgeParallelismMismatchMode
.FAIL
.name())),
text(
"%s: keep the forward partitioner. Record order is preserved but"
+ " the records are funneled to a single consumer subtask, so the"
+ " remaining consumer subtasks stay idle.",
code(
ForwardEdgeParallelismMismatchMode
.KEEP_FORWARD
.name())))
.build());

/**
* The strategy applied when a FORWARD (pointwise) edge connects a producer and consumer with
* mismatched parallelism.
*/
@PublicEvolving
public enum ForwardEdgeParallelismMismatchMode {
/**
* Replace the forward partitioner with a rebalance partitioner. Reorders records; safe only
* for append-only streams.
*/
REBALANCE,
/** Fail the job with an exception. */
FAIL,
/**
* Keep the forward partitioner. Preserves record order but funnels records to a single
* consumer subtask.
*/
KEEP_FORWARD
}

public static final ConfigOption<Boolean> VERTEX_NAME_INCLUDE_INDEX_PREFIX =
key("pipeline.vertex-name-include-index-prefix")
.booleanType()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.apache.flink.configuration.CheckpointingOptions;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.NettyShuffleEnvironmentOptions;
import org.apache.flink.configuration.PipelineOptions;
import org.apache.flink.configuration.PipelineOptions.ForwardEdgeParallelismMismatchMode;
import org.apache.flink.configuration.TaskManagerOptions;
import org.apache.flink.core.execution.RecoveryClaimMode;
import org.apache.flink.core.fs.AutoCloseableRegistry;
Expand Down Expand Up @@ -2051,18 +2053,60 @@ private static void replaceForwardPartitionerIfConsumerParallelismDoesNotMatch(
// The number of subpartitions isn't the consumer parallelism on a POINTWISE edge, so the
// actual consumer parallelism is passed through the deployment descriptor.
final int consumerParallelism = environment.getWriterConsumerParallelism(outputIndex);
if (streamOutput.getPartitioner() instanceof ForwardPartitioner
if (!(streamOutput.getPartitioner() instanceof ForwardPartitioner)
// An undecided consumer parallelism is not a mismatch.
&& consumerParallelism
!= ResultPartitionDeploymentDescriptor.UNKNOWN_CONSUMER_PARALLELISM
&& consumerParallelism != producerParallelism) {
LOG.debug(
"Replacing forward partitioner with rebalance for {} "
+ "(producer parallelism {} != consumer parallelism {}).",
environment.getTaskInfo().getTaskNameWithSubtasks(),
producerParallelism,
consumerParallelism);
streamOutput.setPartitioner(new RebalancePartitioner<>());
|| consumerParallelism
== ResultPartitionDeploymentDescriptor.UNKNOWN_CONSUMER_PARALLELISM
|| consumerParallelism == producerParallelism) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about

Suggested change
|| consumerParallelism == producerParallelism) {
|| consumerParallelism <= producerParallelism) {

maybe we can keep the forward partitioner in this case as well, since each producer will still forward to 1 consumer?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer each mode to mean exactly one behavior:

  • REBALANCE always rebalances
  • KEEP_FORWARD always keeps forward

Special-casing downscale would make REBALANCE silently behave like KEEP_FORWARD and may bring other risks we haven't thought of. Users who want that can set KEEP_FORWARD.

return;
}

final ForwardEdgeParallelismMismatchMode mode =
environment
.getJobConfiguration()
.get(PipelineOptions.FORWARD_EDGE_PARALLELISM_MISMATCH_MODE);
final String taskNameWithSubtasks = environment.getTaskInfo().getTaskNameWithSubtasks();
switch (mode) {
case REBALANCE:
LOG.warn(
"Replacing forward partitioner with rebalance for {} "
+ "(producer parallelism {} != consumer parallelism {}). This may "
+ "reorder records and produce incorrect results for order-sensitive "
+ "(changelog) streams. Set '{}' to {} or {} to avoid it.",
taskNameWithSubtasks,
producerParallelism,
consumerParallelism,
PipelineOptions.FORWARD_EDGE_PARALLELISM_MISMATCH_MODE.key(),
ForwardEdgeParallelismMismatchMode.FAIL,
ForwardEdgeParallelismMismatchMode.KEEP_FORWARD);
streamOutput.setPartitioner(new RebalancePartitioner<>());
break;
case KEEP_FORWARD:
LOG.warn(
"Keeping forward partitioner for {} despite a parallelism mismatch "
+ "(producer parallelism {} != consumer parallelism {}). Record order "
+ "is preserved but records are funneled to a single consumer subtask, "
+ "leaving the remaining consumer subtasks idle.",
taskNameWithSubtasks,
producerParallelism,
consumerParallelism);
break;
case FAIL:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why do we need the fail mode?
Wouldn't it always be preferred to downgrade performance with warn (KEEP_FORWARD) other than crash?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FAIL is for setups where a parallelism mismatch is never expected, so it's surfaced immediately instead of being handled silently.

throw new FlinkRuntimeException(
String.format(
"Forward partitioning cannot be preserved across a parallelism change "
+ "for %s (producer parallelism %d != consumer parallelism %d). "
+ "Silently downgrading a FORWARD edge to a redistributing "
+ "exchange can reorder records and corrupt order-sensitive "
+ "(changelog) results. Set %s to %s or %s to allow it.",
taskNameWithSubtasks,
producerParallelism,
consumerParallelism,
PipelineOptions.FORWARD_EDGE_PARALLELISM_MISMATCH_MODE.key(),
ForwardEdgeParallelismMismatchMode.REBALANCE,
ForwardEdgeParallelismMismatchMode.KEEP_FORWARD));
default:
throw new IllegalStateException("Unhandled mode: " + mode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.IllegalConfigurationException;
import org.apache.flink.configuration.NettyShuffleEnvironmentOptions;
import org.apache.flink.configuration.PipelineOptions;
import org.apache.flink.configuration.PipelineOptions.ForwardEdgeParallelismMismatchMode;
import org.apache.flink.configuration.ReadableConfig;
import org.apache.flink.core.execution.SavepointFormatType;
import org.apache.flink.core.fs.FSDataInputStream;
Expand Down Expand Up @@ -1893,6 +1895,82 @@ public int getNumberOfSubpartitions() {
}
}

@Test
void testForwardPartitionerFailsOnParallelismChangeWhenModeIsFail() throws Exception {
StreamTaskMailboxTestHarnessBuilder<Integer> builder =
new StreamTaskMailboxTestHarnessBuilder<>(
OneInputStreamTask::new, BasicTypeInfo.INT_TYPE_INFO)
.addInput(BasicTypeInfo.INT_TYPE_INFO)
.addJobConfig(
PipelineOptions.FORWARD_EDGE_PARALLELISM_MISMATCH_MODE,
ForwardEdgeParallelismMismatchMode.FAIL)
.setOutputPartitioner(new ForwardPartitioner<>())
.setupOutputForSingletonOperatorChain(
new TestBoundedOneInputStreamOperator());

try (StreamTaskMailboxTestHarness<Integer> harness = builder.build()) {
// Simulate changed downstream task parallelism (1->2)
List<ResultPartitionWriter> newOutputs = new ArrayList<>();
newOutputs.add(
new MockResultPartitionWriter() {
@Override
public int getNumberOfSubpartitions() {
return 2;
}
});
harness.streamMockEnvironment.setOutputs(newOutputs);
harness.streamMockEnvironment.setWriterConsumerParallelism(2);

// With FAIL mode the job is rejected instead of silently downgrading the edge.
assertThatThrownBy(
() ->
harness.streamTask.createRecordWriterDelegate(
harness.streamTask.configuration,
harness.streamMockEnvironment))
.isInstanceOf(FlinkRuntimeException.class)
.hasMessageContaining("Forward partitioning cannot be preserved");
}
}

@Test
void testForwardPartitionerIsKeptOnParallelismChangeWhenModeIsKeepForward() throws Exception {
StreamTaskMailboxTestHarnessBuilder<Integer> builder =
new StreamTaskMailboxTestHarnessBuilder<>(
OneInputStreamTask::new, BasicTypeInfo.INT_TYPE_INFO)
.addInput(BasicTypeInfo.INT_TYPE_INFO)
.addJobConfig(
PipelineOptions.FORWARD_EDGE_PARALLELISM_MISMATCH_MODE,
ForwardEdgeParallelismMismatchMode.KEEP_FORWARD)
.setOutputPartitioner(new ForwardPartitioner<>())
.setupOutputForSingletonOperatorChain(
new TestBoundedOneInputStreamOperator());

try (StreamTaskMailboxTestHarness<Integer> harness = builder.build()) {
// Simulate changed downstream task parallelism (1->2)
List<ResultPartitionWriter> newOutputs = new ArrayList<>();
newOutputs.add(
new MockResultPartitionWriter() {
@Override
public int getNumberOfSubpartitions() {
return 2;
}
});
harness.streamMockEnvironment.setOutputs(newOutputs);
harness.streamMockEnvironment.setWriterConsumerParallelism(2);

RecordWriterDelegate<SerializationDelegate<StreamRecord<Object>>> recordWriterDelegate =
harness.streamTask.createRecordWriterDelegate(
harness.streamTask.configuration, harness.streamMockEnvironment);
// With KEEP_FORWARD mode the forward partitioner is preserved despite the mismatch.
assertThat(
((ChannelSelectorRecordWriter)
((SingleRecordWriter) recordWriterDelegate)
.getRecordWriter(0))
.getChannelSelector())
.isInstanceOf(ForwardPartitioner.class);
}
}

@Test
void testGetAndCheckMaxTraverseSize() {
Configuration config = new Configuration();
Expand Down
Loading