天天看點

Spring-boot+Dubbo應用啟停源碼分析

背景介紹

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-0

container-1

導緻JVM沒有退出。現在我們通過源碼,搜尋一下到底是誰建立的這兩個線程。

通過對spring-boot的源碼分析,我們在

org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer

startDaemonAwaitThread

找到了如下代碼

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-0

container-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