Dubbo启动、下线过程,以及优雅下线解决方案

   服务重启时带来的问题 

        项目分布式服务场景中,系统之间通过RPC服务方式进行交互。经常在服务提供方provider服务重启或者发布的过程中,如果此时业务正处于高峰期,就会有大量的RPC调用失败。如果consumer侧没有重试机制就会发生业务异常。

        本文基于业务流程的角度,记录一下dubbo启动、下线过程。后面会给予源码的维度,详细记录启动、下线过程。

dubbo启动

         对于provider,dubbo会监听Spring容器启动的刷新事件(ContextRefreshedEvent),调用Export暴露服务。

provider

  1. URL装配:读取provider配置,封装URL。
  2. 协议暴露:创建NettyServer,为URL创建一个本地方法的代理,并将二者映射。NettyServer接受请求,就会调用对应的本地方法
  3. 向注册中心注册:将包装好的URL信息,注册到注册中心,完成服务暴露

consumer

  1. 想注册中心注册Consummer信息
  2. 创建监听器,订阅provider节点的信息变化。
    1. 更新内存Provider列表:provider上下线,引起zk节点变化。监听器感知变化后,会调用NotifyListener.notify方法,更新内存provider列表。
    2. 更新本地文件缓存:consumer还会讲最新的provider列表写入到~/.dubbo的文件目录下,这保证Zk挂掉的情况下,consumer依然能够通过本地缓存文件找到provider地址。

dubbo下线

        服务下线过程中,有两处代码来处理dubbo下线。

  1.  ServiceBean的Destory,由Spring销毁Bean的时候调用。
  2. AbstractConfig中的DubboShutdownHook,是JVM退出时的钩子线程,在JVM退出之前执行。

ServiceBean.Destory

provider

  1.         删除zk中的provider节点信息

        对于provider来讲,就是删除zk中的provider节点。这样consumer监听到后,就会删除内存和本地文件中的provider列表,新的RPC就不会调用删除的provider了。

consumer

        destroy方法是上面订阅的逆过程

  1. 关闭监听器
  2. 删除zk中的consumer节点信息

AbstractConfig的DubboShutdownHook

        AbstractConfig类中静态代码块,然后将DubboShutdownHook类,注册到虚拟机关闭钩子中,当虚拟机关闭时,就会调用对应的钩类。

Java虚拟机会关闭以响应两种类型的事件:

当最后一个非守护进程线程退出或调用exit(相当于System.exit)方法时,程序正常退出,或者

虚拟机在响应用户中断(如键入^C)或系统范围事件(如用户注销或系统关闭)时终止。

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");
        DubboShutdownHook.getDubboShutdownHook().register();
    }
public void register() {
        if (!this.registered.get() && this.registered.compareAndSet(false, true)) {
            Runtime.getRuntime().addShutdownHook(getDubboShutdownHook());
        }

    }

        当JVM关闭时,回调用DubboShutdownHook类中的doDestroy-》调用AbstractRegistryFactroy.destroyAll()【调用zkClient的关闭方法】-》调用destroyProtocols方法,遍历所有Protocol协议类,调用Protocol的destroy方法。

private void destroyProtocols() {
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        Iterator var2 = loader.getLoadedExtensions().iterator();

        while(var2.hasNext()) {
            String protocolName = (String)var2.next();

            try {
                Protocol protocol = (Protocol)loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable var5) {
                logger.warn(var5.getMessage(), var5);
            }
        }

    }

        因为我们使用Dubbo协议,DubboProtocol的destroy方法如下

