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


public void close(final int timeout) { startClose(); if (timeout > 0) { final long max = (long) timeout; final long start = System.currentTimeMillis(); if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) { // 如果注册中心有延迟,会立即受到readonly事件,下次不会再调用这台机器,当前已经调用的会处理完 sendChannelReadOnlyEvent(); } while (HeaderExchangeServer.this.isRunning() // ① && System.currentTimeMillis() - start < max) { try { Thread.sleep(10); } catch (InterruptedException e) { logger.warn(e.getMessage(), e); } } } doClose(); // ② server.close(timeout); // ③ } private boolean isRunning() { Collection<Channel> channels = getChannels(); for (Channel channel : channels) { if (DefaultFuture.hasFuture(channel)) { return true; } } return false; }private void doClose() { if (!closed.compareAndSet(false, true)) { return; } stopHeartbeatTimer(); try { scheduled.shutdown(); } catch (Throwable t) { logger.warn(t.getMessage(), t); } }化繁为简,这里只挑选上面代码中打标的两个地方进行分析

  1. 判断服务端是否还在处理请求,在超时时间内一直等待到所有任务处理完毕
  2. 关闭心跳检测
  3. 关闭 NettyServer
特别需要关注第一点,正符合我们在一开始提出的优雅停机的诉求 2:“在途请求需要处理完毕,不能被停机指令中断” 。
3.5 优雅停机初始方案总结
上述介绍的几个类构成了 Dubbo 2.5.x 的优雅停机方案,简单做一下总结,Dubbo 的优雅停机逻辑时序如下:
Registry 注销等待 -Ddubbo.service.shutdown.wait 秒,等待消费方收到下线通知Protocol 注销 DubboProtocol 注销 NettyServer 注销 等待处理中的请求完毕 停止发送心跳 关闭 Netty 相关资源 NettyClient 注销 停止发送心跳 等待处理中的请求完毕 关闭 Netty 相关资源
Dubbo 2.5.3 优雅停机的缺陷
如果你正在使用的 Dubbo 版本 <= 2.5.3,一些并发问题和代码缺陷会导致你的应用不能很好的实现优雅停机功能,请尽快升级 。
详情可以参考该 pull request 的变更:https://github.com/apache/dubbo/pull/568
 
4 Spring 容器下 Dubbo 的优雅停机上述的方案在不使用 Spring 时的确是无懈可击的,但由于现在大多数开发者选择使用 Spring 构建 Dubbo 应用,上述的方案会存在一些缺陷 。
由于 Spring 框架本身也依赖于 shutdown hook 执行优雅停机,并且与 Dubbo 的优雅停机会并发执行,而 Dubbo 的一些 Bean 受 Spring 托管,当 Spring 容器优先关闭时,会导致 Dubbo 的优雅停机流程无法获取相关的 Bean,从而优雅停机失效 。
Dubbo 开发者们迅速意识到了 shutdown hook 并发执行的问题,开始了一系列的补救措施 。
4.1 增加 ShutdownHookListener
Spring 如此受欢迎的原因之一便是它的扩展点非常丰富,例如它提供了 ApplicationListener 接口,开发者可以实现这个接口监听到 Spring 容器的关闭事件,为解决 shutdown hook 并发执行的问题,在 Dubbo 2.6.3 中新增了 ShutdownHookListener 类,用作 Spring 容器下的关闭 Dubbo 应用的钩子 。
private static class ShutdownHookListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextClosedEvent) { // we call it anyway since dubbo shutdown hook make sure its destroyAll() is re-entrant. // pls. note we should not remove dubbo shutdown hook when spring framework is present, this is because // its shutdown hook may not be installed. DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook(); shutdownHook.destroyAll(); } }}当服务提供者 ServiceBean 和服务消费者 ReferenceBean 被初始化时,会触发该钩子被创建 。
再来看看 AbstractConfig 中的代码,依旧保留了 JVM 的 shutdown hook
public abstract class AbstractConfig implements Serializable { static { Runtime.getRuntime().addShutdownHook(DubboShutdownHook.getDubboShutdownHook()); }}也就是说,在 Spring 环境下会注册两个钩子,在 Non-Spring 环境下只会有一个钩子,但看到 2.6.x 的实现大家是否意识到了两个问题呢?
  1. 两个钩子并发执行不会报错吗?
  2. 为什么在 Spring 下不取消 JVM 的钩子,只保留 Spring 的钩子不就可以工作了吗?
先解释第一个问题,这个按照我的理解,这段代码的 Commiter 可能认为只需要有一个 Spring 的钩子能正常注销就完事了,不需要考虑另外一个报不报错,因为都是独立的线程,不会有很大的影响 。
再解释第二个问题,其实这个疑问的答案就藏在上面 ShutdownHookListener 代码的注释中,这段注释的意思是说:在 Spring 框架下不能直接移除原先的 JVM 钩子,因为 Spring 框架可能没有注册 ContextClosed 事件 。啥意思呢?这里涉及到 Spring 框架生命周期的一个细节,我打算单独介绍一下 。


推荐阅读