[
https://issues.apache.org/jira/browse/FLINK-41157?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Dennis-Mircea Ciupitu updated FLINK-41157:
------------------------------------------
Description:
h1. Summary
For a FlinkDeployment in application mode with {{job.upgradeMode: stateless}}
and Kubernetes HA enabled in {{{}spec.flinkConfiguration{}}}, the JobManager
deployment recovery path and the cluster health-check restart path require the
per-job HA ConfigMap ({{{}<cluster-id>--config-map{}}}) to exist. When it does
not, the operator fails the restart with {{RestoreFailed}} ("HA metadata is not
available to restore from last state") instead of redeploying the stateless job
from empty state. The eligibility checks of both paths explicitly treat a
stateless job as always restartable without HA metadata, so the gating and the
execution disagree.
Depending on the trigger, the deployment either stays {{FAILED}} and retries
the same failing restore on every reconcile, or ends up with no running cluster
at all while reporting lifecycle state {{STABLE}} and no error.
h1. Root cause
h2. Eligibility accepts stateless jobs without HA metadata
* {{{}AbstractFlinkResourceReconciler.shouldRecoverDeployment{}}}: a lost
JobManager deployment is recovered when the job is stateless or HA is enabled.
* {{{}ApplicationReconciler.shouldRestartJobBecauseUnhealthy{}}}: a stateless
job is always restarted, other upgrade modes only when
{{isHaMetadataAvailable}} is true.
h2. Execution requires HA metadata whenever HA is enabled
* {{ApplicationReconciler.reconcileOtherChanges}} calls {{resubmitJob(ctx,
HighAvailabilityMode.isHighAvailabilityModeActivated(observeConfig))}} - The
upgrade mode is not considered.
* {{AbstractJobReconciler.resubmitJob}} switches the spec to {{LAST_STATE}}
when {{requireHaMetadata}} is true and keeps the job id.
* {{AbstractFlinkService.submitApplicationCluster}} then calls
{{{}validateHaMetadataExists{}}}, which throws {{{}UpgradeFailureException("HA
metadata is not available to restore from last state. ...",
"RestoreFailed"){}}}.
h2. History
FLINK-31277 (operator 1.5.0) replaced the earlier call site {{requireHaMetadata
= upgradeMode != STATELESS}} with the HA-only condition and at the same time
widened {{shouldRecoverDeployment}} to "stateless or HA".
The combination: stateless + HA enabled + missing metadata has never been
covered.
h2. The availability check is existence-only
* {{FlinkUtils.isValidHaConfigMap}} accepts any ConfigMap whose name ends in
{{-config-map}} except {{{}-cluster-config-map{}}}, ignoring both content and
job id.
* {{FlinkUtils.checkpointExists}} is only used to decide whether the dummy
last-state savepoint path is set in {{{}deploy{}}}.
h2. When the per-job ConfigMap is missing
Flink creates it when the JobMaster builds its checkpoint store and deletes it
itself on global job cleanup. It is missing when a human or a cleanup script
removed the HA ConfigMaps, when the JobManager never reached job submission, or
after the job reached a terminal state (the FLINK-37392 flavour).
h1. Reproduction
The reproduction can be done on {{examples/basic-checkpoint-ha.yaml}} with a
single change: {{{}upgradeMode: stateless{}}}. On minikube the hostPath
additionally needs {{type: DirectoryOrCreate}} and a writable {{/tmp/flink}} on
the node.
Common steps:
* Apply the deployment and wait for {{jobStatus.state=RUNNING}} and
{{lifecycleState=STABLE}} + ConfigMaps {{{}-{}}}{{{}cluster-config-map{}}} and
{{-config-map}} exist.
* Delete the per-job ConfigMap: {{{}kubectl delete cm --config-map{}}}. The
running job is not affected (no checkpointing configured in the example).
h2. Trigger A: JobManager deployment recovery
Delete the deployment:
{code:bash}
kubectl delete deployment basic-checkpoint-ha-example
{code}
Observed on the next reconcile:
{code}
Observing JobManager deployment. Previous status: READY
Exception while getting job status
java.util.concurrent.TimeoutException
at java.base/java.util.concurrent.CompletableFuture.timedGet(Unknown
Source)
at java.base/java.util.concurrent.CompletableFuture.get(Unknown Source)
at
org.apache.flink.kubernetes.operator.service.AbstractFlinkService.getJobStatus(AbstractFlinkService.java:314)
at
org.apache.flink.kubernetes.operator.observer.JobStatusObserver.observe(JobStatusObserver.java:89)
at
org.apache.flink.kubernetes.operator.observer.deployment.ApplicationObserver.observeFlinkCluster(ApplicationObserver.java:49)
at
org.apache.flink.kubernetes.operator.observer.deployment.AbstractFlinkDeploymentObserver.observeInternal(AbstractFlinkDeploymentObserver.java:70)
at
org.apache.flink.kubernetes.operator.observer.AbstractFlinkResourceObserver.observe(AbstractFlinkResourceObserver.java:49)
at
org.apache.flink.kubernetes.operator.controller.FlinkDeploymentController.reconcile(FlinkDeploymentController.java:145)
at
org.apache.flink.kubernetes.operator.controller.FlinkDeploymentController.reconcile(FlinkDeploymentController.java:59)
at
io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:191)
at
io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:147)
at
org.apache.flink.kubernetes.operator.metrics.OperatorJosdkMetrics.timeControllerExecution(OperatorJosdkMetrics.java:82)
at
io.javaoperatorsdk.operator.processing.Controller.reconcile(Controller.java:146)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.reconcileExecution(ReconciliationDispatcher.java:164)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleReconcile(ReconciliationDispatcher.java:145)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleDispatch(ReconciliationDispatcher.java:111)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleExecution(ReconciliationDispatcher.java:76)
at
io.javaoperatorsdk.operator.processing.event.EventProcessor$ReconcilerExecutor.run(EventProcessor.java:578)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown
Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown
Source)
at java.base/java.lang.Thread.run(Unknown Source)
Missing JobManager deployment
>>> Event[Job] | Warning | MISSING | Missing JobManager deployment
Config uses deprecated configuration key 'state.savepoints.dir' instead of
proper key 'execution.checkpointing.savepoint-dir'
>>> Event[Job] | Warning | RECOVERDEPLOYMENT | Recovering lost deployment
Resubmitting Flink job...
>>> Event[Job] | Info | SUBMIT | Starting deployment
Deploying application cluster requiring last-state from HA metadata
Job graph in ConfigMap stateless-ha-repro-cluster-config-map is deleted
>>> Event[Job] | Warning | RESTOREFAILED | HA metadata is not available
>>> to restore from last state.It is possible that the job has finished or
>>> terminally failed, or the configmaps have been deleted.
>>> Status[Job] | Error | FAILED | {...}
{code}
Status: {{{}jobStatus.state=RECONCILING{}}},
{{{}jobManagerDeploymentStatus=MISSING{}}}, {{{}lifecycleState=FAILED{}}},
{{error}} set to the {{{}UpgradeFailureException{}}}. The {{RecoverDeployment}}
/ {{Submit}} / {{RestoreFailed}} sequence repeated on every reconcile (event
count 5 after 3.5 minutes). No pods running.
h2. Trigger B: cluster health-check restart
Additional configuration on the deployment (within
{{{}spec.flinkConfiguration{}}}):
{code:java}
kubernetes.operator.cluster.health-check.enabled: "true"
kubernetes.operator.cluster.health-check.restarts.threshold: "1"
kubernetes.operator.cluster.health-check.restarts.window: 10 min
kubernetes.operator.cluster.health-check.checkpoint-progress.enabled: "false"
{code}
Delete the TaskManager pod twice, waiting for the new TaskManager to register
in between ({{{}numRestarts{}}} goes from 1 to 3). The following command was
used:
{code:java}
kubectl delete pod -l app=basic-checkpoint-ha-example,component=taskmanager
{code}
Observed within a single reconcile (operator log):
{code:java}
>>> Event[Job] | Info | JOBSTATUSCHANGED | Job status changed from
>>> RUNNING to RESTARTING
>>> Event[Job] | Warning | JOBEXCEPTION |
>>> org.apache.flink.util.FlinkExpectedException: The TaskExecutor is shutting
>>> down.
at
org.apache.flink.runtime.taskexecutor.TaskExecutor.onStop(TaskExecutor.java:523)
at
org.apache.flink.runtime.rpc.RpcEndpoint.internalCallOnStop(RpcEndpoint.java:255)
at
org.apache.flink.runtime.rpc.pekko.PekkoRpcActor$StartedState.lambda$terminate$0(PekkoRpcActor.java:583)
at
org.apache.flink.runtime.concurrent.ClassLoadingUtils.runWithContextClassLoader(ClassLoadingUtils.java:83)
... (22 more lines)
>>> Event[Job] | Warning | JOBEXCEPTION |
>>> org.apache.flink.util.FlinkExpectedException: The TaskExecutor is shutting
>>> down.
at
org.apache.flink.runtime.taskexecutor.TaskExecutor.onStop(TaskExecutor.java:523)
at
org.apache.flink.runtime.rpc.RpcEndpoint.internalCallOnStop(RpcEndpoint.java:255)
at
org.apache.flink.runtime.rpc.pekko.PekkoRpcActor$StartedState.lambda$terminate$0(PekkoRpcActor.java:583)
at
org.apache.flink.runtime.concurrent.ClassLoadingUtils.runWithContextClassLoader(ClassLoadingUtils.java:83)
... (22 more lines)
...
Restart count hit threshold: 1
>>> Event[Job] | Warning | RESTARTUNHEALTHYJOB | Restart count hit
>>> threshold: 1
Deleting cluster with Foreground propagation
Scaling JobManager Deployment to zero with 60 seconds timeout...
Completed Scaling JobManager Deployment to zero
Deleting JobManager Deployment with 298 seconds timeout...
Completed Deleting JobManager Deployment
Keeping HA metadata for last-state restore
Resubmitting Flink job...
>>> Event[Job] | Info | SUBMIT | Starting deployment
Deploying application cluster requiring last-state from HA metadata
Job graph in ConfigMap basic-checkpoint-ha-example-cluster-config-map is deleted
>>> Event[Job] | Warning | RESTOREFAILED | HA metadata is not available
>>> to restore from last state.It is possible that the job has finished or
>>> terminally failed, or the configmaps have been deleted.
>>> Status[Job] | Error | FAILED | {...}
{code}
End state: {{{}jobStatus.state=FINISHED{}}},
{{{}jobManagerDeploymentStatus=MISSING{}}}, {{{}lifecycleState=STABLE{}}},
{{error}} empty, no pods, {{RestoreFailed}} event count still 1 after 6 minutes.
In both triggers the attempted submit also removed the {{executionPlan-*}}
entry from {{-cluster-config-map}} (5 keys to 0) before the validation threw.
h1. Impact
The impact is different per each trigger:
* Trigger A: a stateless job with HA enabled cannot recover from a lost
JobManager deployment without manual intervention, and the operator repeats a
failing deploy attempt on every reconcile.
* Trigger B: a stateless job with HA enabled and the health check turned on is
terminated by the operator and never restarted, with no error recorded and
lifecycle {{{}STABLE{}}}. Alerting on {{lifecycleState}} or {{status.error}}
does not catch it.
The documented semantics of {{stateless}} ("upgrades from empty state") are not
honoured by the recovery paths.
h1. Workaround
The workaroung for the described issue is represented by any spec change, for
example bumping {{{}restartNonce{}}}.
* In the trigger A state (job {{{}RECONCILING{}}}) this cancels in stateless
mode, deletes the HA ConfigMaps and redeploys with a new job id (verified,
{{RUNNING}} about 30 s after the patch).
* In the trigger B state (job {{{}FINISHED{}}}) the suspend step is a no-op,
the cluster is redeployed with a new job id and the HA ConfigMaps are left
behind.
was:
h1. Summary
For a FlinkDeployment in application mode with {{job.upgradeMode: stateless}}
and Kubernetes HA enabled in {{{}spec.flinkConfiguration{}}}, the JobManager
deployment recovery path and the cluster health-check restart path require the
per-job HA ConfigMap ({{{}<cluster-id>--config-map{}}}) to exist. When it does
not, the operator fails the restart with {{RestoreFailed}} ("HA metadata is not
available to restore from last state") instead of redeploying the stateless job
from empty state. The eligibility checks of both paths explicitly treat a
stateless job as always restartable without HA metadata, so the gating and the
execution disagree.
Depending on the trigger, the deployment either stays {{FAILED}} and retries
the same failing restore on every reconcile, or ends up with no running cluster
at all while reporting lifecycle state {{STABLE}} and no error.
h1. Root cause
h2. Eligibility accepts stateless jobs without HA metadata
* {{{}AbstractFlinkResourceReconciler.shouldRecoverDeployment{}}}: a lost
JobManager deployment is recovered when the job is stateless or HA is enabled.
* {{{}ApplicationReconciler.shouldRestartJobBecauseUnhealthy{}}}: a stateless
job is always restarted, other upgrade modes only when
{{isHaMetadataAvailable}} is true.
h2. Execution requires HA metadata whenever HA is enabled
* {{ApplicationReconciler.reconcileOtherChanges}} calls {{resubmitJob(ctx,
HighAvailabilityMode.isHighAvailabilityModeActivated(observeConfig))}} - The
upgrade mode is not considered.
* {{AbstractJobReconciler.resubmitJob}} switches the spec to {{LAST_STATE}}
when {{requireHaMetadata}} is true and keeps the job id.
* {{AbstractFlinkService.submitApplicationCluster}} then calls
{{{}validateHaMetadataExists{}}}, which throws {{{}UpgradeFailureException("HA
metadata is not available to restore from last state. ...",
"RestoreFailed"){}}}.
h2. History
FLINK-31277 (operator 1.5.0) replaced the earlier call site {{requireHaMetadata
= upgradeMode != STATELESS}} with the HA-only condition and at the same time
widened {{shouldRecoverDeployment}} to "stateless or HA".
The combination: stateless + HA enabled + missing metadata has never been
covered.
h2. The availability check is existence-only
* {{FlinkUtils.isValidHaConfigMap}} accepts any ConfigMap whose name ends in
{{-config-map}} except {{{}-cluster-config-map{}}}, ignoring both content and
job id.
* {{FlinkUtils.checkpointExists}} is only used to decide whether the dummy
last-state savepoint path is set in {{{}deploy{}}}.
h2. When the per-job ConfigMap is missing
Flink creates it when the JobMaster builds its checkpoint store and deletes it
itself on global job cleanup. It is missing when a human or a cleanup script
removed the HA ConfigMaps, when the JobManager never reached job submission, or
after the job reached a terminal state (the FLINK-37392 flavour).
h1. Reproduction
The reproduction can be done on {{examples/basic-checkpoint-ha.yaml}} with a
single change: {{{}upgradeMode: stateless{}}}. On minikube the hostPath
additionally needs {{type: DirectoryOrCreate}} and a writable {{/tmp/flink}} on
the node.
Common steps:
* Apply the deployment and wait for {{jobStatus.state=RUNNING}} and
{{lifecycleState=STABLE}} + ConfigMaps {{{}-{}}}{{{}cluster-config-map{}}} and
{{-config-map}} exist.
* Delete the per-job ConfigMap: {{{}kubectl delete cm --config-map{}}}. The
running job is not affected (no checkpointing configured in the example).
h2. Trigger A: JobManager deployment recovery
Delete the deployment: {{{}kubectl delete deployment
{{basic-checkpoint-ha-example{}}}}}
Observed on the next reconcile:
{code}
Observing JobManager deployment. Previous status: READY
Exception while getting job status
java.util.concurrent.TimeoutException
at java.base/java.util.concurrent.CompletableFuture.timedGet(Unknown
Source)
at java.base/java.util.concurrent.CompletableFuture.get(Unknown Source)
at
org.apache.flink.kubernetes.operator.service.AbstractFlinkService.getJobStatus(AbstractFlinkService.java:314)
at
org.apache.flink.kubernetes.operator.observer.JobStatusObserver.observe(JobStatusObserver.java:89)
at
org.apache.flink.kubernetes.operator.observer.deployment.ApplicationObserver.observeFlinkCluster(ApplicationObserver.java:49)
at
org.apache.flink.kubernetes.operator.observer.deployment.AbstractFlinkDeploymentObserver.observeInternal(AbstractFlinkDeploymentObserver.java:70)
at
org.apache.flink.kubernetes.operator.observer.AbstractFlinkResourceObserver.observe(AbstractFlinkResourceObserver.java:49)
at
org.apache.flink.kubernetes.operator.controller.FlinkDeploymentController.reconcile(FlinkDeploymentController.java:145)
at
org.apache.flink.kubernetes.operator.controller.FlinkDeploymentController.reconcile(FlinkDeploymentController.java:59)
at
io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:191)
at
io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:147)
at
org.apache.flink.kubernetes.operator.metrics.OperatorJosdkMetrics.timeControllerExecution(OperatorJosdkMetrics.java:82)
at
io.javaoperatorsdk.operator.processing.Controller.reconcile(Controller.java:146)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.reconcileExecution(ReconciliationDispatcher.java:164)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleReconcile(ReconciliationDispatcher.java:145)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleDispatch(ReconciliationDispatcher.java:111)
at
io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleExecution(ReconciliationDispatcher.java:76)
at
io.javaoperatorsdk.operator.processing.event.EventProcessor$ReconcilerExecutor.run(EventProcessor.java:578)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown
Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown
Source)
at java.base/java.lang.Thread.run(Unknown Source)
Missing JobManager deployment
>>> Event[Job] | Warning | MISSING | Missing JobManager deployment
Config uses deprecated configuration key 'state.savepoints.dir' instead of
proper key 'execution.checkpointing.savepoint-dir'
>>> Event[Job] | Warning | RECOVERDEPLOYMENT | Recovering lost deployment
Resubmitting Flink job...
>>> Event[Job] | Info | SUBMIT | Starting deployment
Deploying application cluster requiring last-state from HA metadata
Job graph in ConfigMap stateless-ha-repro-cluster-config-map is deleted
>>> Event[Job] | Warning | RESTOREFAILED | HA metadata is not available
>>> to restore from last state.It is possible that the job has finished or
>>> terminally failed, or the configmaps have been deleted.
>>> Status[Job] | Error | FAILED | {...}
{code}
Status: {{{}jobStatus.state=RECONCILING{}}},
{{{}jobManagerDeploymentStatus=MISSING{}}}, {{{}lifecycleState=FAILED{}}},
{{error}} set to the {{{}UpgradeFailureException{}}}. The {{RecoverDeployment}}
/ {{Submit}} / {{RestoreFailed}} sequence repeated on every reconcile (event
count 5 after 3.5 minutes). No pods running.
h2. Trigger B: cluster health-check restart
Additional configuration on the deployment (within
{{{}spec.flinkConfiguration{}}}):
{code:java}
kubernetes.operator.cluster.health-check.enabled: "true"
kubernetes.operator.cluster.health-check.restarts.threshold: "1"
kubernetes.operator.cluster.health-check.restarts.window: 10 min
kubernetes.operator.cluster.health-check.checkpoint-progress.enabled: "false"
{code}
Delete the TaskManager pod twice, waiting for the new TaskManager to register
in between ({{{}numRestarts{}}} goes from 1 to 3). The following command was
used:
{code:java}
kubectl delete pod -l app=basic-checkpoint-ha-example,component=taskmanager
{code}
Observed within a single reconcile (operator log):
{code:java}
>>> Event[Job] | Info | JOBSTATUSCHANGED | Job status changed from
>>> RUNNING to RESTARTING
>>> Event[Job] | Warning | JOBEXCEPTION |
>>> org.apache.flink.util.FlinkExpectedException: The TaskExecutor is shutting
>>> down.
at
org.apache.flink.runtime.taskexecutor.TaskExecutor.onStop(TaskExecutor.java:523)
at
org.apache.flink.runtime.rpc.RpcEndpoint.internalCallOnStop(RpcEndpoint.java:255)
at
org.apache.flink.runtime.rpc.pekko.PekkoRpcActor$StartedState.lambda$terminate$0(PekkoRpcActor.java:583)
at
org.apache.flink.runtime.concurrent.ClassLoadingUtils.runWithContextClassLoader(ClassLoadingUtils.java:83)
... (22 more lines)
>>> Event[Job] | Warning | JOBEXCEPTION |
>>> org.apache.flink.util.FlinkExpectedException: The TaskExecutor is shutting
>>> down.
at
org.apache.flink.runtime.taskexecutor.TaskExecutor.onStop(TaskExecutor.java:523)
at
org.apache.flink.runtime.rpc.RpcEndpoint.internalCallOnStop(RpcEndpoint.java:255)
at
org.apache.flink.runtime.rpc.pekko.PekkoRpcActor$StartedState.lambda$terminate$0(PekkoRpcActor.java:583)
at
org.apache.flink.runtime.concurrent.ClassLoadingUtils.runWithContextClassLoader(ClassLoadingUtils.java:83)
... (22 more lines)
...
Restart count hit threshold: 1
>>> Event[Job] | Warning | RESTARTUNHEALTHYJOB | Restart count hit
>>> threshold: 1
Deleting cluster with Foreground propagation
Scaling JobManager Deployment to zero with 60 seconds timeout...
Completed Scaling JobManager Deployment to zero
Deleting JobManager Deployment with 298 seconds timeout...
Completed Deleting JobManager Deployment
Keeping HA metadata for last-state restore
Resubmitting Flink job...
>>> Event[Job] | Info | SUBMIT | Starting deployment
Deploying application cluster requiring last-state from HA metadata
Job graph in ConfigMap basic-checkpoint-ha-example-cluster-config-map is deleted
>>> Event[Job] | Warning | RESTOREFAILED | HA metadata is not available
>>> to restore from last state.It is possible that the job has finished or
>>> terminally failed, or the configmaps have been deleted.
>>> Status[Job] | Error | FAILED | {...}
{code}
End state: {{{}jobStatus.state=FINISHED{}}},
{{{}jobManagerDeploymentStatus=MISSING{}}}, {{{}lifecycleState=STABLE{}}},
{{error}} empty, no pods, {{RestoreFailed}} event count still 1 after 6 minutes.
In both triggers the attempted submit also removed the {{executionPlan-*}}
entry from {{-cluster-config-map}} (5 keys to 0) before the validation threw.
h1. Impact
The impact is different per each trigger:
* Trigger A: a stateless job with HA enabled cannot recover from a lost
JobManager deployment without manual intervention, and the operator repeats a
failing deploy attempt on every reconcile.
* Trigger B: a stateless job with HA enabled and the health check turned on is
terminated by the operator and never restarted, with no error recorded and
lifecycle {{{}STABLE{}}}. Alerting on {{lifecycleState}} or {{status.error}}
does not catch it.
The documented semantics of {{stateless}} ("upgrades from empty state") are not
honoured by the recovery paths.
h1. Workaround
The workaroung for the described issue is represented by any spec change, for
example bumping {{{}restartNonce{}}}.
* In the trigger A state (job {{{}RECONCILING{}}}) this cancels in stateless
mode, deletes the HA ConfigMaps and redeploys with a new job id (verified,
{{RUNNING}} about 30 s after the patch).
* In the trigger B state (job {{{}FINISHED{}}}) the suspend step is a no-op,
the cluster is redeployed with a new job id and the HA ConfigMaps are left
behind.
> Stateless jobs with HA enabled cannot be recovered or restarted when HA
> metadata is missing
> -------------------------------------------------------------------------------------------
>
> Key: FLINK-41157
> URL: https://issues.apache.org/jira/browse/FLINK-41157
> Project: Flink
> Issue Type: Bug
> Components: Kubernetes Operator
> Reporter: Dennis-Mircea Ciupitu
> Priority: Major
>
> h1. Summary
> For a FlinkDeployment in application mode with {{job.upgradeMode: stateless}}
> and Kubernetes HA enabled in {{{}spec.flinkConfiguration{}}}, the JobManager
> deployment recovery path and the cluster health-check restart path require
> the per-job HA ConfigMap ({{{}<cluster-id>--config-map{}}}) to exist. When it
> does not, the operator fails the restart with {{RestoreFailed}} ("HA metadata
> is not available to restore from last state") instead of redeploying the
> stateless job from empty state. The eligibility checks of both paths
> explicitly treat a stateless job as always restartable without HA metadata,
> so the gating and the execution disagree.
> Depending on the trigger, the deployment either stays {{FAILED}} and retries
> the same failing restore on every reconcile, or ends up with no running
> cluster at all while reporting lifecycle state {{STABLE}} and no error.
> h1. Root cause
> h2. Eligibility accepts stateless jobs without HA metadata
> * {{{}AbstractFlinkResourceReconciler.shouldRecoverDeployment{}}}: a lost
> JobManager deployment is recovered when the job is stateless or HA is enabled.
> * {{{}ApplicationReconciler.shouldRestartJobBecauseUnhealthy{}}}: a
> stateless job is always restarted, other upgrade modes only when
> {{isHaMetadataAvailable}} is true.
> h2. Execution requires HA metadata whenever HA is enabled
> * {{ApplicationReconciler.reconcileOtherChanges}} calls {{resubmitJob(ctx,
> HighAvailabilityMode.isHighAvailabilityModeActivated(observeConfig))}} - The
> upgrade mode is not considered.
> * {{AbstractJobReconciler.resubmitJob}} switches the spec to {{LAST_STATE}}
> when {{requireHaMetadata}} is true and keeps the job id.
> * {{AbstractFlinkService.submitApplicationCluster}} then calls
> {{{}validateHaMetadataExists{}}}, which throws
> {{{}UpgradeFailureException("HA metadata is not available to restore from
> last state. ...", "RestoreFailed"){}}}.
> h2. History
> FLINK-31277 (operator 1.5.0) replaced the earlier call site
> {{requireHaMetadata = upgradeMode != STATELESS}} with the HA-only condition
> and at the same time widened {{shouldRecoverDeployment}} to "stateless or HA".
> The combination: stateless + HA enabled + missing metadata has never been
> covered.
> h2. The availability check is existence-only
> * {{FlinkUtils.isValidHaConfigMap}} accepts any ConfigMap whose name ends in
> {{-config-map}} except {{{}-cluster-config-map{}}}, ignoring both content and
> job id.
> * {{FlinkUtils.checkpointExists}} is only used to decide whether the dummy
> last-state savepoint path is set in {{{}deploy{}}}.
> h2. When the per-job ConfigMap is missing
> Flink creates it when the JobMaster builds its checkpoint store and deletes
> it itself on global job cleanup. It is missing when a human or a cleanup
> script removed the HA ConfigMaps, when the JobManager never reached job
> submission, or after the job reached a terminal state (the FLINK-37392
> flavour).
> h1. Reproduction
> The reproduction can be done on {{examples/basic-checkpoint-ha.yaml}} with a
> single change: {{{}upgradeMode: stateless{}}}. On minikube the hostPath
> additionally needs {{type: DirectoryOrCreate}} and a writable {{/tmp/flink}}
> on the node.
> Common steps:
> * Apply the deployment and wait for {{jobStatus.state=RUNNING}} and
> {{lifecycleState=STABLE}} + ConfigMaps {{{}-{}}}{{{}cluster-config-map{}}}
> and {{-config-map}} exist.
> * Delete the per-job ConfigMap: {{{}kubectl delete cm --config-map{}}}. The
> running job is not affected (no checkpointing configured in the example).
> h2. Trigger A: JobManager deployment recovery
> Delete the deployment:
> {code:bash}
> kubectl delete deployment basic-checkpoint-ha-example
> {code}
> Observed on the next reconcile:
> {code}
> Observing JobManager deployment. Previous status: READY
> Exception while getting job status
> java.util.concurrent.TimeoutException
> at java.base/java.util.concurrent.CompletableFuture.timedGet(Unknown
> Source)
> at java.base/java.util.concurrent.CompletableFuture.get(Unknown
> Source)
> at
> org.apache.flink.kubernetes.operator.service.AbstractFlinkService.getJobStatus(AbstractFlinkService.java:314)
> at
> org.apache.flink.kubernetes.operator.observer.JobStatusObserver.observe(JobStatusObserver.java:89)
> at
> org.apache.flink.kubernetes.operator.observer.deployment.ApplicationObserver.observeFlinkCluster(ApplicationObserver.java:49)
> at
> org.apache.flink.kubernetes.operator.observer.deployment.AbstractFlinkDeploymentObserver.observeInternal(AbstractFlinkDeploymentObserver.java:70)
> at
> org.apache.flink.kubernetes.operator.observer.AbstractFlinkResourceObserver.observe(AbstractFlinkResourceObserver.java:49)
> at
> org.apache.flink.kubernetes.operator.controller.FlinkDeploymentController.reconcile(FlinkDeploymentController.java:145)
> at
> org.apache.flink.kubernetes.operator.controller.FlinkDeploymentController.reconcile(FlinkDeploymentController.java:59)
> at
> io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:191)
> at
> io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:147)
> at
> org.apache.flink.kubernetes.operator.metrics.OperatorJosdkMetrics.timeControllerExecution(OperatorJosdkMetrics.java:82)
> at
> io.javaoperatorsdk.operator.processing.Controller.reconcile(Controller.java:146)
> at
> io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.reconcileExecution(ReconciliationDispatcher.java:164)
> at
> io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleReconcile(ReconciliationDispatcher.java:145)
> at
> io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleDispatch(ReconciliationDispatcher.java:111)
> at
> io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleExecution(ReconciliationDispatcher.java:76)
> at
> io.javaoperatorsdk.operator.processing.event.EventProcessor$ReconcilerExecutor.run(EventProcessor.java:578)
> at
> java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
> at
> java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
> at java.base/java.lang.Thread.run(Unknown Source)
> Missing JobManager deployment
> >>> Event[Job] | Warning | MISSING | Missing JobManager
> >>> deployment
> Config uses deprecated configuration key 'state.savepoints.dir' instead of
> proper key 'execution.checkpointing.savepoint-dir'
> >>> Event[Job] | Warning | RECOVERDEPLOYMENT | Recovering lost
> >>> deployment
> Resubmitting Flink job...
> >>> Event[Job] | Info | SUBMIT | Starting deployment
> Deploying application cluster requiring last-state from HA metadata
> Job graph in ConfigMap stateless-ha-repro-cluster-config-map is deleted
> >>> Event[Job] | Warning | RESTOREFAILED | HA metadata is not
> >>> available to restore from last state.It is possible that the job has
> >>> finished or terminally failed, or the configmaps have been deleted.
> >>> Status[Job] | Error | FAILED | {...}
> {code}
> Status: {{{}jobStatus.state=RECONCILING{}}},
> {{{}jobManagerDeploymentStatus=MISSING{}}}, {{{}lifecycleState=FAILED{}}},
> {{error}} set to the {{{}UpgradeFailureException{}}}. The
> {{RecoverDeployment}} / {{Submit}} / {{RestoreFailed}} sequence repeated on
> every reconcile (event count 5 after 3.5 minutes). No pods running.
> h2. Trigger B: cluster health-check restart
> Additional configuration on the deployment (within
> {{{}spec.flinkConfiguration{}}}):
> {code:java}
> kubernetes.operator.cluster.health-check.enabled: "true"
> kubernetes.operator.cluster.health-check.restarts.threshold: "1"
> kubernetes.operator.cluster.health-check.restarts.window: 10 min
> kubernetes.operator.cluster.health-check.checkpoint-progress.enabled: "false"
> {code}
> Delete the TaskManager pod twice, waiting for the new TaskManager to register
> in between ({{{}numRestarts{}}} goes from 1 to 3). The following command was
> used:
> {code:java}
> kubectl delete pod -l app=basic-checkpoint-ha-example,component=taskmanager
> {code}
> Observed within a single reconcile (operator log):
> {code:java}
> >>> Event[Job] | Info | JOBSTATUSCHANGED | Job status changed from
> >>> RUNNING to RESTARTING
> >>> Event[Job] | Warning | JOBEXCEPTION |
> >>> org.apache.flink.util.FlinkExpectedException: The TaskExecutor is
> >>> shutting down.
> at
> org.apache.flink.runtime.taskexecutor.TaskExecutor.onStop(TaskExecutor.java:523)
> at
> org.apache.flink.runtime.rpc.RpcEndpoint.internalCallOnStop(RpcEndpoint.java:255)
> at
> org.apache.flink.runtime.rpc.pekko.PekkoRpcActor$StartedState.lambda$terminate$0(PekkoRpcActor.java:583)
> at
> org.apache.flink.runtime.concurrent.ClassLoadingUtils.runWithContextClassLoader(ClassLoadingUtils.java:83)
> ... (22 more lines)
> >>> Event[Job] | Warning | JOBEXCEPTION |
> >>> org.apache.flink.util.FlinkExpectedException: The TaskExecutor is
> >>> shutting down.
> at
> org.apache.flink.runtime.taskexecutor.TaskExecutor.onStop(TaskExecutor.java:523)
> at
> org.apache.flink.runtime.rpc.RpcEndpoint.internalCallOnStop(RpcEndpoint.java:255)
> at
> org.apache.flink.runtime.rpc.pekko.PekkoRpcActor$StartedState.lambda$terminate$0(PekkoRpcActor.java:583)
> at
> org.apache.flink.runtime.concurrent.ClassLoadingUtils.runWithContextClassLoader(ClassLoadingUtils.java:83)
> ... (22 more lines)
> ...
> Restart count hit threshold: 1
> >>> Event[Job] | Warning | RESTARTUNHEALTHYJOB | Restart count hit
> >>> threshold: 1
> Deleting cluster with Foreground propagation
> Scaling JobManager Deployment to zero with 60 seconds timeout...
> Completed Scaling JobManager Deployment to zero
> Deleting JobManager Deployment with 298 seconds timeout...
> Completed Deleting JobManager Deployment
> Keeping HA metadata for last-state restore
> Resubmitting Flink job...
> >>> Event[Job] | Info | SUBMIT | Starting deployment
> Deploying application cluster requiring last-state from HA metadata
> Job graph in ConfigMap basic-checkpoint-ha-example-cluster-config-map is
> deleted
> >>> Event[Job] | Warning | RESTOREFAILED | HA metadata is not
> >>> available to restore from last state.It is possible that the job has
> >>> finished or terminally failed, or the configmaps have been deleted.
> >>> Status[Job] | Error | FAILED | {...}
> {code}
> End state: {{{}jobStatus.state=FINISHED{}}},
> {{{}jobManagerDeploymentStatus=MISSING{}}}, {{{}lifecycleState=STABLE{}}},
> {{error}} empty, no pods, {{RestoreFailed}} event count still 1 after 6
> minutes.
> In both triggers the attempted submit also removed the {{executionPlan-*}}
> entry from {{-cluster-config-map}} (5 keys to 0) before the validation threw.
> h1. Impact
> The impact is different per each trigger:
> * Trigger A: a stateless job with HA enabled cannot recover from a lost
> JobManager deployment without manual intervention, and the operator repeats a
> failing deploy attempt on every reconcile.
> * Trigger B: a stateless job with HA enabled and the health check turned on
> is terminated by the operator and never restarted, with no error recorded and
> lifecycle {{{}STABLE{}}}. Alerting on {{lifecycleState}} or {{status.error}}
> does not catch it.
> The documented semantics of {{stateless}} ("upgrades from empty state") are
> not honoured by the recovery paths.
> h1. Workaround
> The workaroung for the described issue is represented by any spec change, for
> example bumping {{{}restartNonce{}}}.
> * In the trigger A state (job {{{}RECONCILING{}}}) this cancels in stateless
> mode, deletes the HA ConfigMaps and redeploys with a new job id (verified,
> {{RUNNING}} about 30 s after the patch).
> * In the trigger B state (job {{{}FINISHED{}}}) the suspend step is a no-op,
> the cluster is redeployed with a new job id and the HA ConfigMaps are left
> behind.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)