Tomcat進(jìn)程意外退出的問題分析

感謝同事宏江投遞本稿。

節(jié)前某個(gè)部門的測(cè)試環(huán)境反饋tomcat會(huì)意外退出,我們到實(shí)際環(huán)境排查后發(fā)現(xiàn)不是jvm crash,日志里有進(jìn)程銷毀的記錄,從pause到destory的整個(gè)過程:

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指令)

因?yàn)檎jP(guān)閉(viaport)的話會(huì)在 pause 之前有這樣的一句warn日志:

    org.apache.catalina.core.StandardServer await
    A valid shutdown command was received via the shutdown port. Stopping the Server instance.
    然后才是 pause -> stop -> destory 

2) tomcat的shutdownhook被觸發(fā),執(zhí)行了銷毀邏輯

而這又有兩種情況,一是應(yīng)用代碼里有地方用System.exit來退出jvm,二是系統(tǒng)發(fā)的信號(hào)(kill -9除外,SIGKILL信號(hào)JVM不會(huì)有機(jī)會(huì)執(zhí)行shutdownhook)

先通過排查代碼,應(yīng)用方和中間件團(tuán)隊(duì)都排查了System.exit在這個(gè)應(yīng)用中使用的可能。那就只剩下Signal的情況了;經(jīng)過一番排查后,發(fā)現(xiàn)每次tomcat意外退出的時(shí)間與ssh會(huì)話結(jié)束的時(shí)間正好吻合。

有了這個(gè)線索之后,銀時(shí)同學(xué)立刻看了一下對(duì)方測(cè)試環(huán)境的腳本,簡(jiǎn)化后如下:

$ cat test.sh
#!/bin/bash
cd /data/server/tomcat/bin/
./catalina.sh start
tail -f /data/server/tomcat/logs/catalina.out

tomcat啟動(dòng)為后,當(dāng)前shell進(jìn)程并沒有退出,而是掛住在tail進(jìn)程,往終端輸出日志內(nèi)容。這種情況下,如果用戶直接關(guān)閉ssh終端的窗口(用鼠標(biāo)或快捷鍵),則java進(jìn)程也會(huì)退出。而如果先ctrl-c終止test.sh進(jìn)程,然后再關(guān)閉ssh終端的話,則java進(jìn)程不會(huì)退出。

這是一個(gè)有趣的現(xiàn)象,catalina.sh start方式啟動(dòng)的tomcat會(huì)把java進(jìn)程掛到init(進(jìn)程id為1)的父進(jìn)程下,已經(jīng)與當(dāng)前test.sh進(jìn)程脫離了父子關(guān)系,也與ssh進(jìn)程沒有關(guān)系,為什么關(guān)閉ssh終端窗口會(huì)導(dǎo)致java進(jìn)程退出?

我們的推測(cè)是ssh窗口在關(guān)閉時(shí),對(duì)當(dāng)前交互的shell以及正在運(yùn)行的test.sh等子進(jìn)程發(fā)送某個(gè)退出的Signal,找了一臺(tái)裝有systemtap的機(jī)器來驗(yàn)證,所用的stap腳本是從澗泉同學(xué)那里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("");
    }
}

模擬時(shí)的進(jìn)程層級(jí)(pstree)大致如下,tomcat啟動(dòng)后java進(jìn)程已經(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 終止當(dāng)前test.sh進(jìn)程時(shí),系統(tǒng)events進(jìn)程向 java 和 tail 兩個(gè)進(jìn)程發(fā)送了SIGINT 信號(hào)
SIGINT [ 0 11  ] -> [ 0 20629 tail ] 
SIGINT [ 0 11  ] -> [ 0 20628 java ] 
SIGINT [ 0 11  ] -> [ 0 20615 test.sh ] 

注pid 11是events進(jìn)程

b) 關(guān)閉ssh終端窗口時(shí),sshd向下游進(jìn)程發(fā)送SIGHUP, 為何java進(jìn)程也會(huì)收到?
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é)助分析這個(gè)問題(他給出了一些猜測(cè),但后來證明并不是那樣)。

確定了是由signal引起的之后,我的疑惑變成了:

1) 為什么SIGINT (kill -2) 不會(huì)讓tomcat進(jìn)程退出?
2) 為什么SIGHUP (kill -1) 會(huì)讓tomcat進(jìn)程退出?

