Tomcat进程意外退出的问题分析
Tomcat进程意外退出的问题分析
节前某个部门的测试环境反馈tomcat会意外退出,我们到实际环境排查后发现不是jvm crash,⽇志⾥有进程销毁的记录,从pause到destory的整个过程:
AbstractProtocol pause
Pausing ProtocolHandler
org.StandardService stopInternal
Stopping service Catalina
AbstractProtocol stop
Stopping ProtocolHandler
AbstractProtocol destroy
Destroying ProtocolHandler
从上⾯⽇志来可以判断:
1. tomcat不是通过脚本正常关闭(viaport: 即通过8005端⼝发送shutdown指令)
因为正常关闭(viaport)的话会在 pause 之前有这样的⼀句warn⽇志:
org.StandardServer await
A valid shutdown command was received via the shutdown port. Stopping the Server instance.
然后才是 pause -> stop -> destory
2. tomcat的shutdownhook被触发,执⾏了销毁逻辑
⽽这⼜有两种情况,⼀是应⽤代码⾥有地⽅⽤it来退出jvm,⼆是系统发的信号(kill -9除外,SIGKILL信号JVM不会有机会执⾏shutdownhook)
先通过排查代码,应⽤⽅和中间件团队都排查了it在这个应⽤中使⽤的可能。那就只剩下Signal的情况了;经过⼀番排查后,发现每次tomcat意外退出的时间与ssh会话结束的时间正好吻合。
有了这个线索之后,银时同学⽴刻看了⼀下对⽅测试环境的脚本,简化后如下:
$ 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进程不会退出。
这是⼀个有趣的现象,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 [011] ->[020629tail]
SIGINT [011] ->[020628 java ]
SIGINT [011] ->[020615 test.sh ]
注pid 11是events进程
b) 关闭ssh终端窗⼝时,sshd向下游进程发送SIGHUP, 为何java进程也会收到?
为什么使用bootstrap?
SIGHUP [011681 sshd: hongjiang.wanghj [priv]] ->[5731611700bash]
SIGHUP [5731611700 -bash ] ->[5731611700bash]
SIGHUP [5731611700] ->[013299tail]
SIGHUP [5731611700] ->[013298 java ]
SIGHUP [5731611700] ->[013285 test.sh ]
不过伯俞很忙没有继续协助分析这个问题(他给出了⼀些猜测,但后来证明并不是那样)。
确定了是由signal引起的之后,我的疑惑变成了:
1. 为什么SIGINT (kill -2) 不会让tomcat进程退出?
2. 为什么SIGHUP (kill -1) 会让tomcat进程退出?
我第⼀反应可能是jvm在某些参数下(或因为某些jni)对os的信号处理会不同,看了⼀下应⽤的jvm参数,没有看出问题,也排除了tomcat使⽤apr/tcnative的情况。
我们看⼀下默认情况下,jvm进程对SIGINT和SIGHUP是怎么处理的,⽤scala的repl模拟⼀下:
scala> Runtime().addShutdownHook(
new Thread() { override def run() { println(“ok”) } })
对这个java进程分别⽤kill -2和kill -1发现都会导致jvm进程退出,并且也触发shutdownhook。这也符合oracle对hotspot虚拟机处理Signal的说明,参考这⾥,SIGTERM,SIGINT,SIGHUP三种信号都会触发shutdownhook
看来并不是jvm的事,继续猜测是否与进程的状态有关?catalina.sh脚本⾥并没有使⽤start-stop-daemon之类的⽅式启动java进
程,start参数的执⾏⽅式简化后脚本相当于:
eval ‘"/pathofjdk/bin/java"’ ‘params’ org.apache.catalina.startup.Bootstrap start ‘&’
就是简单的把java放到后台执⾏。当catalina.sh⾃⾝进程退出后,java进程的ppid变成了1
花了很多的时间猜测可能是OS层⾯的原因,后来发现并没有关系。春节后回来让少明和涧泉也⼀起分析这个问题,因为他们有c的背景,对系统底层知道的多⼀些,⽤了⼤半天时间,不断猜测和验证,最后确认了是Shell的原因。
SIGINT (kill -2) 不会让后台java进程退出的原因
为了简便,我们⽤sleep来模拟进程,当我们在交互模式下:
$ sleep1000&
$ ps -opid,pgid,ppid,stat,cmd -C sleep
PID  PGID  PPID STAT CMD
989798979813 S    sleep1000
注意,进程sleep 1000的pid与pgid(进程组)是相同的,这时我们⽤kill -2是可以杀掉sleep 1000进程的。
现在我们把sleep进程放到⼀个脚本⾥后台执⾏:
$ cat a.sh
#!/bin/sh
sleep4400&
echo"shell exit"
运⾏a.sh脚本之后,sleep 4400进程的pid与pgid是不同的,pgid是其⽗进程的id,即已经退出了的a.sh进程
$ ps -opid,pgid,ppid,comm -p 63376
PID  PGID  PPID COMM
63376633751sleep
这时我们⽤kill -2是杀不掉sleep 4400进程的。
到了这⼀步,已经⾮常接近原因了,⼀定是shell对后台进程signal_handler做了什么⼿脚。少明实现了⼀个⾃定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(;;){}
return0;
}
我们把编译后的a.out命令在脚本⾥以后台⽅式运⾏:
$ cat a.sh
#!/bin/sh
/
tmp/a.out &
这次再尝试⽤kill -2去杀a.out进程,是可以的。这说明shell对signal_handler做⼿脚是在执⾏⽤户逻辑之前,也就是脚本在fork出⼦进程的时候就设置了。按照这个线索我们google后了解到: 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也不会再向它发送信号。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。