[ 
https://issues.apache.org/jira/browse/FLINK-40918?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18141522#comment-18141522
 ] 

Shawn commented on FLINK-40918:
-------------------------------

[~zhuzh] [~dianfu] I'd like to fix this. Which of the two fixes in the 
description would you prefer? Could one of you assign it to me?

> SIGTERM during application-mode HA recovery permanently fails the suspended 
> jobs of a PyFlink application
> ---------------------------------------------------------------------------------------------------------
>
>                 Key: FLINK-40918
>                 URL: https://issues.apache.org/jira/browse/FLINK-40918
>             Project: Flink
>          Issue Type: Bug
>          Components: API / Python, Runtime / Coordination
>    Affects Versions: 2.3.0
>            Reporter: Ian Stewart
>            Priority: Critical
>
> In Flink 2.3.0 application mode with HA, a graceful JobManager shutdown 
> (SIGTERM) that arrives after a new JobManager has recovered a job from HA, 
> but before the user \{{main()}} has resubmitted it, marks that job globally 
> FAILED with "Job recovery is not needed." Flink then clears the job's HA 
> checkpoint pointers, removes its execution plan and writes a JobResultStore 
> entry. Every later JobManager ignores the resubmission as globally terminal, 
> so the job never runs again. For a PyFlink application the window lasts from 
> JobManager start until the Python driver resubmits the job.
>  
> h3. Environment
>  
> * Flink 2.3.0, application mode on Kubernetes (Flink Kubernetes Operator 
> 1.16.1), Kubernetes HA, \{{FileSystemJobResultStore}}.
> * PyFlink streaming jobs: entry class 
> \{{org.apache.flink.client.python.PythonDriver}}, detached \{{execute()}}, 
> checkpointing enabled.
>  
> h3. What happened
>  
> A node drain sent SIGTERM to a running JobManager. Its replacement recovered 
> 1 job graph from HA, logged \{{Successfully recovered 0 persisted 
> applications}}, started the Python driver (\{{Python Process Started}}), and 
> about 10 s later was itself sent SIGTERM by a second drain, before the driver 
> had resubmitted the job. Within about 100 ms the job was FAILED and its HA 
> data removed. Two unrelated jobs failed the same way in the same drain. The 
> JobResultStore entry:
>  
> {code}
> org.apache.flink.util.FlinkException: Job recovery is not needed.
> at 
> org.apache.flink.runtime.dispatcher.Dispatcher.cleanupRemainingSuspendedJobs(Dispatcher.java:1077)
> at 
> org.apache.flink.runtime.dispatcher.Dispatcher.notifyApplicationStatusChange(Dispatcher.java:1020)
> at 
> org.apache.flink.runtime.application.AbstractApplication.lambda$transitionState$0(AbstractApplication.java:255)
> at java.base/java.util.ArrayList.forEach(Unknown Source)
> at 
> org.apache.flink.runtime.application.AbstractApplication.transitionState(AbstractApplication.java:253)
> at 
> org.apache.flink.runtime.application.AbstractApplication.transitionToFailed(AbstractApplication.java:229)
> at 
> org.apache.flink.client.deployment.application.PackagedProgramApplication.lambda$finishAsFailed$7(PackagedProgramApplication.java:510)
> ...
> {code}
>  
> Every later JobManager logs \{{Successfully recovered 0 persisted job 
> graphs}}, and the driver's resubmit is rejected with \{{Ignoring job 
> submission ... because the job already reached a globally-terminal state}}. 
> The Kubernetes operator reports the job as not found rather than FAILED, so 
> \{{kubernetes.operator.job.restart.failed}} does not apply. The only recovery 
> is to redeploy from a savepoint.
>  
> The INFO/WARN lines for the shutdown-time steps below were not captured in 
> our logs, although the stack trace shows that the code ran. The 
> JobResultStore entry is the reliable evidence.
>  
> h3. Root cause (release-2.3.0)
>  
> # Application mode parks jobs recovered from HA in 
> \{{Dispatcher#suspendedJobs}} (Dispatcher.java:398-410). A parked job resumes 
> only when the user \{{main()}} resubmits the same job id and the client calls 
> \{{Dispatcher#recoverJob}} (Dispatcher.java:885).
> # \{{PythonDriver#main}} registers 
> \{{PythonEnvUtils.PythonProcessShutdownHook}} (PythonDriver.java:105-108). On 
> SIGTERM the JVM runs it at once and it destroys the Python process. The 
> driver's \{{main()}} then throws, and 
> \{{PackagedProgramApplication#onApplicationCanceledOrFailed}} takes the 
> "Application failed unexpectedly" branch 
> (PackagedProgramApplication.java:471-475) into \{{finishAsFailed}} and 
> \{{transitionToFailed}} (:510).
> # The branch that would make this harmless is the \{{isDisposing}} 
> cancellation (PackagedProgramApplication.java:457-460). It needs 
> \{{PackagedProgramApplication#dispose}}, which is called only from 
> \{{Dispatcher#onStop}} through \{{terminateRunningApplications}} 
> (Dispatcher.java:805, 2243-2255). On SIGTERM, 
> \{{DispatcherResourceManagerComponent#internalShutdown}} first closes the 
> dispatcher operation caches and the REST endpoint, and stops the dispatcher 
> only after that (DispatcherResourceManagerComponent.java:145-159). The 
> shutdown hook wins this race.
> # \{{transitionToFailed}} calls 
> \{{Dispatcher#notifyApplicationStatusChange}}. Its 
> \{{cleanupRemainingSuspendedJobs}} (Dispatcher.java:1020, 1066-1087) builds a 
> FAILED \{{JobResult}} "Job recovery is not needed." for every job still 
> parked and runs the cleanup runner. The completed checkpoint store is shut 
> down as FAILED and cleared, the execution plan is removed, and the 
> JobResultStore entry is written.
>  
> h3. Regression from 2.2
>  
> In 2.2.1, recovered jobs started at once. An unexpected \{{main()}} failure 
> was handed to \{{FatalErrorHandler#onFatalError}} 
> (\{{ApplicationDispatcherBootstrap#finishBootstrapTasks}}), so the JobManager 
> failed over with HA data intact. In 2.3.0, any death of the user \{{main()}} 
> during the recovery window is permanent for the parked jobs. Failing parked 
> jobs when an application genuinely fails appears intended (FLINK-38974 / 
> FLIP-560). Treating a JVM shutdown as an application failure is the bug.
>  
> PyFlink is the exposed case because PythonDriver's shutdown hook turns 
> SIGTERM into a \{{main()}} failure. A Java \{{main()}} has no such hook, and 
> \{{kill -9}} runs no shutdown hooks at all. The 2.3 release testing of HA 
> recovery (FLINK-39525) used \{{kill -9}} with Java jobs, so neither path was 
> exercised.
>  
> h3. Steps to reproduce
>  
> # Run a PyFlink streaming job in application mode with HA (ZooKeeper or 
> Kubernetes) and checkpointing enabled.
> # Once the job is running, \{{kill -9}} the JobManager so that a new 
> JobManager recovers the job from HA.
> # On the new JobManager, after \{{Python Process Started}} is logged and 
> before the job is resubmitted, send \{{kill -15}}.
> # Start another JobManager.
>  
> Expected: the job is recovered from HA and resumes from its latest checkpoint.
> Actual: \{{Successfully recovered 0 persisted job graphs}}. The 
> JobResultStore holds a FAILED entry with "Job recovery is not needed.", and 
> the resubmission is ignored as globally terminal.
>  
> h3. Possible fixes
>  
> * Do not fail suspended jobs when the application fails during the recovery 
> window. Either restore the 2.2 behaviour for that case (escalate to 
> \{{onFatalError}} so the JobManager fails over with HA intact), or clean up 
> remaining suspended jobs only when the application reaches FINISHED or 
> CANCELED.
> * Dispose running applications at the start of 
> \{{DispatcherResourceManagerComponent#internalShutdown}}, before the REST 
> endpoint closes, so that a \{{main()}} that dies during shutdown takes the 
> \{{isDisposing}} branch.
>  
> h3. Workaround
>  
> A Kubernetes \{{preStop}} hook on the JobManager that waits, for a bounded 
> time, while the PythonDriver's Python process is still running. This delays 
> SIGTERM until the job has been resubmitted. It does not cover a driver that 
> dies on its own inside the window.
>  



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to