天天看點

Tomcat 優雅關閉之路

本文首發于 vivo網際網路技術 微信公衆号 

作者:馬運傑

本文通過閱讀Tomcat啟動和關閉流程的源碼,深入分析不同的Tomcat關閉方式背後的原理,讓開發人員能夠了解在使用不同的關閉方式時需要注意的點,避免因JVM程序異常退出導緻的各種非預見性錯誤。

一、 Tomcat的啟動過程

要了解Tomcat關閉的原理,首先需要關注下Tomcat是如何啟動的。這裡我們簡單介紹下。

Tomcat啟動的入口是Bootstrap類中的main方法,而後根據server.xml中的配置,對Server、Service、Enigin、Connector、Host、Context等元件進行初始化,之後便是啟動這些元件。我們重點來看下啟動之後,Tomcat做了哪些工作。

在Tomcat的各元件啟動完畢之後,main主線程會進入Catalina.out的await()方法,而此方法又是主要調用了Server元件的await()方法,從名字便可以看出,這個方法的目的是為了阻塞目前線程(main主線程)。

分析await的源碼(源碼比較長,這裡截取了部分,全部的可以自行拉取Tomcat源碼進行閱讀)。

Tomcat 優雅關閉之路

(StandardServer.await())

我們發現await()方法主要是根據server.xml中Server節點port屬性的設定做了以下幾種工作:

  • port為-2時,函數直接退出,此時主線程不會阻塞。
  • port為-1時,将等待線程設定為目前線程,并且進入while循環,直到stopAwait标志位置為true
  • port為其他時,則會建立一個socket服務端,該socket綁定了目前伺服器的ip以及port端口,随後設定等待線程為目前線程,并且socket進入阻塞監聽狀态,直到socket監聽到server.xml中預置的關閉字元串(預設是"SHUTDOWN")

在主線程退出等待後,就會進入Tomcat的關閉流程,進行各個元件的stop和destroy操作。從上述分析可以看出,要想停止Tomcat,就是要中斷main主線程的等待狀态。

下圖為Tomcat的整個生命周期。

Tomcat 優雅關閉之路

(Tomcat生命周期)

二、常見的關閉Tomcat的方式

1、我們下載下傳的Tomcat壓縮包的bin目錄下,有一個由官方提供的腳本(shutdown.sh),可以用來結束Tomcat程序。

2、伺服器上,我們還可以利用kill -x指令來結束Tomcat程序。

3、此外,代碼中的System.exit()以及OOM等異常情況的發生,也會導緻Tomcat程序的關閉,但是這兩者都不是正常的運維手段,在此我們不做分析。

三、shutdown腳本

1、shutdown.sh的原理

檢視分析官方的shutdown.sh腳本以及catalina.sh腳本,發現這兩個腳本最終是在調用Bootstrap類的main方法,和啟動Tomcat時調用的是同一個方法,差異在于傳入了"stop"作為main方法的參數,而傳入了該參數的main方法,會調用Catalina類的stopServer()方法。在此我們抹去不需要關注的代碼,可以把整個stopServer()方法簡化為如下4步:

Tomcat 優雅關閉之路

其主要做了兩件事:

  • 初始化Server元件,和Tomcat啟動時類似,這一步主要是解析server.xml檔案,然後根據server.xml中的屬性初始化Tomcat元件的成員變量,這裡主要關注Server元件的幾個成員變量:port、address、shutdown,預設值分别為8005、127.0.0.1、SHUTDOWN等,需要和啟動時讀取的server.xml保持一緻。
  • 往address port所監聽的Socket端口發生“SHUTDOWN”字元串。

至此,顯而易見的,這對應了第一小節中的第三種阻塞情況,"SHUTDOWN"字元串讓main主線程結束了等待狀态,并在接下來通過調用各元件的stop()和destroy()方法進行資源的釋放。

2、shutdown腳本的缺點

