• Dubbo的优雅下线原理分析


    文/朱季谦

    Dubbo如何实现优雅下线?

    这个问题困扰了我一阵,既然有优雅下线这种说法,那么,是否有非优雅下线的说法呢?

    这,还真有。

    可以从linux进程关闭说起,其实,我们经常使用到杀进程的指令背后,就涉及到是否优雅下线的理念。

    在日常开发当中,经常用到kill来关掉正在运行的进程,可能你曾看到过一些文章是不推荐使用kill -9 pid的指令来删除进程。当执行该执行时,系统会发出一个SIGKILL信号给将被关掉的进程,接收到该信号的进程,都立即结束运行,假如此时内部仍有请求还没有执行完,那怎么办?你想,整个进程都被立即杀死了,线程作为进程里的某一部分,还能活吗?

    打个比方,假如你正在吃东西,物业突然打电话给你,说房子立马就要被炸掉了,你必须立马关门离开,这时,你只能把还没吃完的饭丢下,什么贵重的东西都来不及打理,立马就被迫关门跑路了。

    这样强制执行的后果,可能就会造成一些贵重东西的丢失。

    这种,就属于非优雅下线,简单,粗暴,不管三七二十一,统统停止关闭。

    一般而言,是不推荐使用kill -9 pid来强制杀死进程。

    在线上环境,用到更多的,是kill pid指令,这个指令,等同于kill -15 pid指令,因此,当你在网上看到一些介绍kill -15 pid指令时,不用纠结好像没用到过,其实,就是你用到最多的kill pid指令。使用这个指令时,系统会对pid进程发送一个SIGTERM信号,就像给pid打了一个电话,告诉他,你的房子就要到期了,麻烦快点清理好东西搬走。这时,你仍有充裕的时间,把自己的东西打包好,好好清理下房间,没问题了,再搬出去。

    换到具体程序代码中,就是执行kill pid指令后,该程序不会立马被强制关闭,而是会接受到一个通知,可以在这个通知方法内,做一些清理操作,若是Dubbo容器,则可以关闭zookeeper注册,暂停新的请求,可以把已经执行一半的请求先执行完成,等等。

    这种下线操作,就属于优雅下线。

    指令kill -15 pid是操作系统级别的优雅下线操作,那么,在具体进程当中,是如何根据SIGTERM信号来进行具体的优雅下线处理呢?

    在Dubbo官网上,关于优雅停机的操作有相关介绍:

    优雅停机

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

    原理

    服务提供方
    • 停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。
    • 然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。
    服务消费方
    • 停止时,不再发起新的调用请求,所有新的调用在客户端即报错。
    • 然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。
    设置方式

    设置优雅停机超时时间,缺省超时时间是 10 秒,如果超时则强制关闭。

    # dubbo.properties
    dubbo.service.shutdown.wait=15000
    

    如果 ShutdownHook 不能生效,可以自行调用,使用tomcat等容器部署的場景,建议通过扩展ContextListener等自行调用以下代码实现优雅停机

    ProtocolConfig.destroyAll();
    

    根据以上信息可以得知,其实Dubbo的优雅实现其实是依赖了JVM的ShutdownHook来实现的,JDK提供了一个在JVM关闭时会执行的方法,可以在该方法当中,执行ProtocolConfig.destroyAll()来实现Dubbo的优雅停机操作,而这个JDK的 ShutdownHook方法,正是在系统执行kill -15 pid时,会执行的方法,这样,我们就可以在该方法里做一些关闭前的清理工作了。

    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
               ProtocolConfig.destroyAll();
    }));
    

    这几行代码具体都实现了什么呢?

    简单而言,这里通过JDK注册了一个shutdownHook钩子函数,一旦应用停机就会触发该方法,进而执行ProtocolConfig.destroyAll()。

    这个ProtocolConfig.destroyAll()源码如下:

    public static void destroyAll() {
        //1.注销注册中心
        AbstractRegistryFactory.destroyAll();
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        Iterator var1 = loader.getLoadedExtensions().iterator();
        // 2.循环获取存活的协议
        while(var1.hasNext()) {
            String protocolName = (String)var1.next();
    
            try {
                Protocol protocol = (Protocol)loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    //关闭暴露协议
                    protocol.destroy();
                }
            } catch (Throwable var4) {
                logger.warn(var4.getMessage(), var4);
            }
        
    

    这个destroyAll()里边主要做了两件事:

    1. 首先注销注册中心,即断开与注册中心的连接,Dubbo注册到ZK的是临时节点,故而当连接断开后,临时节点及底下的数据就会被自动删除;
    2. 关闭provider和consumer暴露的协议接口,这样,新的请求就无法再继续进行;

    下面主要按照这两个模块大体介绍下其底层逻辑:

    一、注销注册中心

    public static void destroyAll() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Close all registries " + getRegistries());
        }
        //加锁,防止关闭多次
        LOCK.lock();
        try {
            Iterator var0 = getRegistries().iterator();
            //关闭所有已创建的注册中心
            while(var0.hasNext()) {
                Registry registry = (Registry)var0.next();
                try {
                    registry.destroy();
                } catch (Throwable var6) {
                    LOGGER.error(var6.getMessage(), var6);
                }
            }
            REGISTRIES.clear();
        } finally {
            //释放锁
            LOCK.unlock();
        }
    }
    

    首先获取到所有的注册中心连接,封装成迭代器模式

    Iterator var0 = getRegistries().iterator();
    

    接下来,迭代获取每一个注册连接对象进行关闭:

    registry.destroy();
    

    该destroy方法定义在接口Node当中,其具体实现将会在对应的Dubbo注册对象里:

    public interface Node {
        URL getUrl();
        boolean isAvailable();
        void destroy();
    }
    

    这里Dubbo使用的注册中心是Zookeeper,故而destroy会在ZookeeperRegistry类中具体实现:
    image

    进入到ZookeeperRegistry类,找到registry.destroy()对应的destroy()方法,可以看到,调用destroy(),其本质是关闭zk客户端连接,当客户端关闭之后,其注册到zk里的生产者或者消费者信息,都会被自动删除。

    public void destroy() {
        super.destroy();
        try {
            // 关闭zk客户端
            this.zkClient.close();
        } catch (Exception var2) {
            logger.warn("Failed to close zookeeper client " + this.getUrl() + ", cause: " + var2.getMessage(), var2);
        }
    }
    

    在这里,还有一个需要进一步研究的地方,即 super.destroy(),这个方法实现了什么功能呢?从源码当中,可以看出,其有一行这样的 this.retryFuture.cancel(true)代码,这行代码大概意思是,将失败重试取消方式设置为true,即取消了失败重试的操作,我的理解是,这里是关闭了失败重试,可以在下线过程当中,避免出现因RPC生产者接口缺少而发生反复的失败重试操作,因为到这一步,已经不需要再有失败重试的操作了。

    public void destroy() {
        //移除内存中已经注册的服务,取消所有服务订阅
        super.destroy();
        try {
            //取消失败重试
            this.retryFuture.cancel(true);
        } catch (Throwable var2) {
            this.logger.warn(var2.getMessage(), var2);
        }
    }
    

    注意一点,这里在取消失败重试机制之前,还执行了一行 super.destroy()代码,这行代码的主要功能包括两个:

    第一是移除内存中已经注册的服务,第二是取消所有服务订阅。

    我们先来看一下其方法详情:

    public void destroy() {
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Destroy registry:" + this.getUrl());
        }
        // 1.移除内存中已经注册的服务
        Set<URL> destroyRegistered = new HashSet(this.getRegistered());
        if (!destroyRegistered.isEmpty()) {
            Iterator var2 = (new HashSet(this.getRegistered())).iterator();
    
            while(var2.hasNext()) {
                URL url = (URL)var2.next();
                if (url.getParameter("dynamic", true)) {
                    try {
                        this.unregister(url);
                        if (this.logger.isInfoEnabled()) {
                            this.logger.info("Destroy unregister url " + url);
                        }
                    } catch (Throwable var10) {
                        this.logger.warn("Failed to unregister url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var10.getMessage(), var10);
                    }
                }
            }
        }
    
        //2.取消所有的服务订阅
        Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap(this.getSubscribed());
        if (!destroySubscribed.isEmpty()) {
            Iterator var12 = destroySubscribed.entrySet().iterator();
    
            while(var12.hasNext()) {
                Map.Entry<URL, Set<NotifyListener>> entry = (Map.Entry)var12.next();
                URL url = (URL)entry.getKey();
                Iterator var6 = ((Set)entry.getValue()).iterator();
    
                while(var6.hasNext()) {
                    NotifyListener listener = (NotifyListener)var6.next();
    
                    try {
                        this.unsubscribe(url, listener);
                        if (this.logger.isInfoEnabled()) {
                            this.logger.info("Destroy unsubscribe url " + url);
                        }
                    } catch (Throwable var9) {
                        this.logger.warn("Failed to unsubscribe url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var9.getMessage(), var9);
                    }
                }
            }
        }
    }
    

    1.移除内存中已经注册的服务

        // 1.移除内存中已经注册的服务
        Set<URL> destroyRegistered = new HashSet(this.getRegistered());
        if (!destroyRegistered.isEmpty()) {
            Iterator var2 = (new HashSet(this.getRegistered())).iterator();
    
            while(var2.hasNext()) {
                URL url = (URL)var2.next();
                if (url.getParameter("dynamic", true)) {
                    try {
                        this.unregister(url);
                        if (this.logger.isInfoEnabled()) {
                            this.logger.info("Destroy unregister url " + url);
                        }
                    } catch (Throwable var10) {
                        this.logger.warn("Failed to unregister url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var10.getMessage(), var10);
                    }
                }
            }
        }
    

    这部分代码主要是将内存当中的注册信息移除,这部分缓存记录,是在容器启动时,当向注册中心订阅成功后,会同步缓存一份到内存当中。可见,若注册中心挂掉了,Dubbo仍然可以通过缓存获取到远程RPC服务,但是无法获取到新增的RPC服务。

    这里主要分析两个方法:this.getRegistered()和 this.unregister(url)。

    this.getRegistered()——

    private final Set<URL> registered = new ConcurrentHashSet();
    
    public Set<URL> getRegistered() {
        return this.registered;
    }
    

    这是获取缓存URL的集合。

    this.unregister(url)——

    public void unregister(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("unregister url == null");
        } else {
            if (this.logger.isInfoEnabled()) {
                this.logger.info("Unregister: " + url);
            }
            this.registered.remove(url);
        }
    }
    

    这是将URL从Set集合当中移除的操作。这部分代码其实我有点想明白,为何还需要从Set获取到所有URL,然后再通过迭代器方式一个一个取出去进行移除,直接将Set置空不是更好些吗?当然,这里面应该还有一些我没有考虑到的细节,还有待进一步进行研究。

    2.取消所有服务订阅

        //2.取消所有的服务订阅
        Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap(this.getSubscribed());
        if (!destroySubscribed.isEmpty()) {
            Iterator var12 = destroySubscribed.entrySet().iterator();
    
            while(var12.hasNext()) {
                Map.Entry<URL, Set<NotifyListener>> entry = (Map.Entry)var12.next();
                URL url = (URL)entry.getKey();
                Iterator var6 = ((Set)entry.getValue()).iterator();
    
                while(var6.hasNext()) {
                    NotifyListener listener = (NotifyListener)var6.next();
    
                    try {
                        this.unsubscribe(url, listener);
                        if (this.logger.isInfoEnabled()) {
                            this.logger.info("Destroy unsubscribe url " + url);
                        }
                    } catch (Throwable var9) {
                        this.logger.warn("Failed to unsubscribe url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var9.getMessage(), var9);
                    }
                }
            }
        }
    

    这部分逻辑与移除内存url都很类型,都是先从缓存里把所有订阅信息都取出来,然后再跌代移除。


    二、关闭protocol协议

    这部分个关闭,主要是关闭provider和consumer,即对应前边提到的,服务提供方会先标记不再接受新请求,新请求过来直接报错,然后,检查线程池中的线程是否还在运行,如果有,等待线程完成,若超时,则强制关闭;服务消费者则不再发起新请求,同时检测看还有没有请求的响应没有返回,若有,等待返回,若超时,则强制关闭。

    下面大概分析一下其源码逻辑。

    protocol.destroy(),其方法在接口里定义,具体实现是在RegistryProtocol当中。

    @SPI("dubbo")
    public interface Protocol {
        int getDefaultPort();
        @Adaptive
        <T> Exporter<T> export(Invoker<T> var1) throws RpcException;
        @Adaptive
        <T> Invoker<T> refer(Class<T> var1, URL var2) throws RpcException;
        void destroy();
    }
    

    RegistryProtocol的具体实现如下:

    public void destroy() {
        List<Exporter<?>> exporters = new ArrayList(this.bounds.values());
        Iterator var2 = exporters.iterator();
        while(var2.hasNext()) {
            Exporter<?> exporter = (Exporter)var2.next();
            exporter.unexport();
        }
        this.bounds.clear();
    }
    

    这里的核心方法是exporter.unexport(),根据命名就可以推测出,大概就是说不暴露对外接口协议的方法,也就是关闭那些对外暴露的服务。

    该exporter.unexport()方法具体实现有两类,一个是DubboExporter,一个是AbstractExporter,这里主要分析下AbstractExporter里面的逻辑。

    AbstractExporter内部关于unexport()的方法如下:

    public void unexport() {
        if (!this.unexported) {
            this.unexported = true;
            this.getInvoker().destroy();
        }
    }	
    

    this.getInvoker().destroy()的实现如下:

    public void destroy() {
        Iterator var1 = (new ArrayList(this.serverMap.keySet())).iterator();
    
        String key;
        //关停所有的Server,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());
                    }
                    // HeaderExchangeServer中会停止发送心态的任务,关闭channel
                    server.close(getServerShutdownTimeout());
                } catch (Throwable var7) {
                    this.logger.warn(var7.getMessage(), var7);
                }
            }
        }
    
        var1 = (new ArrayList(this.referenceClientMap.keySet())).iterator();
    
        ExchangeClient client;
        //关停所有Client,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());
                    }
                    // HeaderExchangeClient中会停止发送心态的任务,关闭channel
                    client.close();
                } catch (Throwable var6) {
                    this.logger.warn(var6.getMessage(), var6);
                }
            }
        }
    ......
    }
    

    总结一下,Dubbo的优雅下线,若是通过JDK的shutdownHook来完成优雅停机的,这时当用户对该Dubbo进行执行kill pid后,在关闭JVM时会发起一个线程执行ShutdownHook,进而执行 ProtocolConfig.destroyAll()方法,该方法在关掉进行前,主要做了以下一些清理工作:

    1、关闭zk客户端

    2、 客户端断开ZK连接后,ZK会自动删除临时注册节点

    3、 取消重试机制

    4 、清除内存中已经注册的服务

    5、 取消所有的服务订阅

    6、关闭provider和consumer,停止新的请求

    后面还有一步没分析到,是若仍有在执行的线程,会等待其执行完成。

    最后,在清理完一系列工作后,就可以关闭该进程了。

    这就是Dubbo的优雅下线大概的原理。

    作者:朱季谦
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    mysql BETWEEN操作符 语法
    mysql IN操作符 语法
    mysql LIKE通配符 语法
    mysql TOP语句 语法
    mysql DELETE语句 语法
    mysql Update语句 语法
    mysql INSERT语句 语法
    mysql ORDER BY语句 语法
    mysql OR运算符 语法
    mysql AND运算符 语法
  • 原文地址:https://www.cnblogs.com/zhujiqian/p/15067136.html
Copyright © 2020-2023  润新知