Spring Boot Dubbo 應用啓停源碼分析

做者:張乎興 來源:Dubbo官方博客

背景介紹

Dubbo Spring Boot 工程致力於簡化 Dubbo RPC 框架在Spring Boot應用場景的開發。同時也整合了 Spring Boot 特性:html

  • 自動裝配 (好比: 註解驅動, 自動裝配等).
  • Production-Ready (好比: 安全, 健康檢查, 外部化配置等).

DubboConsumer啓動分析

你有沒有想過一個問題? incubator-dubbo-spring-boot-project中的 DubboConsumerDemo應用就一行代碼, main方法執行完以後,爲何不會直接退出呢?java

@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章節中有清晰的描述:spring

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

  • 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種狀況:bootstrap

  1. 全部的非daemon進程徹底終止
  2. 某個線程調用了 System.exit()或 Runtime.exit()

所以針對上面的狀況,咱們判斷,必定是有某個非daemon線程沒有退出致使。咱們知道,經過jstack能夠看到全部的線程信息,包括他們是不是daemon線程,能夠經過jstack找出那些是非deamon的線程。c#

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沒有退出。如今咱們經過源碼,搜索一下究竟是誰建立的這兩個線程。api

經過對spring-boot的源碼分析,咱們在 org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerstartDaemonAwaitThread找到了以下代碼tomcat

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也會退出。所以須要顯式地啓動一個線程,在某個條件下進行持續等待,從而避免線程退出。Spring Boot 2.x 啓動全過程源碼分析(全),這篇文章推薦你們看下。

下面咱們在深挖一下,在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。 在 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] https://docs.oracle.com/javas...

[2] https://docs.oracle.com/javas...

關注Java技術棧微信公衆號,在後臺回覆關鍵字:dubbo,能夠獲取更多棧長整理的 Dubbo 技術乾貨。

推薦去個人博客閱讀更多:

1.Java JVM、集合、多線程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、後端、架構、阿里巴巴等大廠最新面試題

以爲不錯,別忘了點贊+轉發哦!

相關文章
相關標籤/搜索