一文聊透 Dubbo 优雅停机( 四 )


4.2 Spring 的容器关闭事件详解
在 Spring 中,我们可以使用至少三种方式来注册容器关闭时一些收尾工作:

  1. 使用 DisposableBean 接口
public class TestDisposableBean implements DisposableBean { @Override public void destroy() throws Exception { System.out.println("== invoke DisposableBean =="); }}
  1. 使用 @PreDestroy 注解
public class TestPreDestroy { @PreDestroy public void preDestroy(){ System.out.println("== invoke preDestroy =="); }}
  1. 使用 ApplicationListener 监听 ContextClosedEvent
applicationContext.addApplicationListener(new ApplicationListener<ApplicationEvent>() { @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { if (applicationEvent instanceof ContextClosedEvent) { System.out.println("== receive context closed event =="); } }});但需要注意的是,在使用 SpringBoot 内嵌 Tomcat 容器时,容器关闭钩子是自动被注册,但使用纯粹的 Spring 框架或者外部 Tomcat 容器,需要显式的调用 context.registerShutdownHook(); 接口进行注册
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/beans.xml");context.start();context.registerShutdownHook();context.addApplicationListener(new ApplicationListener<ApplicationEvent>() { @Override public void onApplicationEvent(ApplicationEvent applicationEvent) { if (applicationEvent instanceof ContextClosedEvent) { System.out.println("== receive context closed event =="); } }});否则,上述三种回收方法都无法工作 。我们来看看 registerShutdownHook() 都干了啥
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext, DisposableBean{ @Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { @Override public void run() { synchronized (startupShutdownMonitor) { doClose(); } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); // 重点! } }}其实也就是显式注册了一个属于 Spring 的钩子 。这也解释上了 4.1 小节中,为什么有那段注释了,注册了事件不一定管用,还得保证 Spring 容器注册了它自己的钩子 。
4.3 Dubbo 优雅停机中级方案总结
第 4 节主要介绍了 Dubbo 开发者们在 Spring 环境下解决 Dubbo 优雅停机并发执行 shutdown hook 时的缺陷问题,但其实还不完善,因为在 Spring 环境下,如果没有显式注册 Spring 的 shutdown,还是会存在缺陷的,准确的说,Dubbo 2.6.x 版本可以很好的在 Non-Spring、Spring Boot、Spring + ContextClosedEvent 环境下很好的工作 。
5 Dubbo 2.7 最终方案public class SpringExtensionFactory implements ExtensionFactory { public static void addApplicationContext(ApplicationContext context) { CONTEXTS.add(context); if (context instanceof ConfigurableApplicationContext) { ((ConfigurableApplicationContext) context).registerShutdownHook(); DubboShutdownHook.getDubboShutdownHook().unregister(); } BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER); }}这段代码寥寥数行,却是经过了深思熟虑之后的产物,期间迭代了 3 个大版本,真是不容易 。这段代码很好地解决了第 4 节提出的两个问题
  1. 担心两个钩子并发执行有问题?那就在可以注册 Spring 钩子的时候取消掉 JVM 的钩子 。
  2. 担心当前 Spring 容器没有注册 Spring 钩子?那就显示调用 registerShutdownHook 进行注册 。
其他细节方面的优化和 bugfix 我就不进行详细介绍了,可以见得实现一个优雅停机需要考虑的点非常之多 。
6 总结优雅停机看似是一个不难的技术点,但在一个通用框架中,使用者的业务场景类型非常多,这会大大加剧整个代码实现的复杂度 。
摸清楚整个 Dubbo 优雅停机演化的过程,也着实花费了我一番功夫,有很多实现需要 checkout 到非常古老的分支,同时翻阅了很多 issue、pull request 的讨论,最终才形成了这篇文章,虽然研究的过程是困难的,但获取到真相是让人喜悦的 。
在开源产品的研发过程中,服务到每一个类型的用户真的是非常难的一件事,能做的是满足大部分用户 。例如 2.6.x 在大多数环境下其实已经没问题了,在 2.7.x 中则是得到了更加的完善,但是我相信,在使用 Dubbo 的部分用户中,可能还是会存在优雅停机的问题,只不过还没有被发现 。
商业化的思考:和开源产品一样,商业化产品的研发也同样是一个逐渐迭代的过程,需要数代开发者一起维护一份代码,使用者发现问题,开发者修复问题,这样的正反馈可以形成一个正反馈,促使产品更加优秀 。
喜欢的点个关注,一起学习探讨新技术 。


推荐阅读