Spring Boot Dubbo应用启停源码分析

简介:
背景介绍

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

  • 自动装配 (比如: 注解驱动, 自动装配等).

  • Production-Ready (比如: 安全, 健康检查, 外部化配置等).

DubboConsumer启动分析

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

 
  1. @SpringBootApplication(scanBasePackages = "com.alibaba.boot.dubbo.demo.consumer.controller")

  2. public class DubboConsumerDemo {


  3. public static void main(String[] args) {

  4. SpringApplication.run(DubboConsumerDemo.class,args);

  5. }


  6. }

其实要回答这样一个问题,我们首先需要把这个问题进行一个抽象,即一个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 exitoperation is not forbidden by the security manager.

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

  1. 所有的非daemon进程完全终止

  2. 某个线程调用了 System.exit()Runtime.exit()

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

 
  1. jstack 57785 | grep tid | grep -v "daemon"

  2. "container-0" #37 prio=5 os_prio=31 tid=0x00007fbe312f5800 nid=0x7103 waiting on condition [0x0000700010144000]

  3. "container-1" #49 prio=5 os_prio=31 tid=0x00007fbe3117f800 nid=0x7b03 waiting on condition [0x0000700010859000]

  4. "DestroyJavaVM" #83 prio=5 os_prio=31 tid=0x00007fbe30011000 nid=0x2703 waiting on condition [0x0000000000000000]

  5. "VM Thread" os_prio=31 tid=0x00007fbe3005e800 nid=0x3703 runnable

  6. "GC Thread#0" os_prio=31 tid=0x00007fbe30013800 nid=0x5403 runnable

  7. "GC Thread#1" os_prio=31 tid=0x00007fbe30021000 nid=0x5303 runnable

  8. "GC Thread#2" os_prio=31 tid=0x00007fbe30021800 nid=0x2d03 runnable

  9. "GC Thread#3" os_prio=31 tid=0x00007fbe30022000 nid=0x2f03 runnable

  10. "G1 Main Marker" os_prio=31 tid=0x00007fbe30040800 nid=0x5203 runnable

  11. "G1 Conc#0" os_prio=31 tid=0x00007fbe30041000 nid=0x4f03 runnable

  12. "G1 Refine#0" os_prio=31 tid=0x00007fbe31044800 nid=0x4e03 runnable

  13. "G1 Refine#1" os_prio=31 tid=0x00007fbe31045800 nid=0x4d03 runnable

  14. "G1 Refine#2" os_prio=31 tid=0x00007fbe31046000 nid=0x4c03 runnable

  15. "G1 Refine#3" os_prio=31 tid=0x00007fbe31047000 nid=0x4b03 runnable

  16. "G1 Young RemSet Sampling" os_prio=31 tid=0x00007fbe31047800 nid=0x3603 runnable

  17. "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找到了如下代码

 
  1. private void startDaemonAwaitThread() {

  2. Thread awaitThread = new Thread("container-" + (containerCounter.get())) {


  3. @Override

  4. public void run() {

  5. TomcatEmbeddedServletContainer.this.tomcat.getServer().await();

  6. }


  7. };

  8. awaitThread.setContextClassLoader(getClass().getClassLoader());

  9. awaitThread.setDaemon(false);

  10. awaitThread.start();

  11. }

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

 
  1. initialize:115, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  2. <init>:84, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  3. getTomcatEmbeddedServletContainer:554, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat)

  4. getEmbeddedServletContainer:179, TomcatEmbeddedServletContainerFactory (org.springframework.boot.context.embedded.tomcat)

  5. createEmbeddedServletContainer:164, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  6. onRefresh:134, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  7. refresh:537, AbstractApplicationContext (org.springframework.context.support)

  8. refresh:122, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  9. refresh:693, SpringApplication (org.springframework.boot)

  10. refreshContext:360, SpringApplication (org.springframework.boot)

  11. run:303, SpringApplication (org.springframework.boot)

  12. run:1118, SpringApplication (org.springframework.boot)

  13. run:1107, SpringApplication (org.springframework.boot)

  14. main:35, DubboConsumerDemo (com.alibaba.boot.dubbo.demo.consumer.bootstrap)

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

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

 
  1. public void await() {

  2. // ...

  3. if( port==-1 ) {

  4. try {

  5. awaitThread = Thread.currentThread();

  6. while(!stopAwait) {

  7. try {

  8. Thread.sleep( 10000 );

  9. } catch( InterruptedException ex ) {

  10. // continue and check the flag

  11. }

  12. }

  13. } finally {

  14. awaitThread = null;

  15. }

  16. return;

  17. }

  18. // ...

  19. }

在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的线程调用了该方法:

 
  1. stopAwait:390, StandardServer (org.apache.catalina.core)

  2. stopInternal:819, StandardServer (org.apache.catalina.core)

  3. stop:226, LifecycleBase (org.apache.catalina.util)

  4. stop:377, Tomcat (org.apache.catalina.startup)

  5. stopTomcat:241, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  6. stop:295, TomcatEmbeddedServletContainer (org.springframework.boot.context.embedded.tomcat)

  7. stopAndReleaseEmbeddedServletContainer:306, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  8. onClose:155, EmbeddedWebApplicationContext (org.springframework.boot.context.embedded)

  9. doClose:1014, AbstractApplicationContext (org.springframework.context.support)

  10. run:929, AbstractApplicationContext$2 (org.springframework.context.support)

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

 
  1. @Override

  2. public void registerShutdownHook() {

  3. if (this.shutdownHook == null) {

  4. // No shutdown hook registered yet.

  5. this.shutdownHook = new Thread() {

  6. @Override

  7. public void run() {

  8. synchronized (startupShutdownMonitor) {

  9. doClose();

  10. }

  11. }

  12. };

  13. Runtime.getRuntime().addShutdownHook(this.shutdownHook);

  14. }

  15. }

通过查阅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服务,那又是如何实现不退出的呢?我们将在下一篇文章中回答这个问题。

彩蛋

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

 
  1. @Test

  2. public void test() {

  3. new Thread(new Runnable() {

  4. @Override

  5. public void run() {

  6. try {

  7. Thread.sleep(1000000);

  8. } catch (InterruptedException e) {

  9. e.printStackTrace();

  10. }

  11. }

  12. }).start();

  13. }

[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


原文发布时间为:2018-10-20
本文作者:  Kirito的技术分享
本文来自云栖社区合作伙伴“ Kirito的技术分享”,了解相关信息可以关注“Kirito的技术分享”。

相关文章
|
19天前
|
人工智能 Java API
Spring AI与DeepSeek实战一:快速打造智能对话应用
在 AI 技术蓬勃发展的今天,国产大模型DeepSeek凭借其低成本高性能的特点,成为企业智能化转型的热门选择。而Spring AI作为 Java 生态的 AI 集成框架,通过统一API、简化配置等特性,让开发者无需深入底层即可快速调用各类 AI 服务。本文将手把手教你通过spring-ai集成DeepSeek接口实现普通对话与流式对话功能,助力你的Java应用轻松接入 AI 能力!虽然通过Spring AI能够快速完成DeepSeek大模型与。
342 11
|
24天前
|
存储 人工智能 开发框架
Spring AI Alibaba 应用框架挑战赛圆满落幕,恭喜获奖选手
第二届开放原子大赛 Spring AI Alibaba 应用框架挑战赛决赛于 2 月 23 日在北京圆满落幕。
|
20天前
|
人工智能 Java API
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
|
22天前
|
人工智能 Java API
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
|
6天前
|
缓存 NoSQL Java
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
111 79
|
1天前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 的使用
本文介绍了 Thymeleaf 在 Spring Boot 项目中的使用方法,包括访问静态页面、处理对象和 List 数据、常用标签操作等内容。通过示例代码展示了如何配置 404 和 500 错误页面,以及如何在模板中渲染对象属性和列表数据。同时总结了常用的 Thymeleaf 标签,如 `th:value`、`th:if`、`th:each` 等,并提供了官方文档链接以供进一步学习。
15 0
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 的使用
|
1月前
|
Java 数据库 开发者
详细介绍SpringBoot启动流程及配置类解析原理
通过对 Spring Boot 启动流程及配置类解析原理的深入分析,我们可以看到 Spring Boot 在启动时的灵活性和可扩展性。理解这些机制不仅有助于开发者更好地使用 Spring Boot 进行应用开发,还能够在面对问题时,迅速定位和解决问题。希望本文能为您在 Spring Boot 开发过程中提供有效的指导和帮助。
78 12
|
1天前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于注解的整合
本文介绍了Spring Boot集成MyBatis的两种方式:基于XML和注解的形式。重点讲解了注解方式,包括@Select、@Insert、@Update、@Delete等常用注解的使用方法,以及多参数时@Param注解的应用。同时,针对字段映射不一致的问题,提供了@Results和@ResultMap的解决方案。文章还提到实际项目中常结合XML与注解的优点,灵活使用两者以提高开发效率,并附带课程源码供下载学习。
13 0
|
1天前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
12 0
|
1天前
|
Java 数据库连接 数据库
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——MyBatis 介绍和配置
本文介绍了Spring Boot集成MyBatis的方法,重点讲解基于注解的方式。首先简述MyBatis作为持久层框架的特点,接着说明集成时的依赖导入,包括`mybatis-spring-boot-starter`和MySQL连接器。随后详细展示了`properties.yml`配置文件的内容,涵盖数据库连接、驼峰命名规范及Mapper文件路径等关键设置,帮助开发者快速上手Spring Boot与MyBatis的整合开发。
14 0

热门文章

最新文章