我第一反應(yīng)可能是jvm在某些參數(shù)下(或因?yàn)槟承﹋ni)對(duì)os的信號(hào)處理會(huì)不同,看了一下應(yīng)用的jvm參數(shù),沒有看出問題,也排除了tomcat使用apr/tcnative的情況。

我們看一下默認(rèn)情況下,jvm進(jìn)程對(duì)SIGINTSIGHUP是怎么處理的,用scala的repl模擬一下:

scala> Runtime.getRuntime().addShutdownHook(
            new Thread() { override def run() { println("ok") } })

對(duì)這個(gè)java進(jìn)程分別用kill -2kill -1發(fā)現(xiàn)都會(huì)導(dǎo)致jvm進(jìn)程退出,并且也觸發(fā)shutdownhook。這也符合oracle對(duì)hotspot虛擬機(jī)處理Signal的說明,參考這里SIGTERM,SIGINT,SIGHUP三種信號(hào)都會(huì)觸發(fā)shutdownhook

看來并不是jvm的事,繼續(xù)猜測(cè)是否與進(jìn)程的狀態(tài)有關(guān)?catalina.sh腳本里并沒有使用start-stop-daemon之類的方式啟動(dòng)java進(jìn)程,start參數(shù)的執(zhí)行方式簡(jiǎn)化后腳本相當(dāng)于:

eval '"/pathofjdk/bin/java"' 'params' org.apache.catalina.startup.Bootstrap start '&'

就是簡(jiǎn)單的把java放到后臺(tái)執(zhí)行。當(dāng)catalina.sh自身進(jìn)程退出后,java進(jìn)程的ppid變成了1

花了很多的時(shí)間猜測(cè)可能是OS層面的原因,后來發(fā)現(xiàn)并沒有關(guān)系。春節(jié)后回來讓少明和澗泉也一起分析這個(gè)問題,因?yàn)樗麄冇衏的背景,對(duì)系統(tǒng)底層知道的多一些,用了大半天時(shí)間,不斷猜測(cè)和驗(yàn)證,最后確認(rèn)了是Shell的原因。

SIGINT (kill -2) 不會(huì)讓后臺(tái)java進(jìn)程退出的原因

為了簡(jiǎn)便,我們用sleep來模擬進(jìn)程,當(dāng)我們?cè)诮换ツJ较拢?/p>

$ sleep 1000 & 

$ ps -opid,pgid,ppid,stat,cmd -C sleep
  PID  PGID  PPID STAT CMD
 9897  9897  9813 S    sleep 1000   

注意,進(jìn)程sleep 1000的pid與pgid(進(jìn)程組)是相同的,這時(shí)我們用kill -2是可以殺掉sleep 1000進(jìn)程的。

現(xiàn)在我們把sleep進(jìn)程放到一個(gè)腳本里后臺(tái)執(zhí)行:

$ cat a.sh
#!/bin/sh
sleep 4400 &
echo "shell exit"

運(yùn)行a.sh腳本之后,sleep 4400進(jìn)程的pid與pgid是不同的,pgid是其父進(jìn)程的id,即已經(jīng)退出了的a.sh進(jìn)程

$ ps -opid,pgid,ppid,comm -p 63376
  PID  PGID  PPID COMM
63376 63375     1 sleep

這時(shí)我們用kill -2是殺不掉sleep 4400進(jìn)程的。

到了這一步,已經(jīng)非常接近原因了,一定是shell對(duì)后臺(tái)進(jìn)程signal_handler做了什么手腳。少明實(shí)現(xiàn)了一個(gè)自定handler的命令看看是否對(duì)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命令在腳本里以后臺(tái)方式運(yùn)行:

$ cat a.sh
#!/bin/sh
/tmp/a.out &

這次再嘗試用kill -2去殺a.out進(jìn)程,是可以的。這說明shell對(duì)signal_handler做手腳是在執(zhí)行用戶邏輯之前,也就是腳本在fork出子進(jìn)程的時(shí)候就設(shè)置了。按照這個(gè)線索我們google后了解到: shell在非交互模式下對(duì)后臺(tái)進(jìn)程處理SIGINT信號(hào)時(shí)設(shè)置的是IGNORE。

交互模式與非交互模式對(duì)作業(yè)控制(job control)默認(rèn)方式不同

