Tomcat 優雅關閉之路

vivo網際網路技術發表於2020-02-13

本文首發於 vivo網際網路技術 微信公眾號 
連結: https://mp.weixin.qq.com/s/ZqkmoAR4JEYr0x0Suoq7QQ
作者:馬運傑

本文透過閱讀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原始碼進行閱讀)。

(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的方式

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步:

其主要做了兩件事:

  • 初始化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命令檢視每個數值所代表的訊號量的值。

(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中的shutdownhook)

除了Tomcat會使用關閉鉤子外,很多中介軟體也會使用到這一非常重要的功能。

我們在平常的開發過程中也可以使用關閉鉤子,可以在程式啟動或者執行階段透過呼叫Runtime.getRuntime().addShutdownHook(shutdownHook)方法進行鉤子的新增,但要注意的是,需要在關閉的流程中加入移除鉤子的程式碼。

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

在Spring中,關閉鉤子是在AbstractApplicationContext.registerShutdownHook()方法中新增的(下圖中的程式碼),而其關閉鉤子的run方法則會呼叫destroyBeans()方法,其對所有繼承了DisposableBean介面的類呼叫其destroy()方法。

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

3、shutdownhook的使用注意點

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

  • shutdownhook的呼叫是不保證順序的

  • shutdownhook是JVM結束前呼叫的執行緒,所以該執行緒中的方法應儘量短,並且保證不能發生死鎖的情況,否則也會阻止JVM的正常退出

  • shutdownhook中不能執行System.exit(),否則會導致虛擬機器卡住,而不得不強行殺死程式

五、總結

本文對Tomcat兩種常用關閉方式的原理進行了解讀,從上述分析可以看出,用shutdown.sh指令碼控制Tomcat關閉的方式存在許可權的風險,並且也會由於開發中的執行緒操作導致Tomcat無法關閉,所以這種方法在實際應用中使用情況較少。

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

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2675115/,如需轉載,請註明出處,否則將追究法律責任。

相關文章