一、前言

在 『ShutdownHook- Java 优雅停机解决方案』 一文中我们聊到了 Java 实现优雅停机原理。接下来我们就跟根据上面知识点,深入 Dubbo 内部,去了解一下 Dubbo 如何实现优雅停机。

二、Dubbo 优雅停机待解决的问题

为了实现优雅停机,Dubbo 需要解决一些问题:

  1. 新的请求不能再发往正在停机的 Dubbo 服务提供者。
  2. 若关闭服务提供者,已经接收到服务请求,需要处理完毕才能下线服务。
  3. 若关闭服务消费者,已经发出的服务请求,需要等待响应返回。

解决以上三个问题,才能使停机对业务影响降低到最低,做到优雅停机。

三、2.5.X

Dubbo 优雅停机在 2.5.X 版本实现比较完整,这个版本的实现相对简单,比较容易理解。所以我们先以 Dubbo 2.5.X 版本源码为基础,先来看一下 Dubbo 如何实现优雅停机。

3.1、优雅停机总体实现方案

优雅停机入口类位于 AbstractConfig 静态代码中,源码如下:

static {Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {public void run() {if (logger.isInfoEnabled()) {logger.info("Run shutdown hook now.");}ProtocolConfig.destroyAll();}}, "DubboShutdownHook"));
}

这里将会注册一个 ShutdownHook ,一旦应用停机将会触发调用 ProtocolConfig.destroyAll()

ProtocolConfig.destroyAll()源码如下:

public static void destroyAll() {// 防止并发调用if (!destroyed.compareAndSet(false, true)) {return;}// 先注销注册中心AbstractRegistryFactory.destroyAll();// Wait for registry notificationtry {Thread.sleep(ConfigUtils.getServerShutdownTimeout());} catch (InterruptedException e) {logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");}ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);// 再注销 Protocolfor (String protocolName : loader.getLoadedExtensions()) {try {Protocol protocol = loader.getLoadedExtension(protocolName);if (protocol != null) {protocol.destroy();}} catch (Throwable t) {logger.warn(t.getMessage(), t);}}}

从上面可以看到,Dubbo 优雅停机主要分为两步:

  1. 注销注册中心
  2. 注销所有 Protocol

3.2、注销注册中心

注销注册中心源码如下:

public static void destroyAll() {if (LOGGER.isInfoEnabled()) {LOGGER.info("Close all registries "   getRegistries());}// Lock up the registry shutdown processLOCK.lock();try {for (Registry registry : getRegistries()) {try {registry.destroy();} catch (Throwable e) {LOGGER.error(e.getMessage(), e);}}REGISTRIES.clear();} finally {// Release the lockLOCK.unlock();}
}

这个方法将会将会注销内部生成注册中心服务。注销注册中心内部逻辑比较简单,这里就不再深入源码,直接用图片展示。

ps: 源码位于:AbstractRegistry

以 ZK 为例,Dubbo 将会删除其对应服务节点,然后取消订阅。由于 ZK 节点信息变更,ZK 服务端将会通知 dubbo 消费者下线该服务节点,最后再关闭服务与 ZK 连接。

通过注册中心,Dubbo 可以及时通知消费者下线服务,新的请求也不再发往下线的节点,也就解决上面提到的第一个问题:新的请求不能再发往正在停机的 Dubbo 服务提供者。

但是这里还是存在一些弊端,由于网络的隔离,ZK 服务端与 Dubbo 连接可能存在一定延迟,ZK 通知可能不能在第一时间通知消费端。考虑到这种情况,在注销注册中心之后,加入等待进制,代码如下:

// Wait for registry notification
try {Thread.sleep(ConfigUtils.getServerShutdownTimeout());
} catch (InterruptedException e) {logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
}

默认等待时间为 10000ms,可以通过设置 dubbo.service.shutdown.wait 覆盖默认参数。10s 只是一个经验值,可以根据实际情设置。不过这个等待时间设置比较讲究,不能设置成太短,太短将会导致消费端还未收到 ZK 通知,提供者就停机了。也不能设置太长,太长又会导致关停应用时间边长,影响发布体验。

3.3、注销 Protocol

ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {try {Protocol protocol = loader.getLoadedExtension(protocolName);if (protocol != null) {protocol.destroy();}} catch (Throwable t) {logger.warn(t.getMessage(), t);}
}

loader#getLoadedExtensions 将会返回两种 Protocol 子类,分别为 DubboProtocolInjvmProtocol

DubboProtocol 用与服务端请求交互,而 InjvmProtocol 用于内部请求交互。如果应用调用自己提供 Dubbo 服务,不会再执行网络调用,直接执行内部方法。

这里我们主要来分析一下 DubboProtocol 内部逻辑。

DubboProtocol#destroy 源码:

public void destroy() {// 关闭 Serverfor (String key : new ArrayList<String>(serverMap.keySet())) {ExchangeServer server = serverMap.remove(key);if (server != null) {try {if (logger.isInfoEnabled()) {logger.info("Close dubbo server: "   server.getLocalAddress());}server.close(ConfigUtils.getServerShutdownTimeout());} catch (Throwable t) {logger.warn(t.getMessage(), t);}}}// 关闭 Clientfor (String key : new ArrayList<String>(referenceClientMap.keySet())) {ExchangeClient client = referenceClientMap.remove(key);if (client != null) {try {if (logger.isInfoEnabled()) {logger.info("Close dubbo connect: "   client.getLocalAddress()   "-->"   client.getRemoteAddress());}client.close(ConfigUtils.getServerShutdownTimeout());} catch (Throwable t) {logger.warn(t.getMessage(), t);}}}for (String key : new ArrayList<String>(ghostClientMap.keySet())) {ExchangeClient client = ghostClientMap.remove(key);if (client != null) {try {if (logger.isInfoEnabled()) {logger.info("Close dubbo connect: "   client.getLocalAddress()   "-->"   client.getRemoteAddress());}client.close(ConfigUtils.getServerShutdownTimeout());} catch (Throwable t) {logger.warn(t.getMessage(), t);}}}stubServiceMethodsMap.clear();super.destroy();
}

Dubbo 默认使用 Netty 作为其底层的通讯框架,分为 ServerClientServer 用于接收其他消费者 Client 发出的请求。

上面源码中首先关闭 Server ,停止接收新的请求,然后再关闭 Client。这样做就降低服务被消费者调用的可能性。

3.4、关闭 Server

首先将会调用 HeaderExchangeServer#close,源码如下:

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)) {// 发送 READ_ONLY 事件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 void doClose() {if (!closed.compareAndSet(false, true)) {return;}stopHeartbeatTimer();try {scheduled.shutdown();} catch (Throwable t) {logger.warn(t.getMessage(), t);}
}

