Dubbo ShutdownHook 优雅停机整理


 

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优雅停机机制

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