Spring Boot 优雅退出机制
问题
最近项目重构,改用 Spring Boot 框架,遇到个问题:当程序 catch 住某些 exception ,需要停掉整个 application ,然后人工介入查看。但是,发现没有办法停掉应用,应用本身也不继续跑下去,它就 hang 在那了。报错如下:
o.s.c.s.DefaultLifecycleProcessor.stop(387) - Failed to shut down 1 bean with phase value 2147483647 within timeout of 30000ms: [messageListenerContainer]
调查
定位到相关代码块如下:
public void onMessage(Message msg) {
try {
processMsg(msg);
} catch (Throwable t) {
System.exit(-1);
}
}
收到了JMS消息,但是processMsg
报错,然后会被 catch 住,然后执行System.exit
,但是失败了。
这个DefaultLifecycleProcessor
是 springframework 的类,在程序 shutdown 的时候会被调用来销毁/关闭 bean 。
分析
结合 jstack.review
查看 thread dump 如下:
没有死锁,进一步分析可知,SpringContextShutdownHook
想要 shutdown messageListenerContainer
,但是后者还在等消息。所以前者 timeout 了。
解决1
最简单的方案,退出时不要调用 Spring 的 ShutdownHook ,就不会有后面一系列的问题。在 properties 文件加入一行配置:
spring.main.register-shutdown-hook=false
这个方案足够简单,也奏效。但是没有合理地关闭资源,可能会造成资源浪费。如果频繁启停应用,可能会有问题。
解决2
这个问题的本质是,处理消息的线程不能自己关闭自己的 JMS container 。
那么,就建一个 monitor 线程,如果需要退出时,发一个信号给 monitor 线程,让它去关闭 JMS container (以及 DataSource, File, etc.) 。示例代码如下:
Executors.newSingleThreadExecutor.execute(new Runnable() {
public void run() {
if (signal)
stopTheContainer();
}
}
这个signal
,可以用一个AtomicBoolean
shutdownFlag 来实现。
解决2 - 补充
再“优雅”一点,遇到异常需要退出时,抛一个自定义的 Exception
,比如 ApplicationExitException
,然后用一个自定义的 ErrorHandler
去接住这个异常,然后再新起线程发出退出信号。
@Service
public class ApplicationExitErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable t) {
// actual exit logic
}
}
参考 -> https://www.baeldung.com/spring-jms#error-handler
这样做的好处是:解耦,业务代码和异常处理代码分离,逻辑上更加清晰。
坏处是:不熟悉 Spring Error Handling 框架的人 查代码/debug 起来更加困难。
解决3
网上有说升级 Spring Boot version to 2.3.4.RELEASE
就能解决问题的,这个笔者没有试过。仅仅列在这里作为一个可能的选项。
参考这里 -> https://github.com/spring-cloud/spring-cloud-gateway/issues/2037