17条回复html
节前某个部门的测试环境反馈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 ProtocolHandler
从上面日志来能够判断:shell
由于正常关闭(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 -> destroy
而这又有两种状况,一是应用代码里有地方用System.exit
来退出jvm,二是系统发的信号(kill -9
除外,SIGKILL信号JVM不会有机会执行shutdownhook)tomcat
先经过排查代码,应用方和中间件团队都排查了System.exit
在这个应用中使用的可能。那就只剩下Signal的状况了;通过一番排查后,发现每次tomcat意外退出的时间与ssh会话结束的时间正好吻合。bash
有了这个线索以后,银时同窗马上看了一下对方测试环境的脚本,简化后以下:oracle
$ 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进程不会退出。ssh
这是一个有趣的现象,catalina.sh start
方式启动的tomcat会把java进程挂到init
(进程id为1)的父进程下,已经与当前test.sh
进程脱离了父子关系,也与ssh进程没有关系,为何关闭ssh终端窗口会致使java进程退出?jvm
咱们的推测是ssh窗口在关闭时,对当前交互的shell以及正在运行的test.sh等子进程发送某个退出的Signal,找了一台装有systemtap的机器来验证,所用的stap脚本是从涧泉同窗那里copy的:ide
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)
通过内核组伯俞的协助,咱们发现
SIGINT
信号SIGINT [ 0 11 ] -> [ 0 20629 tail ] SIGINT [ 0 11 ] -> [ 0 20628 java ] SIGINT [ 0 11 ] -> [ 0 20615 test.sh ] 注pid 11是events进程
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引发的以后,个人疑惑变成了:
SIGINT
(kill -2) 不会让tomcat进程退出?SIGHUP
(kill -1) 会让tomcat进程退出?我第一反应多是jvm在某些参数下(或由于某些jni)对os的信号处理会不一样,看了一下应用的jvm参数,没有看出问题,也排除了tomcat使用apr/tcnative的状况。
咱们看一下默认状况下,jvm进程对SIGINT
和SIGHUP
是怎么处理的,用scala的repl模拟一下:
scala> Runtime.getRuntime().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来模拟进程,当咱们在交互模式下:
$ 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
进程的。
如今咱们把sleep进程放到一个脚本里后台执行:
$ cat a.sh #!/bin/sh sleep 4400 & echo "shell exit"
运行a.sh脚本以后,sleep 4400
进程的pid与pgid是不一样的,pgid是其父进程的id,即已经退出了的a.sh进程
$ ps -opid,pgid,ppid,comm -p 63376 PID PGID PPID COMM 63376 63375 1 sleep
这时咱们用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(;;) { } return 0; }
咱们把编译后的a.out命令在脚本里之后台方式运行:
$ cat a.sh #!/bin/sh /tmp/a.out &
此次再尝试用kill -2
去杀a.out进程,是能够的。这说明shell对signal_handler
作手脚是在执行用户逻辑以前,也就是脚本在fork出子进程的时候就设置了。按照这个线索咱们google后了解到: shell在非交互模式下对后台进程处理SIGINT
信号时设置的是IGNORE
。
为何在交互模式下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也不会再向它发送信号