一、前言

在 『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 notification    try {        Thread.sleep(ConfigUtils.getServerShutdownTimeout());    } catch (InterruptedException e) {        logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");    }    ExtensionLoader 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 优雅停机主要分为两步:

  1. 注销注册中心
  2. 注销所有 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 notificationtry {    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 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(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(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(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 closepublic 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 CONTEXTS = new ConcurrentHashSet();    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 protocol port 消费者端_Dubbo 优雅停机演进之路相关推荐

  1. dubbo protocol port 消费者端_Dubbo源码:搞定URL,就走完了进度条的一半

    Dubbo 中的 URL 大家都知道,在互联网领域,每个信息资源都有统一的且在网上唯一的地址,该地址就叫 URL(Uniform Resource Locator,统一资源定位符),它是互联网的统一资 ...

  2. dubbo protocol port 消费者端_企业级 SpringBoot 与 Dubbo 的并用

    点击上方"匠心零度",选择"设为星标" 做积极的人,而不是积极废人 作者:SimpleWu cnblogs.com/SimpleWu/p/10833555.ht ...

  3. dubbo protocol port 消费者端_springboot整合dubbo设置全局唯一ID进行日志追踪

    击上方蓝色"程序员白楠楠",选择"设为星标" 作者:松下听泉 出处:https://blog.csdn.net/weixin_39427718 1.新建项目 利 ...

  4. dubbo 无法访问消费端_Dubbo最佳实践,我整理了以下9点

    Dubbo服务化,在当前互联网后端开发中,大部分都使用了Dubbo.截止目前github dubbo上,star也将近3万,使用dubbo的公司数量也很可观,Dubbo确实也是一个比较不错的服务化框架 ...

  5. 淘宝服务端并发分布式架构演进之路

    -     前言    - 本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构 ...

  6. 手机淘宝移动端接入网关基础架构演进之路

    http://www.infoq.com/cn/articles/taobao-mobile-terminal-access-gateway-infrastructure 移动网络优化是超级App永恒 ...

  7. 阿里无线11.11 | 手机淘宝移动端接入网关基础架构演进之路

    移动网络优化是超级App永恒的话题,对于无线电商来说更为重要,网络请求体验跟用户的购买行为息息相关,手机淘宝从过去的HTTP API网关,到2014年升级支持SPDY,2015年双十一自研高性能.全双 ...

  8. 讲一讲移动端跨平台技术的演进之路

    /   今日科技快讯   / 近日,爱奇艺会员及海外业务群总裁杨向华对外公开表示,其正在酝酿会员费用上涨,不排除会员率先提价,但并无时间表.爱奇艺宣称要涨价背后是其业绩长期亏损,而且内容成本不断攀升, ...

  9. Dubbo 提供者和消费者

    一.说明 Dubbo官方建议将服务接口.服务模型.服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP). ...

最新文章

  1. PE文件结构 - NT头学习
  2. java的标量和聚合量_第5节:Java基础 - 必知必会(下)
  3. DBUtils (30)
  4. 关于char[]转换成LPCWSTR的有关问题
  5. 动态代理的概述和实现
  6. stm32F1和stm32F4的区别
  7. Spring boot(九):定时任务
  8. 怎么判断机械硬盘要多大_秋天要多吃芋头,买芋头是买大的还是小的?学学广西大妈怎么买...
  9. x390拆机_用了七八年的笔记本电脑依然流畅如初,从X230i换到X390
  10. vmware下Ubuntu屏幕分辨率设置
  11. iOS开发之获取LaunchImage启动图
  12. 树莓派 4B 配置 Ubuntu20.04 和 ROS2
  13. 算法:判断是否是循环链表,并返回循环链表开始节点Linked List Cycle II
  14. 安卓版有道词典的离线词库-《21世纪大英汉词典》等_我是亲民_新浪博客
  15. 汉庭董事长季琦:成功创业者的必经之路
  16. 【转载】CodeWarrior IDE使用Tips-如何编译生成和调用静态库
  17. Python基础操作(2)
  18. 浅谈知识表示之语义网络、RDF和OWL
  19. [生活] 领带的打法
  20. 阅读笔记 |《上帝掷骰子吗:量子物理史话》曹天元

热门文章

  1. 【Git基础笔记】常用命令
  2. 风好大,我好冷——个人分工理解
  3. electron 的窗口设置最大化 最小化
  4. Linked List Two Finish
  5. 数据结构之线性表之顺序存储结构(3)
  6. Cocos2d-x——导入Cocostudio资源
  7. Signaltap的使用
  8. Django之模板层
  9. 终极版Servlet——我只能提示您路过别错过
  10. Element-ui框架Tree树形控件切换高亮显示选中效果