Dubbo 优雅停机演进之路
一、前言
在 『shutdownhook- java 优雅停机解决方案』 一文中我们聊到了 java 实现优雅停机原理。接下来我们就跟根据上面知识点,深入 dubbo 内部,去了解一下 dubbo 如何实现优雅停机。
二、dubbo 优雅停机待解决的问题
为了实现优雅停机,dubbo 需要解决一些问题:
- 新的请求不能再发往正在停机的 dubbo 服务提供者。
- 若关闭服务提供者,已经接收到服务请求,需要处理完毕才能下线服务。
- 若关闭服务消费者,已经发出的服务请求,需要等待响应返回。
解决以上三个问题,才能使停机对业务影响降低到最低,做到优雅停机。
三、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 notification try { 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); // 再注销 protocol for (string protocolname : loader.getloadedextensions()) { try { protocol protocol = loader.getloadedextension(protocolname); if (protocol != null) { protocol.destroy(); } } catch (throwable t) { logger.warn(t.getmessage(), t); } } }
从上面可以看到,dubbo 优雅停机主要分为两步:
- 注销注册中心
- 注销所有
protocol
3.2、注销注册中心
注销注册中心源码如下:
public static void destroyall() { if (logger.isinfoenabled()) { logger.info("close all registries " + getregistries()); } // lock up the registry shutdown process lock.lock(); try { for (registry registry : getregistries()) { try { registry.destroy(); } catch (throwable e) { logger.error(e.getmessage(), e); } } registries.clear(); } finally { // release the lock lock.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
子类,分别为 dubboprotocol
与 injvmprotocol
。
dubboprotocol
用与服务端请求交互,而 injvmprotocol
用于内部请求交互。如果应用调用自己提供 dubbo 服务,不会再执行网络调用,直接执行内部方法。
这里我们主要来分析一下 dubboprotocol
内部逻辑。
dubboprotocol#destroy
源码:
public void destroy() { // 关闭 server for (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); } } } // 关闭 client for (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 作为其底层的通讯框架,分为 server
与 client
。server
用于接收其他消费者 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 { @override public 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 优雅停机
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客: