Spring-boot+Dubbo应用启停源码分析

简介: Dubbo Spring Boot 工程致力于简化 Dubbo RPC 框架在Spring Boot应用场景的开发。同时也整合了 Spring Boot 特性: 自动装配 (比如: 注解驱动, 自动装配等). Production-Ready (比如: 安全, 健康检查, 外部化配置等).

背景介绍

Dubbo Spring Boot 工程致力于简化 Dubbo RPC 框架在Spring Boot应用场景的开发。同时也整合了 Spring Boot 特性:

DubboConsumer启动分析

你有没有想过一个问题?incubator-dubbo-spring-boot-project中的DubboConsumerDemo应用就一行代码,main方法执行完之后,为什么不会直接退出呢?

@SpringBootApplication(scanBasePackages = "com.alibaba.boot.dubbo.demo.consumer.controller")
public class DubboConsumerDemo {

    public static void main(String[] args) {
        SpringApplication.run(DubboConsumerDemo.class,args);
    }

}

其实要回答这样一个问题,我们首先需要把这个问题进行一个抽象,即一个JVM进程,在什么情况下会退出?

以Java 8为例,通过查阅JVM语言规范[1],在12.8章节中有清晰的描述:

A program terminates all its activity and exits when one of two things happens:

  • All the threads that are not daemon threads terminate.
  • Some thread invokes the exit method of class Runtime or class System, and the exit operation is not forbidden by the security manager.

也就是说,导致JVM的退出只有2种情况:

  1. 所有的非daemon进程完全终止
  2. 某个线程调用了System.exit()Runtime.exit()

因此针对上面的情况,我们判断,一定是有某个非daemon线程没有退出导致。我们知道,通过jstack可以看到所有的线程信息,包括他们是否是daemon线程,可以通过jstack找出那些是非deamon的线程。

  jstack 57785 | grep tid | grep -v "daemon"
"container-0" #37 prio=5 os_prio=31 tid=0x00007fbe312f5800 nid=0x7103 waiting on condition  [0x0000700010144000]
"container-1" #49 prio=5 os_prio=31 tid=0x00007fbe3117f800 nid=0x7b03 waiting on condition  [0x0000700010859000]
"DestroyJavaVM" #83 prio=5 os_prio=31 tid=0x00007fbe30011000 nid=0x2703 waiting on condition  [0x0000000000000000]
"VM Thread" os_prio=31 tid=0x00007fbe3005e800 nid=0x3703 runnable
"GC Thread#0" os_prio=31 tid=0x00007fbe30013800 nid=0x5403 runnable
"GC Thread#1" os_prio=31 tid=0x00007fbe30021000 nid=0x5303 runnable
"GC Thread#2" os_prio=31 tid=0x00007fbe30021800 nid=0x2d03 runnable
"GC Thread#3" os_prio=31 tid=0x00007fbe30022000 nid=0x2f03 runnable
"G1 Main Marker" os_prio=31 tid=0x00007fbe30040800 nid=0x5203 runnable
"G1 Conc#0" os_prio=31 tid=0x00007fbe30041000 nid=0x4f03 runnable
"G1 Refine#0" os_prio=31 tid=0x00007fbe31044800 nid=0x4e03 runnable
"G1 Refine#1" os_prio=31 tid=0x00007fbe31045800 nid=0x4d03 runnable
"G1 Refine#2" os_prio=31 tid=0x00007fbe31046000 nid=0x4c03 runnable
"G1 Refine#3" os_prio=31 tid=0x00007fbe31047000 nid=0x4b03 runnable
"G1 Young RemSet Sampling" os_prio=31 tid=0x00007fbe31047800 nid=0x3603 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x00007fbe31129000 nid=0x6703 waiting on condition

此处通过grep tid 找出所有的线程摘要,通过grep -v找出不包含daemon关键字的行

通过上面的结果,我们发现了一些信息:

  • 有两个线程container-0, container-1非常可疑,他们是非daemon线程,处于wait状态
  • 有一些GC相关的线程,和VM打头的线程,也是非daemon线程,但他们很有可能是JVM自己的线程,在此暂时忽略。

综上,我们可以推断,很可能是因为container-0container-1导致JVM没有退出。现在我们通过源码,搜索一下到底是谁创建的这两个线程。

通过对spring-boot的源码分析,我们在org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerstartDaemonAwaitThread找到了如下代码

    private void startDaemonAwaitThread() {
        Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

            @Override
            public void run() {
                TomcatEmbeddedServletContainer.this.tomcat.getServer().await();
            }

        };
        awaitThread.setContextClassLoader(getClass().getClassLoader());
        awaitThread.setDaemon(false);
        awaitThread.start();
    }

在这个方法加个断点,看下调用堆栈:

initialize:115, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)
<init>:84, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)
getTomcatEmbeddedServletContainer:554, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat)
getEmbeddedServletContainer:179, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat)
createEmbeddedServletContainer:164, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)
onRefresh:134, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)
refresh:537, AbstractApplicationContext (org.springframework.context.support)
refresh:122, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)
refresh:693, SpringApplication (org.springframework.boot)
refreshContext:360, SpringApplication (org.springframework.boot)
run:303, SpringApplication (org.springframework.boot)
run:1118, SpringApplication (org.springframework.boot)
run:1107, SpringApplication (org.springframework.boot)
main:35, DubboConsumerDemo (com.alibaba.boot.dubbo.demo.consumer.bootstrap)

可以看到,spring-boot应用在启动的过程中,由于默认启动了Tomcat暴露HTTP服务,所以执行到了上述方法,而Tomcat启动的所有的线程,默认都是daemon线程,例如监听请求的Acceptor,工作线程池等等,如果这里不加控制的话,启动完成之后JVM也会退出。因此需要显示的启动一个线程,在某个条件下进行持续等待,从而避免线程退出。

下面我们在深挖一下,在Tomcat的this.tomcat.getServer().await()这个方法中,线程是如何实现不退出的。这里为了阅读方便,去掉了不相关的代码。

public void await() {
        // ...
        if( port==-1 ) {
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }
        // ...
    }

在await方法中,实际上当前线程在一个while循环中每10秒检查一次 stopAwait这个变量,它是一个volatile类型变量,用于确保被另一个线程修改后,当前线程能够立即看到这个变化。如果没有变化,就会一直处于while循环中。这就是该线程不退出的原因,也就是整个spring-boot应用不退出的原因。

因为Springboot应用同时启动了8080和8081(management port)两个端口,实际是启动了两个Tomcat,因此会有两个线程container-0container-1

接下来,我们再看看,这个Spring-boot应用又是如何退出的呢?

DubboConsumer退出分析

在前面的描述中提到,有一个线程持续的在检查stopAwait这个变量,那么我们自然想到,在Stop的时候,应该会有一个线程去修改stopAwait,打破这个while循环,那又是谁在修改这个变量呢?

通过对源码分析,可以看到只有一个方法修改了stopAwait,即org.apache.catalina.core.StandardServer#stopAwait,我们在此处加个断点,看看是谁在调用。

注意,当我们在Intellij IDEA的Debug模式,加上一个断点后,需要在命令行下使用kill -s INT $PID或者kill -s TERM $PID才能触发断点,点击IDE上的Stop按钮,不会触发断点。这是IDEA的bug

可以看到有一个名为Thread-3的线程调用了该方法:

stopAwait:390, StandardServer (org.apache.catalina.core)
stopInternal:819, StandardServer (org.apache.catalina.core)
stop:226, LifecycleBase (org.apache.catalina.util)
stop:377, Tomcat (org.apache.catalina.startup)
stopTomcat:241, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)
stop:295, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)
stopAndReleaseEmbeddedServletContainer:306, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)
onClose:155, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)
doClose:1014, AbstractApplicationContext (org.springframework.context.support)
run:929, AbstractApplicationContext$2 (org.springframework.context.support)

通过源码分析,原来是通过Spring注册的ShutdownHook来执行的

    @Override
    public void registerShutdownHook() {
        if (this.shutdownHook == null) {
            // No shutdown hook registered yet.
            this.shutdownHook = new Thread() {
                @Override
                public void run() {
                    synchronized (startupShutdownMonitor) {
                        doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }
    }

通过查阅Java的API文档[2], 我们可以知道ShutdownHook将在下面两种情况下执行

The Java virtual machine shuts down in response to two kinds of events:

  • The program exits normally, when the last non-daemon thread exits or when the exit (equivalently, System.exit) method is invoked, or
  • The virtual machine is terminated in response to a user interrupt, such as typing ^C, or a system-wide event, such as user logoff or system shutdown.
  1. 调用了System.exit()方法
  2. 响应外部的信号,例如Ctrl+C(其实发送的是SIGINT信号),或者是SIGTERM信号(默认kill $PID发送的是SIGTERM信号)

因此,正常的应用在停止过程中(kill -9 $PID除外),都会执行上述ShutdownHook,它的作用不仅仅是关闭tomcat,还有进行其他的清理工作,在此不再赘述。

总结

  1. DubboConsumer启动的过程中,通过启动一个独立的非daemon线程循环检查变量的状态,确保进程不退出
  2. DubboConsumer停止的过程中,通过执行spring容器的shutdownhook,修改了变量的状态,使得程序正常退出

问题

在DubboProvider的例子中,我们看到Provider并没有启动Tomcat提供HTTP服务,那又是如何实现不退出的呢?我们将在下一篇文章中回答这个问题。

彩蛋

Intellij IDEA中运行了如下的单元测试,创建一个线程执行睡眠1000秒的操作,我们惊奇的发现,代码并没有线程执行完就退出了,这又是为什么呢?(被创建的线程是非daemon线程)

    @Test
    public void test() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

[1] https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.8

[2] https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#addShutdownHook

相关文章
|
8月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1218 3
|
7月前
|
SQL Java 数据库连接
Spring Data JPA 技术深度解析与应用指南
本文档全面介绍 Spring Data JPA 的核心概念、技术原理和实际应用。作为 Spring 生态系统中数据访问层的关键组件,Spring Data JPA 极大简化了 Java 持久层开发。本文将深入探讨其架构设计、核心接口、查询派生机制、事务管理以及与 Spring 框架的集成方式,并通过实际示例展示如何高效地使用这一技术。本文档约1500字,适合有一定 Spring 和 JPA 基础的开发者阅读。
686 0
|
6月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
414 8
|
8月前
|
Java 应用服务中间件 开发者
Spring Boot 技术详解与应用实践
本文档旨在全面介绍 Spring Boot 这一广泛应用于现代企业级应用开发的框架。内容将涵盖 Spring Boot 的核心概念、核心特性、项目自动生成与结构解析、基础功能实现(如 RESTful API、数据访问)、配置管理以及最终的构建与部署。通过本文档,读者将能够理解 Spring Boot 如何简化 Spring 应用的初始搭建和开发过程,并掌握其基本使用方法。
576 2
|
8月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
Dubbo Java 应用服务中间件
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
362 2
|
Dubbo Java 应用服务中间件
💥Spring Cloud Dubbo火爆来袭!微服务通信的终极利器,你知道它有多强大吗?🔥
【8月更文挑战第29天】随着信息技术的发展,微服务架构成为企业应用开发的主流模式,而高效的微服务通信至关重要。Spring Cloud Dubbo通过整合Dubbo与Spring Cloud的优势,提供高性能RPC通信及丰富的生态支持,包括服务注册与发现、负载均衡和容错机制等,简化了服务调用管理并支持多种通信协议,提升了系统的可伸缩性和稳定性,成为微服务通信领域的优选方案。开发者仅需关注业务逻辑,而无需过多关心底层通信细节,使得Spring Cloud Dubbo在未来微服务开发中将更加受到青睐。
271 0
|
Dubbo 应用服务中间件 Apache
Star 4w+,Apache Dubbo 3.3 全新发布,Triple X 领衔,开启微服务通信新时代
在 Apache Dubbo 突破 4w Star 之际,Apache Dubbo 团队正式宣布,Dubbo 3.3 正式发布!作为全球领先的开源微服务框架,Dubbo 一直致力于为开发者提供高性能、可扩展且灵活的分布式服务解决方案。此次发布的 Dubbo 3.3,通过 Triple X 的全新升级,突破了以往局限,实现了对南北向与东西向流量的全面支持,并提升了对云原生架构的友好性。
474 101
|
负载均衡 Dubbo 应用服务中间件
框架巨擘:Dubbo如何一统异构微服务江湖,成为开发者的超级武器!
【8月更文挑战第8天】在软件开发中,微服务架构因灵活性和可扩展性备受欢迎。面对异构微服务的挑战,Apache Dubbo作为高性能Java RPC框架脱颖而出。它具备服务注册与发现、负载均衡及容错机制等核心特性,支持多种通信协议和序列化方式,能有效连接不同技术栈的微服务。Dubbo的插件化设计保证了面向未来的扩展性,使其成为构建稳定高效分布式系统的理想选择。
375 80