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的优雅下线大概的原理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/163456.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32硬件调试器不一定准确,proteus不一定准确

我在做实验的过程中&#xff0c;发现里面的那个变量ii一直都不变搞了很久没有发现问题&#xff0c; 然后怀疑是不是软件出了问题&#xff0c;然后直接只用单片机的一个灯泡来检测是否正常&#xff0c;发现&#xff1a;单片机里面正常&#xff0c;但是硬件调试的时候&#xff0…

计算机网络秋招面试题

自己在秋招过程中遇到的计算机网络的面试题 OSI七层网络模型 DNS&#xff1a;应用层协议 根据域名查IP地址 DNS查询⽅式有哪些&#xff1f; 递归解析 局部DNS服务器⾃⼰负责向其他DNS服务器进⾏查询&#xff0c;⼀般是先向该域名的根域服务器查询&#xff0c;再由根域名服…

【C++】容器string的常用成员函数接口

目录 string - C Reference 1 容量相关 1.1 size/length 1.2 capacity 1.3 resize 1.4 reserve 1.5 empty 2 运算符重载 2.1 operator 2.2 operator[] 2.3 operator&#xff08;非成员函数&#xff09; 2.4 operator 2.5 operator>> && operator<…

【使用vscode在线web搭建开发环境--code-server搭建】

官方版本下载 https://github.com/coder/code-server/releases?q4.0.0&expandedtrue使用大于版本3.8.0,因为旧版本有插件市场不能访问的情况版本太高需要更新环境依赖 拉取安装包 []# wget "https://github.com/coder/code-server/releases/download/v4.0.0/code-…

64位ATT汇编语言使用bss段.skip指令储存字符,并使用系统调用输出字符

.global main .section .data .section .bss# 需要输出的字符数组&#xff0c;还没有初始化mystring: .skip 4 .section .text main:# 将mystring这个字符串的地址存入到rbx寄存器中leaq mystring,%rbx# 将a放入到mystring第一个字节里边movb $a,(%rbx)# 将地址往后边移动一个字…

16万亿Web3蓝图落地新加坡

作者&#xff1a;秦晋 11月15日&#xff0c;新加坡金管局&#xff08;MAS&#xff09;宣布与金融行业合作&#xff0c;以扩大资产代币化计划&#xff0c;并开发扩大代币化市场的基础能力。Project Guardian 由 17 家金融机构组成&#xff0c;启动五个行业试点&#xff0c;以测试…

Leetcode—3.无重复字符的最长子串【中等】

2023每日刷题&#xff08;三十二&#xff09; Leetcode—3.无重复字符的最长子串 实现代码 class Solution { public:int lengthOfLongestSubstring(string s) {unordered_set<char> smap;int maxlen 0;int left 0;for(int i 0; i < s.size(); i) {while(smap.fi…

公共字段自动填充-Mybatis Plus实现

简历描述 使用ThreadLocal动态获取当前登录用户&#xff0c;从而解决MybatisPlus公共字段自动填充问题。达到简化编码的目的&#xff0c;使业务方法更加简洁。 问题分析 前面我们已经完成了后台系统的员工管理功能的开发&#xff0c;在新增员工时需要设置创建时间、创建人、…

Zabbix5.0部署及应用

环境 主机名 IP 类型server01192.168.134.165zabbix-serverserver02 192.168.134.166zabbix-agent 官方部署文档 1 .安装yum源 [rootserver01 ~]# rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-rel…

浅谈WPF之控件模板和数据模板

WPF不仅支持传统的Windows Forms编程的用户界面和用户体验设计&#xff0c;同时还推出了以模板为核心的新一代设计理念。在WPF中&#xff0c;通过引入模板&#xff0c;将数据和算法的“内容”和“形式”进行解耦。模板主要分为两大类&#xff1a;数据模板【Data Template】和控…

初刷leetcode题目(2)——数据结构与算法

&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️…

PostgreSQL 难搞的事系列 --- vacuum 的由来与PG16的命令的改进 (1)

开头还是介绍一下群&#xff0c;如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, Oceanbase, Sql Server等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友…

从零开始:抖音酒店景区小程序开发指南

为了满足用户多样化的需求&#xff0c;开发一款抖音酒店景区小程序成为了业界的一个新兴趋势。在这篇文章中&#xff0c;我们将探讨如何开发一款引人注目的抖音风格的酒店景区小程序。 一、抖音风格的设计理念 在设计酒店景区小程序时&#xff0c;我们需要融入抖音的设计理念。…

22 - 如何优化垃圾回收机制?

我们知道&#xff0c;在 Java 开发中&#xff0c;开发人员是无需过度关注对象的回收与释放的&#xff0c;JVM 的垃圾回收机制可以减轻不少工作量。但完全交由 JVM 回收对象&#xff0c;也会增加回收性能的不确定性。在一些特殊的业务场景下&#xff0c;不合适的垃圾回收算法以及…

Leetcode—剑指OfferII LCR 022.环形链表II【中等】

2023每日刷题&#xff08;三十三&#xff09; Leetcode—LCR 022.环形链表II 算法思想 参考k神的博客 实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode *detectCycle(struct List…

【C++】类和对象(6)--运算符重载

目录 一 概念 二 运算符重载的实现 三 关于时间的所有运算符重载 四 默认赋值运算符 五 const取地址操作符重载 一 概念 C为了增强代码的可读性引入了运算符重载&#xff0c;运算符重载是具有特殊函数名的函数&#xff0c;也具有其返回值类型&#xff0c;函数名字以及参数…

Git 简介及使用

前言 假设有这样一个场景&#xff0c;老板让员工做一个档案&#xff0c;员工这个档案做好了之后交给老板看&#xff0c;此时老板不满意&#xff0c;又让回去改&#xff0c;改完给老板看&#xff0c;但是老板又不是很满意&#xff0c;就这样改了又改&#xff0c;给老板看过之后&…

代码随想录算法训练营第四十五天| 139.单词拆分

文档讲解&#xff1a;代码随想录 视频讲解&#xff1a;代码随想录B站账号 状态&#xff1a;看了视频题解和文章解析后做出来了 139.单词拆分 class Solution:def wordBreak(self, s: str, wordDict: List[str]) -> bool:n len(s)dp [False] * (n1)dp[0] Truemax_len m…

WMS重力式货架库位对应方法

鉴于重力式货架的特殊结构和功能&#xff0c;货物由高的一端存入&#xff0c;滑至低端&#xff0c;从低端取出。所以重力式货架的每个货位在物理上都会有一个进货口和一个出货口。因此&#xff0c;在空间上&#xff0c;对同一个货位执行出入库操作需要处于不同的位置。 比如对…

【AD封装】芯片IC-SOP,SOIC,SSOP,TSSOP,SOT(带3D)

包含了我们平时常用的芯片IC封装&#xff0c;包含SOP,SOIC,SSOP,TSSOP,SOT&#xff0c;总共171种封装及精美3D模型。完全能满足日常设计使用。每个封装都搭配了精美的3D模型哦。 ❖ TSSOP和SSOP 均为SOP衍生出来的封装。TSSOP的中文解释为&#xff1a;薄的缩小型 SOP封装。SSO…