这里将会向服务消费者发送 READ_ONLY 事件。消费者接受之后,主动排除这个节点,将请求发往其他正常节点。这样又进一步降低了注册中心通知延迟带来的影响。

接下来将会关闭心跳检测,关闭底层通讯框架 NettyServer。这里将会调用 NettyServer#close 方法,这个方法实际在 AbstractServer 处实现。

AbstractServer#close 源码如下:

public void close(int timeout) {ExecutorUtil.gracefulShutdown(executor, timeout);close();
}

这里首先关闭业务线程池,这个过程将会尽可能将线程池中的任务执行完毕,再关闭线程池,最后在再关闭 Netty 通讯底层 Server。

Dubbo 默认将会把请求/心跳等请求派发到业务线程池中处理。

关闭 Server,优雅等待线程池关闭,解决了上面提到的第二个问题:若关闭服务提供者,已经接收到服务请求,需要处理完毕才能下线服务。

Dubbo 服务提供者关闭流程如图:

ps:为了方便调试源码,附上 Server 关闭调用联。

DubboProtocol#destroy->HeaderExchangeServer#close->AbstractServer#close->NettyServer#doClose

3.5 关闭 Client

Client 关闭方式大致同 Server,这里主要介绍一下处理已经发出请求逻辑,代码位于HeaderExchangeChannel#close