雖然shutdown腳本是由Tomcat官方出品,但是其在實際應用中并不廣泛,主要是由于下面兩個缺點:

  • 從上述原理就可以分析出,shutdown腳本是基于啟動時監聽了相應的端口,這就允許任意人員,隻要能夠發送"SHUTDOWN"字元串到相應的端口,就可以對Tomcat程序進行關閉,這對于生産環境是相當危險的。是以一般生産環境會将Server的port屬性設定為-1
  • shutdown腳本隻是結束了main主線程的等待狀态,讓其正常的走下去。我們知道,JVM中的線程分為守護線程和使用者線程兩種類型,守護線程會在所有使用者線程結束後,自動回收,進而導緻JVM程序的退出。main主線程是一個使用者線程,但是随着程式越來越複雜,可能會出現很多其他的使用者線程。比如我們平常開發過程中,常用的建立線程池的操作Executors.newFixedThreadPool(n) 便會建立n個使用者線程,這些線程在main主線程退出後,并不會自動回收,進而阻止了JVM的正常退出。是以經常會發生調用了shutdown腳本,但是Tomcat程序無法退出的場景。

四、kill -x

1、kill -9 or kill -15

Linux中的kill -x操作是向目标程序發送對應的信号量。可以用kill -l指令檢視每個數值所代表的信号量的值。

Tomcat 優雅關閉之路

(kill信号量)

這裡面,我們經常會使用kill-9這一指令,kill -9會立即強制結束目前程序,這個操作既友善,但同時也極具破壞性。在實際的環境中,我們可能有在running的任務,如果此時程式被強制關閉,便會導緻目前任務資料的丢失,特别是時間特别長的任務,極有可能造成前功盡棄的局面。同時,如果程式設計不當,沒有相應的幂等操作,還有可能會造成實際環境中資料缺失或者髒資料的産生,對生産環境造成緻命的問題。

相比kill -9, kill -15(15隻是一個例子,Linux中還有其他的中斷信号)會相對優雅很多。kill -15是向程序發送一個TERM的中斷信号量,在JVM接收到該信号量後,會響應中斷,進而結束目前程序。而這一操作能夠優雅關閉Tomcat的原因在于,JVM在結束目前程序前,會啟動一系列名為shutdownhook(關閉鈎子)的線程,而這些線程就會成為我們進行風險控制的工具。接下來我們首先看看Tomcat中的關閉鈎子。

2、shutdownhook關閉鈎子

Tomcat的關閉鈎子的定義是在Catalina類中,有一個名為CatalinaShutdownHook内部類,繼承了Thread類。跟着這個線程類中的run()方法往下看,其調用了Catalina的stop()方法,而此處stop方法,除了正常去停止各元件外,還會去中斷并快速結束main主線程(如果主線程還存在的話),最後再調用各元件的destroy()方法進行資源釋放。

Tomcat 優雅關閉之路

(Tomcat中的shutdownhook)

除了Tomcat會使用關閉鈎子外,很多中間件也會使用到這一非常重要的功能。

我們在平常的開發過程中也可以使用關閉鈎子,可以在程式啟動或者運作階段通過調用Runtime.getRuntime().addShutdownHook(shutdownHook)方法進行鈎子的添加,但要注意的是,需要在關閉的流程中加入移除鈎子的代碼。

Spring中當然也有關閉鈎子的應用,并且還為我們使用關閉鈎子提供了更為友好的程式設計體驗。

在Spring中,關閉鈎子是在AbstractApplicationContext.registerShutdownHook()方法中添加的(下圖中的代碼),而其關閉鈎子的run方法則會調用destroyBeans()方法,其對所有繼承了DisposableBean接口的類調用其destroy()方法。

讀到這裡我們就明白了,在平時開發時,如果有使用關閉鈎子的需求,可以通過繼承DisposableBean,并實作其destroy(),很友善的來達到我們回收資源,打掃戰場的目的。

Tomcat 優雅關閉之路

3、shutdownhook的使用注意點

shutdownhook在使用中也并不是可以随意亂用的,需要注意以下幾點:

  • shutdownhook的調用是不保證順序的
  • shutdownhook是JVM結束前調用的線程,是以該線程中的方法應盡量短,并且保證不能發生死鎖的情況,否則也會阻止JVM的正常退出
  • shutdownhook中不能執行System.exit(),否則會導緻虛拟機卡住,而不得不強行殺死程序

五、總結

本文對Tomcat兩種常用關閉方式的原理進行了解讀,從上述分析可以看出,用shutdown.sh腳本控制Tomcat關閉的方式存在權限的風險,并且也會由于開發中的線程操作導緻Tomcat無法關閉,是以這種方法在實際應用中使用情況較少。

而kill -15則能夠安全的殺死Tomcat程序,并且由于JVM shutdownhook的存在,我們可以對整個程式關閉時進行更強有力的控制,退出過程也更為優雅,是以使用較為廣泛。