[
https://issues.apache.org/jira/browse/FLINK-40302?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
최원용 updated FLINK-40302:
------------------------
Priority: Critical (was: Major)
> Periodic materialization permanently stops after taking a native-format
> savepoint when the changelog state backend is enabled
> -------------------------------------------------------------------------------------------------------------------------------
>
> Key: FLINK-40302
> URL: https://issues.apache.org/jira/browse/FLINK-40302
> Project: Flink
> Issue Type: Bug
> Components: Runtime / State Backends
> Affects Versions: 2.0.0, 1.20.0, 1.20.1, 2.1.0, 2.2.0, 2.3.0
> Reporter: 최원용
> Priority: Critical
>
> h3. Summary
> With the changelog state backend enabled ({{state.changelog.enabled: true}}),
> triggering a *single intermediate savepoint in NATIVE format* permanently
> stops periodic materialization.
> The job continues running and checkpointing normally, and no exception is
> reported. However, the delegated RocksDB backend is never flushed again:
> MemTables grow to their write-buffer limit, tombstones are never removed by
> compaction, and the changelog on which the job depends for recovery continues
> to grow. The only way to recover is to restart and restore the job.
> The only visible indication is the following INFO log, which repeats on every
> materialization attempt:
> {code}
> INFO o.a.f.state.changelog.ChangelogKeyedStateBackend - materialization:819
> not confirmed or failed or cancelled,
> skip trigger new one.
> {code}
> This was observed in production on Flink 1.20.1. A job remained in this state
> for more than seven hours, with materialization stopped and RocksDB
> flush/compaction stalled, until the job was restarted.
> h3. Reproduction
> # Run any streaming job using RocksDB with {{state.changelog.enabled: true}}.
> The default periodic materialization interval of 10 minutes is sufficient.
> # Let the job complete at least one successful materialization cycle.
> # Trigger an intermediate savepoint in NATIVE format while leaving the job
> running:
> {code:bash}
> flink savepoint <jobId> --type native
> {code}
> # After the savepoint completes, every subsequent materialization attempt
> logs the following message indefinitely:
> {code}
> materialization:N not confirmed or failed or cancelled, skip trigger new one.
> {code}
> The delegated {{RocksDBKeyedStateBackend}} is never flushed again.
> h3. Root cause
> Three individually correct mechanisms interact to cause this issue. All code
> references below point to master at
> [69cdb54|https://github.com/apache/flink/commit/69cdb54e953e]. The relevant
> code paths are unchanged on the current master branch; the {{release-1.20}}
> and {{release-2.2}} branches are functionally identical in these paths.
> *1. {{nativeSavepoint()}} consumes a materialization ID and relies on
> {{notifyCheckpointComplete()}} to confirm it.*
> See [ChangelogKeyedStateBackend#nativeSavepoint(),
> L447-L496|https://github.com/apache/flink/blob/69cdb54e953e/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/ChangelogKeyedStateBackend.java#L447-L496]:
> {code:java}
> long materializationID = materializedId++; // consumes an ID
> ...
> materializationIdByCheckpointId.put(
> checkpointId, materializationID); // waits for a notification
> {code}
> *2. However, intermediate savepoints never receive
> {{notifyCheckpointComplete()}}.*
> This is intentional behavior introduced by
> [FLIP-203|https://cwiki.apache.org/confluence/display/FLINK/FLIP-203%3A+Incremental+savepoints]
> in Flink 1.15: intermediate savepoints do not commit any side effects.
> [CheckpointCoordinator#cleanupAfterCompletedCheckpoint(),
> L1416-L1441|https://github.com/apache/flink/blob/69cdb54e953e/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/CheckpointCoordinator.java#L1416-L1441]
> sends acknowledge messages only under the following condition:
> {code:java}
> if (!props.isSavepoint() || props.isSynchronous())
> {code}
> Therefore, a savepoint that does not stop the job never sends a completion
> notification to the tasks.
> *3. The skip guard in {{initMaterialization()}} then blocks all subsequent
> materializations.*
> The guard was added by hotfix
> [85f32d6bcb|https://github.com/apache/flink/commit/85f32d6bcb31] as a
> follow-up to [FLINK-29913|https://issues.apache.org/jira/browse/FLINK-29913]
> / [PR #22669|https://github.com/apache/flink/pull/22669].
> See [ChangelogKeyedStateBackend#initMaterialization(),
> L853-L864|https://github.com/apache/flink/blob/69cdb54e953e/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/ChangelogKeyedStateBackend.java#L853-L864]:
> {code:java}
> if (lastConfirmedMaterializationId < materializedId - 1
> && lastFailedMaterializationId < materializedId - 1) {
> LOG.info(
> "materialization:{} not confirmed or failed or cancelled, "
> + "skip trigger new one.",
> materializedId - 1);
> return Optional.empty();
> }
> {code}
> The following sequence demonstrates the issue, starting from a steady state
> in which {{lastConfirmedMaterializationId = 7}} and {{materializedId = 8}}:
> || Event || {{lastConfirmedMaterializationId}} || {{materializedId}} ||
> Guard: {{lastConfirmed < materializedId -
> 1}} ||
> | Steady state | 7 | 8 | {{7 < 7}} = false → materialize |
> | NATIVE savepoint consumes ID 8 | 7 | 9 | Completion notification never
> arrives |
> | Every subsequent {{initMaterialization()}} call | 7 | 9 | {{7 < 8}} =
> *true* → skip indefinitely |
> *Why it never recovers automatically:*
> * The savepoint succeeds, so {{handleMaterializationFailureOrCancellation()}}
> is never called. Therefore, {{lastFailedMaterializationId}} remains {{-1}}.
> * Because materialization has stopped, the backend's base remains at ID 7.
> Every subsequent checkpoint maps its checkpoint ID to materialization ID 7,
> so {{notifyCheckpointComplete()}} can never advance
> {{lastConfirmedMaterializationId}} beyond 7.
> * The savepoint's pending entry is silently removed. The next checkpoint
> notification executes the following code at L655:
> {code:java}
> materializationIdByCheckpointId.headMap(checkpointId, true).clear();
> {code}
> This removes the savepoint's mapping without confirming it. After that, no
> state remains that could ever confirm materialization ID 8.
> * The only code path that resets these counters is {{completeRestore()}},
> which requires a full job restore.
> Regular checkpoints cannot cause this issue. {{snapshot()}} only references
> the current materialized base and never increments {{materializedId}}.
> The only two producers of materialization IDs are:
> * Periodic materialization, for which the confirmation loop is complete.
> * {{nativeSavepoint()}}, for which the confirmation path is severed.
> h3. Affected versions
> This deadlock requires both of the following:
> # The skip guard introduced by
> [85f32d6bcb|https://github.com/apache/flink/commit/85f32d6bcb31], which is
> present in Flink 1.16.3, 1.17.2, 1.18.0, and later versions.
> # Native savepoint support in the changelog state backend, introduced by
> [FLINK-29802|https://issues.apache.org/jira/browse/FLINK-29802] /
> [b62de02f|https://github.com/apache/flink/commit/b62de02f], which is present
> starting with Flink 1.20.0.
> Therefore, all releases starting with *Flink 1.20.0*, including all 2.x
> releases, are affected. The relevant code on the current master branch
> remains unchanged.
> h3. Proposed fix
> The guard protects {{SharedStateRegistry}} continuity for shared files, as
> discussed in [PR #22669|https://github.com/apache/flink/pull/22669]. However,
> a native savepoint uses {{NO_SHARING}} and does not participate in shared
> state. Furthermore, by design under FLIP-203, an intermediate savepoint can
> never be confirmed through {{notifyCheckpointComplete()}}.
> Therefore, a materialization ID consumed by {{nativeSavepoint()}} should not
> be able to block all subsequent materializations indefinitely.
> Two possible approaches are:
> *Option A:* When the native savepoint's snapshot future completes, whether
> successfully or exceptionally, mark its materialization ID as terminated for
> the purpose of the guard. For example:
> {code:java}
> lastFailedMaterializationId =
> Math.max(lastFailedMaterializationId, materializationID);
> {code}
> While the savepoint is in progress, the guard would continue to block
> periodic materialization, preserving the no-concurrent-materialization
> property. Restore behavior would remain unaffected because
> {{completeRestore()}} reads the materialization ID from the savepoint handle
> itself.
> *Option B:* Do not register the savepoint's checkpoint ID in
> {{materializationIdByCheckpointId}}, because the notification it waits for
> can never arrive. Instead, account for materialization IDs consumed by native
> savepoints separately in the guard.
> I would like to work on this issue. Could a committer please assign it to me?
--
This message was sent by Atlassian Jira
(v8.20.10#820010)