// graceful close
public void close(int timeout) {if (closed) {return;}closed = true;if (timeout > 0) {long start = System.currentTimeMillis();// 等待发送的请求响应信息while (DefaultFuture.hasFuture(channel)&& System.currentTimeMillis() - start < timeout) {try {Thread.sleep(10);} catch (InterruptedException e) {logger.warn(e.getMessage(), e);}}}close();
}

关闭 Client 的时候,如果还存在未收到响应的信息请求,将会等待一定时间,直到确认所有请求都收到响应,或者等待时间超过超时时间。

ps:Dubbo 请求会暂存在 DefaultFuture Map 中,所以只要简单判断一下 Map 就能知道请求是否都收到响应。

通过这一点我们就解决了第三个问题:若关闭服务消费者,已经发出的服务请求,需要等待响应返回。

Dubbo 优雅停机总体流程如图所示。

ps: Client 关闭调用链如下所示:

DubboProtocol#close->ReferenceCountExchangeClient#close->HeaderExchangeChannel#close->AbstractClient#close

四、2.7.X

Dubbo 一般与 Spring 框架一起使用,2.5.X 版本的停机过程可能导致优雅停机失效。这是因为 Spring 框架关闭时也会触发相应的 ShutdownHook 事件,注销相关 Bean。这个过程若 Spring 率先执行停机,注销相关 Bean。而这时 Dubbo 关闭事件中引用到 Spring 中 Bean,这就将会使停机过程中发生异常,导致优雅停机失效。

为了解决该问题,Dubbo 在 2.6.X 版本开始重构这部分逻辑,并且不断迭代,直到 2.7.X 版本。

新版本新增 ShutdownHookListener,继承 Spring ApplicationListener 接口,用以监听 Spring 相关事件。这里 ShutdownHookListener 仅仅监听 Spring 关闭事件,当 Spring 开始关闭,将会触发 ShutdownHookListener 内部逻辑。


public class SpringExtensionFactory implements ExtensionFactory {private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();private static final ApplicationListener SHUTDOWN_HOOK_LISTENER = new ShutdownHookListener();public static void addApplicationContext(ApplicationContext context) {CONTEXTS.add(context);if (context instanceof ConfigurableApplicationContext) {// 注册 ShutdownHook((ConfigurableApplicationContext) context).registerShutdownHook();// 取消 AbstractConfig 注册的 ShutdownHook 事件DubboShutdownHook.getDubboShutdownHook().unregister();}BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);}// 继承 ApplicationListener,这个监听器将会监听容器关闭事件private static class ShutdownHookListener implements ApplicationListener {@Overridepublic void onApplicationEvent(ApplicationEvent event) {if (event instanceof ContextClosedEvent) {DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();shutdownHook.doDestroy();}}}
}

当 Spring 框架开始初始化之后,将会触发 SpringExtensionFactory 逻辑,之后将会注销 AbstractConfig 注册 ShutdownHook,然后增加 ShutdownHookListener。这样就完美解决上面『双 hook』 问题。

五、最后

优雅停机看起来实现不难,但是里面设计细枝末节却非常多,一个点实现有问题,就会导致优雅停机失效。如果你也正在实现优雅停机,不妨参考一下 Dubbo 的实现逻辑。

Dubbo 系列文章推荐

1.如果有人问你 Dubbo 中注册中心工作原理,就把这篇文章给他
2.不知道如何实现服务的动态发现?快来看看 Dubbo 是如何做到的
3.Dubbo Zk 数据结构
4.缘起 Dubbo ,讲讲 Spring XML Schema 扩展机制

帮助文章

1、强烈推荐阅读 kirito 大神文章:一文聊透 Dubbo 优雅停机

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

Dubbo 优雅停机演进之路相关推荐

  1. dubbo protocol port 消费者端_Dubbo 优雅停机演进之路

    一.前言 在 『ShutdownHook- Java 优雅停机解决方案』 一文中我们聊到了 Java 实现优雅停机原理.接下来我们就跟根据上面知识点,深入 Dubbo 内部,去了解一下 Dubbo 如 ...

  2. 一文聊透 Dubbo 优雅停机

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 作者 | kiritomoe 来源 | 公众号「Kirito的技术分享」 1 前言 一年之前 ...

  3. 处于停机等非正常状态_一文聊透 Dubbo 优雅停机

    1 前言 一年之前,我曾经写过一篇<研究优雅停机时的一点思考>,主要介绍了 kill -9,kill -15 两个 Linux 指令的含义,并且针对性的聊到了 Spring Boot 应用 ...

  4. Dubbo 优雅停机

    背景 对于任何一个线上应用,如何在服务更新部署过程中保证客户端无感知是开发者必须要解决的问题,即从应用停止到重启恢复服务这个阶段不能影响正常的业务请求.理想条件下,在没有请求的时候再进行更新是最安全可 ...

  5. Java应用的优雅停机

    一. 优雅停机的概念 优雅停机一直是一个非常严谨的话题,但由于其仅仅存在于重启.下线这样的部署阶段,导致很多人忽视了它的重要性,但没有它,你永远不能得到一个完整的应用生命周期,永远会对系统的健壮性持怀 ...

  6. 知乎Redis的演进之路:从单机到2000万QPS的挑战

    按:对于业务技术而言,技术是什么?深刻理解业务的本质,掌握技术底层原理.并合理应用.中间件就是其中支点,作为中间件一员的Redis产品,是如何演进的?与业务系统有何不同? 本文来自知乎陈鹏老师的精彩分 ...

  7. 研究优雅停机时的一点思考

    作者:徐靖峰 来源:Kirito的技术分享 开头先废话几句,有段时间没有更新博客了,除了公司项目比较忙之外,还有个原因就是开始思考如何更好地写作. 远的来说,我从大一便开始在 CSDN 上写博客,回头 ...

  8. shutdown thread.java_ShutdownHook- Java 优雅停机解决方案

    想象一下,如果你现在刚好在 word 上写需求文档,电脑突然重启.等待开机完成,你可能会发现写了一个小时文档没有保存,就这么没了... 一个正在运行 Java 应用如果突然将其停止,影响不止数据丢失, ...

  9. 干货 | 携程度假无线前端架构演进之路

    作者简介 Jade Gu,携程高级前端开发专家,负责度假前端框架设计和 Node.js 基础设施建设等工作. 这篇文章将简略地介绍我们当前的无线前端架构设计及其演进之路.主要内容包含以下几个部分,希望 ...

  10. 一张图来看看.NETCore和前后端技术的演进之路

    一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和前后端分离那些事儿>上,我们曾经试图通过一系列抽丝剥茧的过程来引导大家在这条基于.NET Core ...

最新文章

  1. 柱状图中xy轴怎么出现_烤烟烘烤中出现叶片发霉怎么办?
  2. pytorch--nn.Sequential学习
  3. 聊天工具简单实现(python 半双工聊天)
  4. C语言中变量的链接属性
  5. DOM获取元素位置的三大系列offset/scroll/client
  6. 3.通道 Channel
  7. IntelliJ IDEA中的神仙插件
  8. java中ajax由哪些组成,java中ajax
  9. git刷新分支列表_如何使用Git小技巧让你开发协作过程更加顺利?
  10. 删除整个目录(API)
  11. 智能合约漏洞检测工具mythril使用
  12. linux huge模式设置,Linux HugePages 配置步骤
  13. 百度的搜索引擎相关技术的分析
  14. php如何做防抖,Vue中怎么对事件进行防抖和节流操作?
  15. 华为使用计算机投屏要打开什么,华为Mate20手机怎么投屏到电脑上呢
  16. 构建自定义安全令牌服务
  17. draftsight linux 32,DraftSight停止提供Linux版:所有免费版将于2019年12月31日后停止运行...
  18. python统计英文文章中单词出现的次数并排序_Python实现的统计文章单词次数功能示例...
  19. 在stm32f103c8上移植nuttx系统
  20. 硬币排成线-LintCode

热门文章

  1. 公网服务器与局域网手机长连接
  2. .net,微软,薪资及其他
  3. c html 转 pdf,HTML 转 PDF
  4. 宁夏计算机科学与技术产业发展新趋势,2021年CCF数据库发展战略研讨会在宁夏银川顺利召开...
  5. 拓端tecdat|数据观察“双十一”网购新常态
  6. 拓端tecdat|Excel实例:数组公式和函数
  7. 拓端tecdat|R语言ggsurvplot绘制生存曲线报错 : object of type ‘symbol‘ is not subsettable
  8. 机器人弹古筝图片_除了百度,还有这些搜索引擎哦:深网搜索引擎「第二弹」...
  9. visual studio code安装
  10. TensorFlow 和keras有什么区别?