public void destroy() {
        Iterator var1 = (new ArrayList(this.serverMap.keySet())).iterator();

        String key;
        //循环遍历server,调用server.close。关闭provider
        while(var1.hasNext()) {
            key = (String)var1.next();
            ExchangeServer server = (ExchangeServer)this.serverMap.remove(key);
            if (server != null) {
                try {
                    if (this.logger.isInfoEnabled()) {
                        this.logger.info("Close dubbo server: " + server.getLocalAddress());
                    }

                    server.close(ConfigUtils.getServerShutdownTimeout());
                } catch (Throwable var7) {
                    this.logger.warn(var7.getMessage(), var7);
                }
            }
        }

        var1 = (new ArrayList(this.referenceClientMap.keySet())).iterator();

        ExchangeClient client;
        //循环遍历consumer,调用client.close。关闭consumer
        while(var1.hasNext()) {
            key = (String)var1.next();
            client = (ExchangeClient)this.referenceClientMap.remove(key);
            if (client != null) {
                try {
                    if (this.logger.isInfoEnabled()) {
                        this.logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
                    }

                    client.close(ConfigUtils.getServerShutdownTimeout());
                } catch (Throwable var6) {
                    this.logger.warn(var6.getMessage(), var6);
                }
            }
        }

        var1 = (new ArrayList(this.ghostClientMap.keySet())).iterator();

        while(var1.hasNext()) {
            key = (String)var1.next();
            client = (ExchangeClient)this.ghostClientMap.remove(key);
            if (client != null) {
                try {
                    if (this.logger.isInfoEnabled()) {
                        this.logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
                    }

                    client.close(ConfigUtils.getServerShutdownTimeout());
                } catch (Throwable var5) {
                    this.logger.warn(var5.getMessage(), var5);
                }
            }
        }

        this.stubServiceMethodsMap.clear();
        super.destroy();
    }

        DubboProtocol的destroy():先关闭provider,再关闭consumer,如果先关闭consumer后关闭provider,那么上游服务的请求依然能够被provider处理,如果provider依赖consummer,会导致调用链路失败。

关闭Provider代码:

    public void close(int timeout) {
        this.startClose();
        if (timeout > 0) {
            long max = (long)timeout;
            long start = System.currentTimeMillis();
            if (this.getUrl().getParameter("channel.readonly.send", true)) {
                //发送readonly信号
                this.sendChannelReadOnlyEvent();
            }

            //等待任务正在运行的任务执行完成
            while(this.isRunning() && System.currentTimeMillis() - start < max) {
                try {
                    Thread.sleep(10L);
                } catch (InterruptedException var7) {
                    this.logger.warn(var7.getMessage(), var7);
                }
            }
        }
        //停止与consumer的心跳
        this.doClose();
        //关闭NettyServer
        this.server.close(timeout);
    }

关闭Consumer代码:

    public void close(int timeout) {
        this.startClose();
        //停止与provider心跳
        this.doClose();
        //关闭NettyClient
        this.channel.close(timeout);
    }

具体步骤如下

  1. 遍历关闭provider:
    1. 发送readonly信号:调用HeaderExchangerServer.sendChannelReadOnlyEvent(),向consumer发送readonly信号,目的告诉consumer不要想我发送请求。由于consumer在Zk挂掉的情况下依然可以读取本地的provider,readonly信号的存在为consumer提供了另一种剔除provider的方式
    2. 等待正在运行的任务执行完毕或者超时:while循环,等待正在执行的任务完成或超时
    3. 停止与consumer的心跳
    4. 关闭NettyServer.
  2. 遍历关闭consumer:
    1. 停止与provider心跳
    2. 关闭NettyClient

优雅下线

下线问题

        2.4.9版本中,NettyClient 中创建ChannelFactory构造器中,创建了一个HashEdWheelTimer的非Daemon线程。在dubbo销毁过程中,没有显示释放HashedWheelTimer线程,导致Jvm无法正常退出,导致DubboShutdown没有被执行。

        总而言之,再JVM停止时,没有执行到DubboShutdownHook,优雅下线根本没有执行。导致没有按照dubbo涉及所期待的那样与运行。

优雅下线方法

方法1

        Provider摘除节点后,consumer收到通知更新provider列表这两步并不是同步的原子操作。可能Provider摘除节点,检测没有进行中的调用后,立马关闭服务,consumer还未来得及更新provider列表。导致上游consumer调用失败。

        provider摘除节点后,需要给consumer足够的时间更新服务列表。简单的解决方式provider摘除ZK节点之后,销毁协议之前,主动sleep一段时间。从而减少consumer调用失败的概率。

方法2

        Provider对外关闭暴露,并且已有任务执行成功后。不应该里面关闭consumer,即client.close ()。考虑到业务中有异步或者定时任务调用consumer,立即关闭可能导致这部分业务失败。所以可以在调用client.close()增加等待时间。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

菜鸟long

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

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

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

打赏作者

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

抵扣说明:

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

余额充值