DUBBO优雅停机

先说结论

现在相对完美的 dubbo 优雅停机方案是 qos + dubbo 自身优雅停机

qos 接入

前置条件:dubbo >= 2.5.8, netty 4

简而言之,通过 qos 提供的接口,在停机前先调用 qos 提供的 offline 接口下线所有服务 官方文档

和运维同学沟通了下,现在新的 dubbo 应用默认都接入了 qos, 旧的应用如果不确定的话,可以找运维同学确认下  运维接入Qos功能

如果你想监听 qos offline 事件,可以配合 qos-plus 使用 qos offline功能增强

dubbo 优雅停机

官方解释

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

最佳实践

1、低版本的 dubbo 优雅停机有很多问题,建议升级 dubbo 版本到相对稳定的版本(2.6.3进一步优化了dubbo优雅停机和 2.6.5 修复了leastactive loadbalance的预热问题) dubbo 2.5.x 升级到 2.7.15

2、Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的(高版本优化了,放到了 spring ContextClosedEvent 事件里面)

真实项目中依赖关系比较复杂,开发要保证先停掉所有 dubbo 消费的地方(比如 消费 kafka 消息然后调用 dubbo 服务,  定时任务调用 dubbo 服务等),然后再触发 dubbo 的优雅停机

4、JDK 的 ShutdownHook 的执行顺序是并发加无序执行的,建议统一放到 spring ContextClosedEvent 里面,由自己编排需要停机的任务顺序


以下是部分原理分享,有兴趣的可以看下

优雅停机流程

dubbo.service.shutdown.wait设置优雅停机等待时间,默认10s 下文中的10s为该值

  1. 从注册中心中取消注册自己,从而使消费者不要再拉取到它(client会移除该服务提供者信息)
  2. sleep (默认10s) 等到服务消费接收到注册中心通知到该服务提供者已经下线 (2.6.x移除了该功能)
  3. 优先关闭自身对外提供的服务,然后关闭外部的引用,最后关闭幽灵链接(ghostClient)

关闭自身对外提供的服务流程

  1. sendChannelReadOnlyEvent对所有的consumer广播READONLY事件告诉consumer我要下线了不要调用我了
  2. 判断连接是否还存在(2.6.2+以后逻辑,之前版本判断 CHANNELS map中是否存在chanel) && 超过timeout (10s)
  3. 停止心跳
  4. 关闭server

关闭server流程

  1. shutdown io 线程池
  2. 等待线程池中任务执行(最多10s)shutdownNow 强制终止io线程
  3. 关闭tcp连接

2.6.x优化点

  1. AnnotationBean销毁时不销毁service 让dubbo 的shutdown hook销毁该服务保证业务处理完成在销毁服务
  2. 判断连接是否存在的逻辑,就版本中判断CHANNELS map中是否存在chanel 该map会在有请求时放入map,请求完成后移除,会存在请求来了还未放入map中的情况,修改为channel.isConnected() 判断这个连接还连着,就不处理等待超时
  3. 为什么移除从注册中心下线后 等待时间; 3.1. 关闭自身服务时会广播所有消费者不要给我发消息了 3.2. 关闭服务本身会有等待时间

整合spring

dubbo 的优雅停机和 spring 的关闭都是通过 shutdownhook 实现,但因为 shutdownhook 是 并发 + 无序 执行的,理想状态下希望 dubbo 在 spring 之前关闭

dubbo 从 2.6x 开始逐渐对优雅停机进行优化,加入对spring的支持,以下是 2.7x org.apache.dubbo.config.spring.extension.SpringExtensionFactory 的部分源码

    public static void addApplicationContext(ApplicationContext context) {
        contexts.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            // 注册 spring ShutdownHook
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            // 取消dubbo自己默认的shutdownHook
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
        BeanFactoryUtils.addApplicationListener(context, shutdownHookListener);
    }

    private static class ShutdownHookListener implements ApplicationListener {
        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ContextClosedEvent) {
                DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();
                shutdownHook.doDestroy();
            }
        }
    }

源码分析

  1. 注册 shutdownhook
    com.alibaba.dubbo.config.AbstractConfig中通过静态初始化块注册shutdownhook
    // 2.6.x
    static {
        legacyProperties.put("dubbo.protocol.name", "dubbo.service.protocol");
        legacyProperties.put("dubbo.protocol.host", "dubbo.service.server.host");
        legacyProperties.put("dubbo.protocol.port", "dubbo.service.server.port");
        legacyProperties.put("dubbo.protocol.threads", "dubbo.service.max.thread.pool.size");
        legacyProperties.put("dubbo.consumer.timeout", "dubbo.service.invoke.timeout");
        legacyProperties.put("dubbo.consumer.retries", "dubbo.service.max.retry.providers");
        legacyProperties.put("dubbo.consumer.check", "dubbo.service.allow.no.provider");
        legacyProperties.put("dubbo.service.url", "dubbo.service.address");

        Runtime.getRuntime().addShutdownHook(DubboShutdownHook.getDubboShutdownHook());
    }
    
    // com.alibaba.dubbo.config.DubboShutdownHook
    @Override
    public void run() {
        if (logger.isInfoEnabled()) {
            logger.info("Run shutdown hook now.");
        }
        destroyAll();
    }


    // 2.5.x 
    static {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            public void run() {
                if (logger.isInfoEnabled()) {
                    logger.info("Run shutdown hook now.");
                }
                ProtocolConfig.destroyAll();
            }
        }, "DubboShutdownHook"));
    }
  1. destroyAll
   // 2.6.x
   public void destroyAll() {
        if (!destroyed.compareAndSet(false, true)) {
            return;
        }
         //通过AbstractRegistryFactory.destroyAll()来“注销”在所有注册中心注册的服务,
        // 通过调用ZkClient客户端的zkClient.close()关闭ZK长连接。
        // 这样服务消费者就看不到已经被注销的服务了。
        // 当然这是理想情况。毕竟从服务提供者注销自己,到消费者发现提供者不可用中间存在一定的时间差。
        // @see com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry#destroy
        AbstractRegistryFactory.destroyAll();
        // destroy all the protocols
        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);
            }
        }
    }
    // 2.5.x
    public static void destroyAll() {
        if (!destroyed.compareAndSet(false, true)) {
            return;
        }
       
        AbstractRegistryFactory.destroyAll();

        // Wait for registry notification
        // 2.6.x去掉了下面代码
        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);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

  1. protocol.destroy() 以dubbo协议为列
 @Override
    public void destroy() {
        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);
                }
            }
        }

        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 优雅停机_程序猿DD-CSDN博客

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tudou186

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值