当前位置: 移动技术网 > IT编程>开发语言>Java > Spring Boot Dubbo 应用启停源码分析

Spring Boot Dubbo 应用启停源码分析

2020年05月04日  | 移动技术网IT编程  | 我要评论

作者:张乎兴 来源:dubbo官方博客

背景介绍

dubbo spring boot 工程致力于简化 dubbo 框架在spring boot应用场景的开发。同时也整合了 spring boot 特性:

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

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

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 exitoperation is not forbidden by the security manager.

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

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

  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-0container-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](http://mp.weixin.qq.com/s?__biz=mzi3odcxmzqzmw==&mid=2247483916&idx=1&sn=89daf388da0d6fe40dc54e9a4018baeb&chksm=eb53873adc240e2cf55400f3261228d08fc943c4f196566e995681549c47630b70ac01b75031&scene=21#wechat_redirect) 类型变量,用于确保被另一个线程修改后,当前线程能够立即看到这个变化。如果没有变化,就会一直处于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服务,那又是如何实现不退出的呢?我们将在下一篇文章中回答这个问题。

彩蛋

intellijidea中运行了如下的单元测试,创建一个线程执行睡眠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]

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

关注java技术栈微信公众号,在后台回复关键字:dubbo,可以获取更多栈长整理的 dubbo 技术干货。

推荐去我的博客阅读更多:

1.

2.

3.

4.

觉得不错,别忘了点赞+转发哦!

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网