Tomcat進程意外退出的問題分析

節前某個部門的測試環境反饋tomcat會意外退出,咱們到實際環境排查後發現不是jvm crash,日誌裏有進程銷燬的記錄,從pause到destory的整個過程:java

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 ProtocolHandlershell

從上面日誌來能夠判斷: 1) tomcat不是經過腳本正常關閉(viaport: 即經過8005端口發送shutdown指令),由於正常關閉(viaport)的話會在 pause 以前有這樣的一句warn日誌:apache

org.apache.catalina.core.StandardServer await A valid shutdown command was received via the shutdown port. Stopping the Server instance. 而後纔是 pause -> stop -> destorytomcat

2) tomcat的shutdownhook被觸發,執行了銷燬邏輯,而這又有兩種狀況,一是應用代碼裏有地方用System.exit來退出jvm,二是系統發的信號(kill -9除外,SIGKILL信號JVM不會有機會執行shutdownhook)。bash

先經過排查代碼,應用方和中間件團隊都排查了System.exit在這個應用中使用的可能。那就只剩下Signal的狀況了;通過一番排查後,發現每次tomcat意外退出的時間與ssh會話結束的時間正好吻合。ssh

有了這個線索以後,馬上看了一下對方測試環境的腳本,簡化後以下:jvm

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

tomcat啓動後,當前shell進程並無退出,而是掛住在tail進程,往終端輸出日誌內容。這種狀況下,若是用戶直接關閉ssh終端的窗口(用鼠標或快捷鍵),則java進程也會退出。而若是先ctrl-c終止test.sh進程,而後再關閉ssh終端的話,則java進程不會退出。ui

這是一個有趣的現象,catalina.sh start方式啓動的tomcat會把java進程掛到init(進程id爲1)的父進程下,已經與當前test.sh進程脫離了父子關係,也與ssh進程沒有關係,爲何關閉ssh終端窗口會致使java進程退出?日誌

咱們的推測是ssh窗口在關閉時,對當前交互的shell以及正在運行的test.sh等子進程發送某個退出的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進程已經脫離test.sh,掛在init下:

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

最終,咱們發現: a) 用 ctrl-c 終止當前test.sh進程時,系統events進程向 java 和 tail 兩個進程發送了SIGINT 信號

SIGINT [ 0 11 ] -> [ 0 20629 tail ] SIGINT [ 0 11 ] -> [ 0 20628 java ] SIGINT [ 0 11 ] -> [ 0 20615 test.sh ] 注pid 11是events進程

b) 關閉ssh終端窗口時,sshd向下遊進程發送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 ]

肯定了是由signal引發的以後,個人疑惑變成了:

  1. 爲何SIGINT (kill -2) 不會讓tomcat進程退出?
  2. 爲何SIGHUP (kill -1) 會讓tomcat進程退出?

**shell在非交互模式下對後臺進程處理SIGINT信號時設置的是IGNORE。**交互模式與非交互模式對做業控制(job control)默認方式不一樣。

爲何在交互模式下shell不會對後臺進程處理SIGINT信號設置爲忽略,而非交互模式下會設置爲忽略呢?仍是比較好理解的,舉例來講,咱們先某個前臺進程運行時間太長,能夠ctrl-z停止一下,而後經過bg %n把這個進程放入後臺,一樣也能夠把一個cmd &方式啓動的後臺進程,經過fg %n放回前臺,而後在ctrl-c中止它,固然不能忽略SIGINT。

爲什麼交互模式下的後臺進程會設置一個本身的進程組ID呢?由於默認若是採用父進程的進程組ID,父進程會把收到的鍵盤事件好比ctrl-c之類的SIGINT傳播給進程組中的每一個成員,假設後臺進程也是父進程組的成員,由於做業控制的須要不能忽略SIGINT,你在終端隨意ctrl-c就可能致使全部的後臺進程退出,顯然這樣是不合理的;因此爲了不這種干擾後臺進程設置爲本身的pgid。

而非交互模式下,一般是不須要做業控制的,因此做業控制在非交互模式下默認也是關閉的(固然也能夠在腳本里經過選項set -m打開做業控制選項)。不開啓做業控制的話,腳本里的後臺進程能夠經過設置忽略SIGINT信號來避免父進程對組中成員的傳播,由於對它來講這個信號已經沒有意義。

回到tomcat的例子,catalina.sh腳本經過start參數啓動的時候,就是以非交互方式後臺啓動,java進程也被shell設置了忽略SIGINT信號,所以在ctrl-c結束test.sh進程時,系統發送的SIGINT對java沒有影響。

SIGHUP (kill -1) 讓tomcat進程退出的緣由。

在非交互模式下,shell對java進程設置了SIGINT,SIGQUIT信號設置了忽略,但並無對SIGHUP信號設爲忽略。再看一下當時的進程層級:

|-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裏設置開啓做業控制的話,就不會讓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進程在執行完退出後,java進程掛到了init下,java與test.sh進程就徹底脫離關係了,bash也不會再向它發送信號。

相關文章
相關標籤/搜索