背景
前段时间,线上X项目关闭时频繁抛出org.springframework.beans.factory.BeanCreationNotAllowedException
。收集报错日志,大概分为以下两类:
-
由dubbo调用触发
-
由MQ消费者触发
原因
简单观察以上栈信息,结合异常触发的情景,我们可以确认的事实有以下几点:
- 应用正在关闭
- MQ消费者在消费消息
- Dubbo线程在处理请求
- Spring容器的Bean正在销毁,不允许创建新的实例
不合理之处很明显,应用关闭时MQ消费者不应该再消费消息,Dubbo也不应该在接收请求。异常抛出的直接原因是MQ消费者和Dubbo线程依赖的Bean被销毁了,根本原因是停机顺序混乱。
分析
按照运行方式,我们的Spring应用大致可以分为两类:
-
独立的Tomcat运行Spring应用——传统Spring应用
-
Spring内嵌Tomcat以jar包的形式运行,也就是SpringBoot
现在我们以Spring容器、Servlet、Dubbo、MQ-Consumer做组件为例,简单分析一下以上两类情况如何做到优雅停机。
传统Spring应用
我们首先分析Spring容器、Servlet、Dubbo、MQ-Consumer这四者的依赖关系
如上图所示,MQ消费者工作时需要依赖Dubbo服务和Spring容器;Servlet提供Http服务时需要依赖Dubbo服务和Spring容器;Dubbo服务需要依赖Spring容器。本着被依赖者要先于依赖者启动,后于依赖者关闭的原则,各组件的关闭顺序应该为:MQ-Consumer|Servlet(不分先后)、Dubbo、Spring容器。然而,实际上的关闭顺序却并不是这样。
以X项目为例,其生命周期的控制由Servlet容器占主导地位。
应用启动时,
- 首先执行Servlet容器初始化,触发
org.springframework.web.context.ContextLoaderListener#contextInitialized
方法,初始化并刷新RootApplicationContext
,Service
Bean和DAO
Bean等实例化并初始化,MQ-Consumer和Dubbo服务也是在此时启动的; - 初始化
org.springframework.web.servlet.DispatcherServlet
等Servlet和Filter,触发WebApplicationContext
初始化,并设置和RootApplicationContext
的父子关系,然后刷新容器,Controller
Bean就是在此时实例化的。至此,Servlet容器启动完毕,可以对外提供http服务了。
应用关闭时,进程收到SIGTERM
信号,有两个重要的钩子会被触发,一个是org.apache.catalina.startup.Catalina.CatalinaShutdownHook
,另一个是org.apache.dubbo.config.DubboShutdownHook
。
其中DubboShutdownHook
执行时会关闭Dubbo服务。而CatalinaShutdownHook
执行过程中会顺序触发以下内容:
- 销毁Servlet和Filter。
- 销毁
DispatcherServlet
时会触发WebApplicationContext
关闭,Controller
等Bean会被销毁。 - 调用
javax.servlet.ServletContextListener#contextDestroyed
方法。 org.springframework.web.context.ContextLoaderListener#contextDestroyed
被调用时会关闭RootApplicationContext
,Service
Bean和DAO
Bean会在此时销毁,MQ-Consumer也是在此时作为一个普通的Bean被关闭的。