文/朱季谦

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 秒,如果超时则强制关闭。

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

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

  1. ProtocolConfig.destroyAll();

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

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

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

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

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

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

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

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

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

一、注销注册中心

  1. public static void destroyAll() {
  2. if (LOGGER.isInfoEnabled()) {
  3. LOGGER.info("Close all registries " + getRegistries());
  4. }
  5. //加锁,防止关闭多次
  6. LOCK.lock();
  7. try {
  8. Iterator var0 = getRegistries().iterator();
  9. //关闭所有已创建的注册中心
  10. while(var0.hasNext()) {
  11. Registry registry = (Registry)var0.next();
  12. try {
  13. registry.destroy();
  14. } catch (Throwable var6) {
  15. LOGGER.error(var6.getMessage(), var6);
  16. }
  17. }
  18. REGISTRIES.clear();
  19. } finally {
  20. //释放锁
  21. LOCK.unlock();
  22. }
  23. }

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

  1. Iterator var0 = getRegistries().iterator();

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

  1. registry.destroy();

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

  1. public interface Node {
  2. URL getUrl();
  3. boolean isAvailable();
  4. void destroy();
  5. }

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

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

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

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

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

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

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

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

  1. public void destroy() {
  2. if (this.logger.isInfoEnabled()) {
  3. this.logger.info("Destroy registry:" + this.getUrl());
  4. }
  5. // 1.移除内存中已经注册的服务
  6. Set<URL> destroyRegistered = new HashSet(this.getRegistered());
  7. if (!destroyRegistered.isEmpty()) {
  8. Iterator var2 = (new HashSet(this.getRegistered())).iterator();
  9. while(var2.hasNext()) {
  10. URL url = (URL)var2.next();
  11. if (url.getParameter("dynamic", true)) {
  12. try {
  13. this.unregister(url);
  14. if (this.logger.isInfoEnabled()) {
  15. this.logger.info("Destroy unregister url " + url);
  16. }
  17. } catch (Throwable var10) {
  18. this.logger.warn("Failed to unregister url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var10.getMessage(), var10);
  19. }
  20. }
  21. }
  22. }
  23. //2.取消所有的服务订阅
  24. Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap(this.getSubscribed());
  25. if (!destroySubscribed.isEmpty()) {
  26. Iterator var12 = destroySubscribed.entrySet().iterator();
  27. while(var12.hasNext()) {
  28. Map.Entry<URL, Set<NotifyListener>> entry = (Map.Entry)var12.next();
  29. URL url = (URL)entry.getKey();
  30. Iterator var6 = ((Set)entry.getValue()).iterator();
  31. while(var6.hasNext()) {
  32. NotifyListener listener = (NotifyListener)var6.next();
  33. try {
  34. this.unsubscribe(url, listener);
  35. if (this.logger.isInfoEnabled()) {
  36. this.logger.info("Destroy unsubscribe url " + url);
  37. }
  38. } catch (Throwable var9) {
  39. this.logger.warn("Failed to unsubscribe url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var9.getMessage(), var9);
  40. }
  41. }
  42. }
  43. }
  44. }

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

  1. // 1.移除内存中已经注册的服务
  2. Set<URL> destroyRegistered = new HashSet(this.getRegistered());
  3. if (!destroyRegistered.isEmpty()) {
  4. Iterator var2 = (new HashSet(this.getRegistered())).iterator();
  5. while(var2.hasNext()) {
  6. URL url = (URL)var2.next();
  7. if (url.getParameter("dynamic", true)) {
  8. try {
  9. this.unregister(url);
  10. if (this.logger.isInfoEnabled()) {
  11. this.logger.info("Destroy unregister url " + url);
  12. }
  13. } catch (Throwable var10) {
  14. this.logger.warn("Failed to unregister url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var10.getMessage(), var10);
  15. }
  16. }
  17. }
  18. }

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

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

this.getRegistered()——

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

这是获取缓存URL的集合。

this.unregister(url)——

  1. public void unregister(URL url) {
  2. if (url == null) {
  3. throw new IllegalArgumentException("unregister url == null");
  4. } else {
  5. if (this.logger.isInfoEnabled()) {
  6. this.logger.info("Unregister: " + url);
  7. }
  8. this.registered.remove(url);
  9. }
  10. }

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

2.取消所有服务订阅

  1. //2.取消所有的服务订阅
  2. Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap(this.getSubscribed());
  3. if (!destroySubscribed.isEmpty()) {
  4. Iterator var12 = destroySubscribed.entrySet().iterator();
  5. while(var12.hasNext()) {
  6. Map.Entry<URL, Set<NotifyListener>> entry = (Map.Entry)var12.next();
  7. URL url = (URL)entry.getKey();
  8. Iterator var6 = ((Set)entry.getValue()).iterator();
  9. while(var6.hasNext()) {
  10. NotifyListener listener = (NotifyListener)var6.next();
  11. try {
  12. this.unsubscribe(url, listener);
  13. if (this.logger.isInfoEnabled()) {
  14. this.logger.info("Destroy unsubscribe url " + url);
  15. }
  16. } catch (Throwable var9) {
  17. this.logger.warn("Failed to unsubscribe url " + url + " to registry " + this.getUrl() + " on destroy, cause: " + var9.getMessage(), var9);
  18. }
  19. }
  20. }
  21. }

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

二、关闭protocol协议

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

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

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

  1. @SPI("dubbo")
  2. public interface Protocol {
  3. int getDefaultPort();
  4. @Adaptive
  5. <T> Exporter<T> export(Invoker<T> var1) throws RpcException;
  6. @Adaptive
  7. <T> Invoker<T> refer(Class<T> var1, URL var2) throws RpcException;
  8. void destroy();
  9. }

RegistryProtocol的具体实现如下:

  1. public void destroy() {
  2. List<Exporter<?>> exporters = new ArrayList(this.bounds.values());
  3. Iterator var2 = exporters.iterator();
  4. while(var2.hasNext()) {
  5. Exporter<?> exporter = (Exporter)var2.next();
  6. exporter.unexport();
  7. }
  8. this.bounds.clear();
  9. }

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

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

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

  1. public void unexport() {
  2. if (!this.unexported) {
  3. this.unexported = true;
  4. this.getInvoker().destroy();
  5. }
  6. }

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

  1. public void destroy() {
  2. Iterator var1 = (new ArrayList(this.serverMap.keySet())).iterator();
  3. String key;
  4. //关停所有的Server,provider不再接收新的请求
  5. while(var1.hasNext()) {
  6. key = (String)var1.next();
  7. ExchangeServer server = (ExchangeServer)this.serverMap.remove(key);
  8. if (server != null) {
  9. try {
  10. if (this.logger.isInfoEnabled()) {
  11. this.logger.info("Close dubbo server: " + server.getLocalAddress());
  12. }
  13. // HeaderExchangeServer中会停止发送心态的任务,关闭channel
  14. server.close(getServerShutdownTimeout());
  15. } catch (Throwable var7) {
  16. this.logger.warn(var7.getMessage(), var7);
  17. }
  18. }
  19. }
  20. var1 = (new ArrayList(this.referenceClientMap.keySet())).iterator();
  21. ExchangeClient client;
  22. //关停所有Client,consumer将不再发送新的请求
  23. while(var1.hasNext()) {
  24. key = (String)var1.next();
  25. client = (ExchangeClient)this.referenceClientMap.remove(key);
  26. if (client != null) {
  27. try {
  28. if (this.logger.isInfoEnabled()) {
  29. this.logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
  30. }
  31. // HeaderExchangeClient中会停止发送心态的任务,关闭channel
  32. client.close();
  33. } catch (Throwable var6) {
  34. this.logger.warn(var6.getMessage(), var6);
  35. }
  36. }
  37. }
  38. ......
  39. }

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

1、关闭zk客户端

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

3、 取消重试机制

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

5、 取消所有的服务订阅

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

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

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

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

Dubbo的优雅下线原理分析的更多相关文章

  1. dubbo monitor simple 监控原理分析

    监控机制: 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心.监控中心负责统计各服务调用次数,调用时间等. 监控元数据存储目录结构: --dubbo.jetty ...

  2. dubbo源码解析五 --- 集群容错架构设计与原理分析

    欢迎来我的 Star Followers 后期后继续更新Dubbo别的文章 Dubbo 源码分析系列之一环境搭建 博客园 Dubbo 入门之二 --- 项目结构解析 博客园 Dubbo 源码分析系列之 ...

  3. Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题

    Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题 相关文章: Dubbo源码学习文章目录 前言 主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题 ...

  4. Dubbo系列讲解之扩展点实现原理分析【2万字分享】

    Apache Dubbo 是一款微服务开发框架,它提供了 RPC通信 与 微服务治理 两大关键能力.这意味着,使用 Dubbo 开发的微服务,将具备相互之间的远程发现与通信能力, 同时利用 Dubbo ...

  5. Serverless 架构下的服务优雅下线实践

    作者 | 行松 阿里巴巴云原生团队 应用发布.服务升级一直是一个让开发和运维同学既兴奋又担心的事情. 兴奋的是有新功能上线,自己的产品可以对用户提供更多的能力和价值:担心的是上线的过程会不会出现意外情 ...

  6. synchronized 原理分析

    synchronized 原理分析 1. synchronized 介绍    在并发程序中,这个关键字可能是出现频率最高的一个字段,他可以避免多线程中的安全问题,对代码进行同步.同步的方式其实就是隐 ...

  7. 知识点:synchronized 原理分析

    synchronized 原理分析 1. synchronized 介绍 在并发程序中,这个关键字可能是出现频率最高的一个字段,他可以避免多线程中的安全问题,对代码进行同步.同步的方式其实就是隐式的加 ...

  8. Hadoop生态圈-Zookeeper的工作原理分析

    Hadoop生态圈-Zookeeper的工作原理分析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   无论是是Kafka集群,还是producer和consumer都依赖于Zoo ...

  9. MyBatis的深入原理分析之1-架构设计以及实例分析

    MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单.优雅.本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询实例, ...

随机推荐

  1. ES6中的数组方法扩展

    上一篇文章小编简单介绍了在ES6中,一些常用的方法和一些简单的应用,在这篇文章中,小编将针对ES6中数组常用方法进行扩展,相信经过这篇文章之后,每一位小伙伴下班时间会有所提前,就算从原来的996变成9 ...

  2. 七、SSL加密网站(待解决)

    keytool -genkeypair -alias tomcat -keyalg RSA -keystore /usr/local/tomcat/keystore  //创建私钥和证书文件提示输入密 ...

  3. 网页站点下载器teleport ultra

    软件名称:teleport ultra 介绍:teleport ultra是一款专门的网页站点下载器,使用这款工具可以方便地下载网页数据,包括网站的文字.图片.flash动画等,可以轻松下载所有的网站 ...

  4. 学习响应式编程 Reactor (1) - 响应式编程

    响应式编程 命令式编程(Imperative Programing),是一种描述计算机所需做出的行为的编程范式.详细的命令机器怎么(How)去处理以达到想要的结果(What). 声明式编程(Decla ...

  5. 题解 P3605 [USACO17JAN]Promotion Counting P

    分块\(yyds\) ----关于线段树合并的题我用分块过掉这件事 题目传送门 先说正解 正解当然是线段树合并等一类做法了 至于解析...出门右转题解区第一篇 (就是他让我看不懂,然后用分块打的\(Q ...

  6. csp-s模拟测试42「世界线·时间机器·密码」

    $t3$不会 世界线 题解 题目让求的就是每个点能到点的数量$-$出度 设每个点能到的点为$f[x]$ 则$f[x]=x \sum\limits_{y}^{y\in son[x]} U f[y]$ 用 ...

  7. perror()函数的使用

    perror()函数的功能是打印一个系统错误信息.        perror()函数在Linux系统中属于库函数,在头文件中有如下定义: #include <stdio.h>       ...

  8. SSM动态展示分页

    这个作业属于哪个课程 2021春软件工程实践|S班(福州大学) 这个作业要求在哪里 作业具体要求 这个作业的目标 个人技术 参考文献 ... 目录 技术概述 技术详述 问题和解决过程 总结 参考文献 ...

  9. 《面试补习》- Java集合知识梳理

    一.ArrayList ArrayList 底层数据结构为 动态数组 ,所以我们可以将之称为数组队列. ArrayList 的依赖关系: public class ArrayList<E> ...

  10. 用jquery通过点击事件把下拉列表币种的值传给文本框1,再通过文本框1的币种名称用if转化为币别传值给文本框2保存

    <script src="https://cdn.staticfile.org/jquery/2.2.4/jquery.min.js"></script>& ...