關(guān)于 Tomcat進程意外退出的問題解析
節(jié)前某個部門的測試環(huán)境反饋tomcat會意外退出,我們到實際環(huán)境排查后發(fā)現(xiàn)不是jvm crash,日志里有進程銷毀的記錄,從pause到destory的整個過程:
org.apache.coyote.AbstractProtocol pause
Pausing ProtocolHandler
org.apache.catalina.core.StandardService stopInternal
Stopping service Catalina
org.apache.coyote.AbstractProtocol stop
Stopping ProtocolHandler
org.apache.coyote.AbstractProtocol destroy
Destroying ProtocolHandler
從上面日志來可以判斷:
1) tomcat不是通過腳本正常關(guān)閉(viaport: 即通過8005端口發(fā)送shutdown指令)
因為正常關(guān)閉(viaport)的話會在 pause 之前有這樣的一句warn日志:
org.apache.catalina.core.StandardServer await
A valid shutdown command was received via the shutdown port. Stopping the Server instance.
然后才是 pause -> stop -> destroy
2) tomcat的shutdownhook被觸發(fā),執(zhí)行了銷毀邏輯
而這又有兩種情況,一是應(yīng)用代碼里有地方用System.exit來退出jvm,二是系統(tǒng)發(fā)的信號(kill -9除外,SIGKILL信號JVM不會有機會執(zhí)行shutdownhook)
先通過排查代碼,應(yīng)用方和中間件團隊都排查了System.exit在這個應(yīng)用中使用的可能。那就只剩下Signal的情況了;經(jīng)過一番排查后,發(fā)現(xiàn)每次tomcat意外退出的時間與ssh會話結(jié)束的時間正好吻合。
有了這個線索之后,銀時同學立刻看了一下對方測試環(huán)境的腳本,簡化后如下:
$ cat test.sh #!/bin/bash cd /data/server/tomcat/bin/ ./catalina.sh start tail -f /data/server/tomcat/logs/catalina.out
tomcat啟動為后,當前shell進程并沒有退出,而是掛住在tail進程,往終端輸出日志內(nèi)容。這種情況下,如果用戶直接關(guān)閉ssh終端的窗口(用鼠標或快捷鍵),則java進程也會退出。而如果先ctrl-c終止test.sh進程,然后再關(guān)閉ssh終端的話,則java進程不會退出。
這是一個有趣的現(xiàn)象,catalina.sh start方式啟動的tomcat會把java進程掛到init(進程id為1)的父進程下,已經(jīng)與當前test.sh進程脫離了父子關(guān)系,也與ssh進程沒有關(guān)系,為什么關(guān)閉ssh終端窗口會導(dǎo)致java進程退出?
我們的推測是ssh窗口在關(guān)閉時,對當前交互的shell以及正在運行的test.sh等子進程發(fā)送某個退出的Signal,找了一臺裝有systemtap的機器來驗證,所用的stap腳本是從澗泉同學那里copy的:
function time_str: string () {
return ctime(gettimeofday_s() + 8 * 60 * 60);
}
probe begin {
printdln(" ", time_str(), "BEGIN");
}
probe end {
printdln(" ", time_str(), "END");
}
probe signal.send {
if (sig_name == "SIGHUP" || sig_name == "SIGQUIT" ||
sig_name=="SIGINT" || sig_name=="SIGKILL" || sig_name=="SIGABRT") {
printd(" ", time_str(), sig_name, "[", uid(), pid(), cmdline_str(),
"] -> [", task_uid(task), sig_pid, pid_name, "], ");
task = pid2task(pid());
while (task_pid(task) > 0) {
printd(" ", "[", task_uid(task), task_pid(task), task_execname(task), "]");
task = task_parent(task);
}
println("");
}
}
模擬時的進程層級(pstree)大致如下,tomcat啟動后java進程已經(jīng)脫離test.sh,掛在init下:
|-sshd(1622)-+-sshd(11681)---sshd(11699)---bash(11700)---test.sh(13285)---tail(13299)
經(jīng)過內(nèi)核組伯俞的協(xié)助,我們發(fā)現(xiàn)
a) 用 ctrl-c 終止當前test.sh進程時,系統(tǒng)events進程向 java 和 tail 兩個進程發(fā)送了SIGINT 信號
SIGINT [ 0 11 ] -> [ 0 20629 tail ] SIGINT [ 0 11 ] -> [ 0 20628 java ] SIGINT [ 0 11 ] -> [ 0 20615 test.sh ]
注pid 11是events進程
b) 關(guān)閉ssh終端窗口時,sshd向下游進程發(fā)送SIGHUP, 為何java進程也會收到?
SIGHUP [ 0 11681 sshd: hongjiang.wanghj [priv] ] -> [ 57316 11700 bash ] SIGHUP [ 57316 11700 -bash ] -> [ 57316 11700 bash ] SIGHUP [ 57316 11700 ] -> [ 0 13299 tail ] SIGHUP [ 57316 11700 ] -> [ 0 13298 java ] SIGHUP [ 57316 11700 ] -> [ 0 13285 test.sh ]
不過伯俞很忙沒有繼續(xù)協(xié)助分析這個問題(他給出了一些猜測,但后來證明并不是那樣)。
確定了是由signal引起的之后,我的疑惑變成了:
1) 為什么SIGINT (kill -2) 不會讓tomcat進程退出?
2) 為什么SIGHUP (kill -1) 會讓tomcat進程退出?
我第一反應(yīng)可能是jvm在某些參數(shù)下(或因為某些jni)對os的信號處理會不同,看了一下應(yīng)用的jvm參數(shù),沒有看出問題,也排除了tomcat使用apr/tcnative的情況。
我們看一下默認情況下,jvm進程對SIGINT和SIGHUP是怎么處理的,用scala的repl模擬一下:
scala> Runtime.getRuntime().addShutdownHook(
new Thread() { override def run() { println("ok") } })
對這個java進程分別用kill -2和kill -1發(fā)現(xiàn)都會導(dǎo)致jvm進程退出,并且也觸發(fā)shutdownhook。這也符合oracle對hotspot虛擬機處理Signal的說明,參考這里,SIGTERM,SIGINT,SIGHUP三種信號都會觸發(fā)shutdownhook
看來并不是jvm的事,繼續(xù)猜測是否與進程的狀態(tài)有關(guān)?catalina.sh腳本里并沒有使用start-stop-daemon之類的方式啟動java進程,start參數(shù)的執(zhí)行方式簡化后腳本相當于:
eval '"/pathofjdk/bin/java"' 'params' org.apache.catalina.startup.Bootstrap start '&'
就是簡單的把java放到后臺執(zhí)行。當catalina.sh自身進程退出后,java進程的ppid變成了1
花了很多的時間猜測可能是OS層面的原因,后來發(fā)現(xiàn)并沒有關(guān)系。春節(jié)后回來讓少明和澗泉也一起分析這個問題,因為他們有c的背景,對系統(tǒng)底層知道的多一些,用了大半天時間,不斷猜測和驗證,最后確認了是Shell的原因。
SIGINT (kill -2) 不會讓后臺java進程退出的原因
為了簡便,我們用sleep來模擬進程,當我們在交互模式下:
$ sleep 1000 & $ ps -opid,pgid,ppid,stat,cmd -C sleep PID PGID PPID STAT CMD 9897 9897 9813 S sleep 1000
注意,進程sleep 1000的pid與pgid(進程組)是相同的,這時我們用kill -2是可以殺掉sleep 1000進程的。
現(xiàn)在我們把sleep進程放到一個腳本里后臺執(zhí)行:
$ cat a.sh #!/bin/sh sleep 4400 & echo "shell exit"
運行a.sh腳本之后,sleep 4400進程的pid與pgid是不同的,pgid是其父進程的id,即已經(jīng)退出了的a.sh進程
$ ps -opid,pgid,ppid,comm -p 63376 PID PGID PPID COMM 63376 63375 1 sleep
這時我們用kill -2是殺不掉sleep 4400進程的。
到了這一步,已經(jīng)非常接近原因了,一定是shell對后臺進程signal_handler做了什么手腳。少明實現(xiàn)了一個自定handler的命令看看是否對kill -2有效:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void my_handler(int sig) {
printf("handler aaa\n");
exit(0);
}
int main() {
signal(SIGINT, my_handler);
for(;;) { }
return 0;
}
我們把編譯后的a.out命令在腳本里以后臺方式運行:
$ cat a.sh #!/bin/sh /tmp/a.out &
這次再嘗試用kill -2去殺a.out進程,是可以的。這說明shell對signal_handler做手腳是在執(zhí)行用戶邏輯之前,也就是腳本在fork出子進程的時候就設(shè)置了。按照這個線索我們google后了解到: shell在非交互模式下對后臺進程處理SIGINT信號時設(shè)置的是IGNORE。
交互模式與非交互模式對作業(yè)控制(job control)默認方式不同
為什么在交互模式下shell不會對后臺進程處理SIGINT信號設(shè)置為忽略,而非交互模式下會設(shè)置為忽略呢?還是比較好理解的,舉例來說,我們先某個前臺進程運行時間太長,可以ctrl-z中止一下,然后通過bg %n把這個進程放入后臺,同樣也可以把一個cmd &方式啟動的后臺進程,通過fg %n放回前臺,然后在ctrl-c停止它,當然不能忽略SIGINT。
為何交互模式下的后臺進程會設(shè)置一個自己的進程組ID呢?因為默認如果采用父進程的進程組ID,父進程會把收到的鍵盤事件比如ctrl-c之類的SIGINT傳播給進程組中的每個成員,假設(shè)后臺進程也是父進程組的成員,因為作業(yè)控制的需要不能忽略SIGINT,你在終端隨意ctrl-c就可能導(dǎo)致所有的后臺進程退出,顯然這樣是不合理的;所以為了避免這種干擾后臺進程設(shè)置為自己的pgid。
而非交互模式下,通常是不需要作業(yè)控制的,所以作業(yè)控制在非交互模式下默認也是關(guān)閉的(當然也可以在腳本里通過選項set -m打開作業(yè)控制選項)。不開啟作業(yè)控制的話,腳本里的后臺進程可以通過設(shè)置忽略SIGINT信號來避免父進程對組中成員的傳播,因為對它來說這個信號已經(jīng)沒有意義。
回到tomcat的例子,catalina.sh腳本通過start參數(shù)啟動的時候,就是以非交互方式后臺啟動,java進程也被shell設(shè)置了忽略SIGINT信號,因此在ctrl-c結(jié)束test.sh進程時,系統(tǒng)發(fā)送的SIGINT對java沒有影響。
SIGHUP (kill -1) 讓tomcat進程退出的原因
在非交互模式下,shell對java進程設(shè)置了SIGINT,SIGQUIT信號設(shè)置了忽略,但并沒有對SIGHUP信號設(shè)為忽略。再看一下當時的進程層級:
|-sshd(1622)-+-sshd(11681)---sshd(11699)---bash(11700)---test.sh(13285)---tail(13299)
sshd把SIGHUP傳遞給bash進程后,bash會把SIGHUP傳遞給它的子進程,并且對于其子進程test.sh,bash還會對test.sh的進程組里的成員都傳播一遍SIGHUP。因為java后臺進程從父進程catalina.sh(又是從其父進程test.sh)繼承的pgid,所以java進程仍屬于test.sh進程組里的成員,收到SIGHUP后退出。
如果我們在test.sh里設(shè)置開啟作業(yè)控制的話,就不會讓java進程退出了
#!/bin/bash set -m cd /home/admin/tt/tomcat/bin/ ./catalina.sh start tail -f /home/admin/tt/tomcat/logs/catalina.out
此時java后臺進程繼承父進程catalina.sh的pgid,而catalina.sh不再使用test.sh的進程組,而是自己的pid作為pgid,catalina.sh進程在執(zhí)行完退出后,java進程掛到了init下,java與test.sh進程就完全脫離關(guān)系了,bash也不會再向它發(fā)送信號。
以上所述是小編給大家介紹的關(guān)于 Tomcat進程意外退出的問題解析,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復(fù)大家的!
相關(guān)文章
liunx下centos7中tomcat報錯訪問域名超時的問題解決
本文主要介紹了liunx下centos7中tomcat報錯訪問域名超時,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-02-02
在Win10上安裝Tomcat服務(wù)器及配置環(huán)境變量的詳細教程(圖文)
Tomcat 服務(wù)器是一個免費的開放源代碼的 Web 應(yīng)用服務(wù)器,屬于輕量級應(yīng)用服務(wù)器,本文主要講述Windows環(huán)境Tomcat服務(wù)器安裝與環(huán)境變量配置,感興趣的朋友跟隨小編一起看看吧2019-11-11
idea發(fā)布web項目后Tomcat服務(wù)器找不到該項目的問題及解決方法
這篇文章主要介紹了idea發(fā)布web項目后Tomcat服務(wù)器找不到該項目,本文給大家分享解決方案,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
騰訊云申請免費ssl證書配置tomcat使http變https
這篇文章主要介紹了騰訊云申請免費ssl證書配置tomcat使http變https,詳細的介紹了每個步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
CentOS設(shè)置IP連接網(wǎng)絡(luò)實現(xiàn)過程圖解
這篇文章主要介紹了CentOS設(shè)置IP連接網(wǎng)絡(luò)實現(xiàn)過程圖解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09