為什么在交互模式下shell不會(huì)對(duì)后臺(tái)進(jìn)程處理SIGINT信號(hào)設(shè)置為忽略,而非交互模式下會(huì)設(shè)置為忽略呢?還是比較好理解的,舉例來說,我們先某個(gè)前臺(tái)進(jìn)程運(yùn)行時(shí)間太長(zhǎng),可以ctrl-z中止一下,然后通過bg %n把這個(gè)進(jìn)程放入后臺(tái),同樣也可以把一個(gè)cmd &方式啟動(dòng)的后臺(tái)進(jìn)程,通過fg %n放回前臺(tái),然后在ctrl-c停止它,當(dāng)然不能忽略SIGINT。

為何交互模式下的后臺(tái)進(jìn)程會(huì)設(shè)置一個(gè)自己的進(jìn)程組ID呢?因?yàn)槟J(rèn)如果采用父進(jìn)程的進(jìn)程組ID,父進(jìn)程會(huì)把收到的鍵盤事件比如ctrl-c之類的SIGINT傳播給進(jìn)程組中的每個(gè)成員,假設(shè)后臺(tái)進(jìn)程也是父進(jìn)程組的成員,因?yàn)樽鳂I(yè)控制的需要不能忽略SIGINT,你在終端隨意ctrl-c就可能導(dǎo)致所有的后臺(tái)進(jìn)程退出,顯然這樣是不合理的;所以為了避免這種干擾后臺(tái)進(jìn)程設(shè)置為自己的pgid。

而非交互模式下,通常是不需要作業(yè)控制的,所以作業(yè)控制在非交互模式下默認(rèn)也是關(guān)閉的(當(dāng)然也可以在腳本里通過選項(xiàng)set -m打開作業(yè)控制選項(xiàng))。不開啟作業(yè)控制的話,腳本里的后臺(tái)進(jìn)程可以通過設(shè)置忽略SIGINT信號(hào)來避免父進(jìn)程對(duì)組中成員的傳播,因?yàn)閷?duì)它來說這個(gè)信號(hào)已經(jīng)沒有意義。

回到tomcat的例子,catalina.sh腳本通過start參數(shù)啟動(dòng)的時(shí)候,就是以非交互方式后臺(tái)啟動(dòng),java進(jìn)程也被shell設(shè)置了忽略SIGINT信號(hào),因此在ctrl-c結(jié)束test.sh進(jìn)程時(shí),系統(tǒng)發(fā)送的SIGINT對(duì)java沒有影響。

SIGHUP (kill -1) 讓tomcat進(jìn)程退出的原因

在非交互模式下,shell對(duì)java進(jìn)程設(shè)置了SIGINT,SIGQUIT信號(hào)設(shè)置了忽略,但并沒有對(duì)SIGHUP信號(hào)設(shè)為忽略。再看一下當(dāng)時(shí)的進(jìn)程層級(jí):

|-sshd(1622)-+-sshd(11681)---sshd(11699)---bash(11700)---test.sh(13285)---tail(13299)

sshd把SIGHUP傳遞給bash進(jìn)程后,bash會(huì)把SIGHUP傳遞給它的子進(jìn)程,并且對(duì)于其子進(jìn)程test.sh,bash還會(huì)對(duì)test.sh的進(jìn)程組里的成員都傳播一遍SIGHUP。因?yàn)閖ava后臺(tái)進(jìn)程從父進(jìn)程catalina.sh(又是從其父進(jìn)程test.sh)繼承的pgid,所以java進(jìn)程仍屬于test.sh進(jìn)程組里的成員,收到SIGHUP后退出。

如果我們?cè)趖est.sh里設(shè)置開啟作業(yè)控制的話,就不會(huì)讓java進(jìn)程退出了

#!/bin/bash
set -m  
cd /home/admin/tt/tomcat/bin/
./catalina.sh start
tail -f /home/admin/tt/tomcat/logs/catalina.out

此時(shí)java后臺(tái)進(jìn)程繼承父進(jìn)程catalina.sh的pgid,而catalina.sh不再使用test.sh的進(jìn)程組,而是自己的pid作為pgid,catalina.sh進(jìn)程在執(zhí)行完退出后,java進(jìn)程掛到了init下,java與test.sh進(jìn)程就完全脫離關(guān)系了,bash也不會(huì)再向它發(fā)送信號(hào)。

原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明: 轉(zhuǎn)載自并發(fā)編程網(wǎng) – ifeve.com本文鏈接地址: Tomcat進(jìn)程意外退出的問題分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容