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); } }化繁为简,这里只挑选上面代码中打标的两个地方进行分析
- 判断服务端是否还在处理请求,在超时时间内一直等待到所有任务处理完毕
- 关闭心跳检测
- 关闭 NettyServer
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 的实现大家是否意识到了两个问题呢?
- 两个钩子并发执行不会报错吗?
- 为什么在 Spring 下不取消 JVM 的钩子,只保留 Spring 的钩子不就可以工作了吗?
再解释第二个问题,其实这个疑问的答案就藏在上面 ShutdownHookListener 代码的注释中,这段注释的意思是说:在 Spring 框架下不能直接移除原先的 JVM 钩子,因为 Spring 框架可能没有注册 ContextClosed 事件 。啥意思呢?这里涉及到 Spring 框架生命周期的一个细节,我打算单独介绍一下 。
推荐阅读
- 什么是编译器?什么是集成开发环境?一文讲明白
- 并使用java实现 一文看懂Base64原理
- 一文看懂MySQL中基于XA实现的分布式事务
- 一文搞懂Python字符编码问题,值得收藏
- 一文搞懂 PHP 数组的真正用法
- 一文读懂,“算法”究竟是个啥?
- 看了这一文,你可以彻底认识锂电池的使用了
- 一文看懂mysql数据库监控指标--缓冲池使用情况
- Dubbo+Zookeeper的RPC远程调用框架demo
- 你知道 Redis数据结构底层实现吗?一文详解,彻底弄懂