Dubbo ShutdownHook 优雅停机整理

Dubbo优雅停机的机制

Dubbo是通过JDK的ShutdownHook来完成优雅停机的
所以如果用户使用 kill -9 PID 等强制关闭命令,是不会执行优雅停机的,只有通过 kill PID时,才会执行

Dubbo 中实现的优雅停机机制主要包含6个步骤:
(1)收到 kill PID 进程退出信号,Spring 容器会触发容器销毁事件。
(2)provider 端会注销服务元数据信息(删除ZK节点)。
(3)consumer 会拉取最新服务提供者列表。
(4)provider 会发送 readonly 事件报文通知 consumer 服务不可用。
(5)服务端等待已经执行的任务结束并拒绝新任务执行。


Dubbo ShutdownHook 优雅停机整理_第1张图片
Dubbo优雅停机机制

Spring 容器下 Dubbo 的优雅停机

由于现在大多数开发者选择使用 Spring 构建 Dubbo 应用,Spring 框架本身也依赖于 shutdown hook 执行优雅停机,并且与 Dubbo 的优雅停机会并发执行,而 Dubbo 的一些 Bean 受 Spring 托管,当 Spring 容器优先关闭时,会导致 Dubbo 的优雅停机流程无法获取相关的 Bean 而报错,从而优雅停机失效。Dubbo 开发者们迅速意识到了 shutdown hook 并发执行的问题,开始了一系列的补救措施。

Dubbo 2.6.3 中新增了ShutdownHookListener类

    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();
            }
        }
    }

Spring 先发布ContextClosedEvent事件,调用关闭 Dubbo 应用的钩子,然后再关闭自身的 Spring 应用。从而解决了上述因 Spring 钩子早于 Dubbo 钩子执行导致 Dubbo 优雅停机失效的问题。

dubbo 2.6.3 版本,也有缺点,因为它仍然保留了原先的 Dubbo 注册 JVM 关闭钩子,只是这个钩子的报错不会影响 Spring 钩子中关闭 Dubbo 应用的执行,因为它们是两个独立的线程。但是 Dubbo 注册 JVM 关闭钩子的操作难免有点多余,所以网上能见到类似remove dubbo JVM 钩子的方案。

/**
 * @author liuliu
 * this working for dubbo 2.6.+
 */
@Configuration
public class ShutdownHookListener implements ApplicationListener {
    
    private static final Logger log = LoggerFactory.getLogger(ShutdownHookListener.class);
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationStartedEvent) {
            Runtime.getRuntime().removeShutdownHook(DubboShutdownHook.getDubboShutdownHook());
            log.info("dubbo default shutdown hook removed,will be managed by spring");
        } else if (event instanceof ContextClosedEvent) {
            log.info("start destroy dubbo on spring close event");
            DubboShutdownHook.getDubboShutdownHook().destroyAll();
            log.info("dubbo destroy finished");
        }
    }
}

Dubbo 2.7 方案

在 dubbo 2.7.x 版本中,通过SpringExtensionFactory类移除了该操作。

public class SpringExtensionFactory implements ExtensionFactory {
    public static void addApplicationContext(ApplicationContext context) {
        CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
        BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);
    }
}

该方案完美的解决了上述并发钩子问题,直接取消掉 Dubbo 的 JVM 的钩子。

业务线程池如何优雅关闭?

线程池的错误关闭很有可能会造成,dubbo的优雅关机无法关闭。所以在关闭线程池的时候要注意,下面就介绍下错误的和正确的做法。
错误的做法:

Runtime.getRuntime().addShutdownHook(new Thread(){
// 线程池虽然关闭,但是队列中的任务任然继续执行,所以用 shutdown()方式关闭线程池时需要考虑是否是你想要的效果
//如果希望立即停止,抛弃队列中的任务,可以使用shutdownNow()
threadPoolExecutor.shutdown();
});

上面的代码忽视了多个钩子函数是并发执行的问题,线程池的业务逻辑可能需要数据源链接、redis链接等,但是这个时候有可能数据源已经关闭了。
正确的做法:

    @PostConstruct
    public void afterPropertiesSet() throws Exception {
    DEAL_EVENT_THREAD_POOL = ThreadPoolUtils.newExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
                QUEUE_MAX_SIZE, "DealEventLogTask-service");
    }

    @PreDestroy
    public void destroy() throws Exception {
        ThreadPoolUtils.stop(DEAL_EVENT_THREAD_POOL);
    }

文章参考自:
https://www.jianshu.com/p/ebb83e6d35fc
https://www.jianshu.com/p/aa22eac09d8c
https://blog.csdn.net/liubenlong007/article/details/79958076

你可能感兴趣的:(Dubbo ShutdownHook 优雅停机整理)