1.Signal的分类
Signal我理解为进程与进程之间的通讯信号, 或者同一个进程内的处理信号, 这个信号的作用比较单一, 一般是为了关闭进程或者是触发某一个函数(如异常捕获).
1.1 Windows和Linux的Signal区别
Windows的Signal相对少一些, 如下:
ABRT SIGABRT
FPE SIGFPE
SEGV SIGSEGV
INT SIGINT
TERM SIGTERM
ILL SIGILL
Linux的Signal比较多, 如下:
HUP SIGHUP
INT SIGINT
QUIT SIGQUIT
ILL SIGILL
TRAP SIGTRAP
ABRT SIGABRT
IOT SIGIOT (*)
EMT SIGEMT (*)
FPE SIGFPE
KILL SIGKILL
BUS SIGBUS
SEGV SIGSEGV
SYS SIGSYS
PIPE SIGPIPE
ALRM SIGALRM
TERM SIGTERM
STKFLT SIGSTKFLT
USR1 SIGUSR1
USR2 SIGUSR2
CHLD SIGCHLD
PWR SIGPWR
WINCH SIGWINCH
URG SIGURG
POLL SIGPOLL
IO SIGIO
STOP SIGSTOP
TSTP SIGTSTP
CONT SIGCONT
TTIN SIGTTIN
TTOU SIGTTOU
VTALRM SIGVTALRM
PROF SIGPROF
XCPU SIGXCPU
XFSZ SIGXFSZ
UNUSED SIGUNUSED
SWI SIGSWI
Linux中的Signal可以由kill
命令发起, 比如kill -1 [pid]
是对某一个进程发出SIGHUP信息.
1.2 JVM会处理的Signal
不是每个Signal在JVM中都会被处理, 只有部分的Signal会被处理.
JVM 所使用的信号:
信号的类型为异常、错误、中断和控制。
表 1
-
异常
无论何时出现无法恢复的情况,操作系统都会同步发出一个相应的异常信号。
-
错误
如果 JVM 检测到不能从中恢复的情形,它会发出 SIGABRT。
-
中断
将从 JVM 进程外部异步发出中断信号以请求关闭。
-
控制
以JVM 为控制目的而使用的其他信号。
信号名称 | 信号类型 | 描述 | 是否被 -Xrs 禁用 | 是否被 -Xrs:sync 禁用 |
---|---|---|---|---|
SIGBUS (7) | 异常 | 访问内存不正确(数据未对准) | 是 | 是 |
SIGSEGV (11) | 异常 | 访问内存不正确(写到不可访问的内存) | 是 | 是 |
SIGILL (4) | 异常 | 非法指令(尝试调用未知的机器指令) | 是 | 是 |
SIGFPE (8) | 异常 | 浮点异常(除数为零) | 是 | 是 |
SIGABRT (6) | 错误 | 异常终止。无论何时检测到 JVM 错误,JVM 都发出该信号。 | 是 | 是 |
SIGINT (2) | 中断 | 交互式注意信号(CTRL-C)。JVM 正常退出。 | 是 | 否 |
SIGTERM (15) | 中断 | 终止请求。JVM 将正常退出。 | 是 | 否 |
SIGHUP (1) | 中断 | 挂起。JVM 正常退出。 | 是 | 否 |
SIGQUIT (3) | 控制 | 终端的退出信号。缺省情况下,这将触发 Javadump。 | 是 | 否 |
SIGTRAP(5) | 控制 | 由 JIT 使用。 | 是 | 是 |
SIGRTMIN (32) | 控制 | 由 JVM 用于内部控制目的。 | 否 | 否 |
SIGRTMAX (64) | 控制 | 由 SDK 使用。 | 否 | 否 |
SIGCHLD (17) | 控制 | 由 SDK 用于内部控制。 | 否 | 否 |
注:
信号名称后提供的数字是该信号的标准数值。
使用 -Xrs(减少信号使用)选项来防止 JVM 处理大多数的信号。有关更多信息,请参阅 Oracle 的 Java™ 应用程序启动程序页面 。
JVM 线程上的信号 1(SIGHUP)、2(SIGINT)、4(SIGILL)、7(SIGBUS)、8(SIGFPE)、11(SIGSEGV)和 15(SIGTERM)导致 JVM 关闭;因此,应用程序信号处理程序不应该尝试从这些信号恢复,除非它不再需要 JVM。
以上表格引用原文链接:
至于JVM是如何处理这些Signal的, 请参考以下链接:
1.3 JVM中的自定义SignalHandler
除了JVM默认处理Signal的行为, 我们还可以自定义SignalHandler
来做一些额外的工作, 比如在关闭JVM之前做一些回收或记录的事情.
例子:
import sun.misc.Signal;
import sun.misc.SignalHandler;
import java.lang.reflect.*;
public class Main
{
public static void main(String []args) {
DebugSignalHandler.listenTo("HUP");
DebugSignalHandler.listenTo("INT");
DebugSignalHandler.listenTo("KILL");
DebugSignalHandler.listenTo("TERM");
while (true) {
try {
Thread.sleep(1000);
}
catch(InterruptedException e) {
}
}
}
}
class DebugSignalHandler implements SignalHandler
{
public static void listenTo(String name) {
Signal signal = new Signal(name);
Signal.handle(signal, new DebugSignalHandler());
}
public void handle(Signal signal) {
System.out.println("Signal: " + signal);
if (signal.toString().trim().equals("SIGTERM")) {
System.out.println("SIGTERM raised, terminating...");
System.exit(1);
}
}
}
2.深入JVM关闭与关闭钩子
前面说到, Signal的一大作用是关闭进程, 然而Java提供了Shutdown Hook(关闭钩子)机制,它让我们在程序正常退出或者发生异常时能有机会做一些清场工作。
关闭钩子使用的方法也很简单,Runtime.getRuntime().addShutdownHook(Thread hook)
即可。关闭钩子其实可以看成是一个已经初始化了的但还没启动的线程,当JVM关闭时会并发地执行注册的所有关闭钩子。
JVM的关闭方式可以分为三种:
- 正常关闭:当最后一个非守护线程结束或者调用了System.exit或者通过其他特定平台的方法关闭(发送SIGINT,SIGTERM信号等)
- 强制关闭:通过调用Runtime.halt方法或者是在操作系统中直接kill(发送SIGKILL信号)掉JVM进程
- 异常关闭:运行中遇到RuntimeException异常, OOM错误等。
注意: Hook线程在JVM 正常关闭才会执行,在强制关闭时不会执行。(异常关闭没试过, 有空试一下..)
另外在使用关闭钩子还要注意以下几点:
- 不能在钩子调用
System.exit()
,否则卡住JVM的关闭过程,但是可以调用Runtime.halt()
。 - 不能再钩子中再进行钩子的添加和删掉操作,否则将会抛出IllegalStateException。
- 在
System.exit()
之后添加的钩子无效。 - 当JVM收到SIGTERM命令(比如操作系统在关闭时)后,如果钩子线程在一定时间没有完成,那么Hook线程可能在执行过程中被终止。
- Hool线程中同样会抛出异常,如果抛出异常又不处理,那么钩子的执行序列就会被停止。
Spring在初始化容器的时候就会注册一个hook线程用于清理容器.
[图片上传失败...(image-3b9611-1513089974690)]
3.客服后台系统JVM崩溃的案例
3.1 现象
JVM进程已经不在了, 重启后, 几分钟到半小时之间, 会看到获取不到spring bean的错误日志, 同时系统服务异常.
[图片上传失败...(image-cfc2ef-1513089974690)]
奇怪的是, 2台集群中的其他一台一直都是稳定运行, 只有这台是一直异常状态.
3.2 现场分析
从以上的日志, 可以看出spring容器已经在销毁中了, 感觉是一个正常的关闭系统的流程.
在监控系统(Marvin)中观察了内存的情况, 没有什么波动, 基本排除了oom的情况.
接下来, 我使用jstack输出了当时的线程栈信息, 保留现场痕迹.
[图片上传失败...(image-508b17-1513089974690)]
由上图所示, 从jstack日志中发现了2个关键的点:
- spring确实在执行销毁容器的操作
- 有一个SIGHUP handler的线程, 在执行
自此, 大家可能已经看出来, SIGHUP正是JVM会处理的Signal之一, 并且在上面的表格中已经清楚的写着SIGHUP的操作是挂起, 让JVM正常退出, SIGHUP是中断类型的信号, 上面对于中断类型的信号是这样描述的: 将从 JVM 进程外部异步发出中断信号以请求关闭。
结论:
基本确定是由外部发出的中断信号, 导致JVM正常退出. 那么如果能找到信号来源的话, 这个事情就清楚了.
可惜的是, 找了很多资料, 始终没有找到确定信号来源的方案. Linux本身也没有相关的日志, JVM也只能获取信号的名称, 对于信号源也是无法确定.(如果有这方面研究的同学望告知..)
3.3 解决方案
找不到根本原因, 那么只能是想办法绕过这个问题.
所幸的是, 在搜索问题的时候, 让我知道了Linux还有一个nohup
的命令.
nohup命令可以将程序以忽略挂起信号(SIGHUP)的方式运行起来,被运行的程序的输出信息将不会显示到终端。
于是把JVM的启动脚本改动了一下:
[图片上传失败...(image-7897ca-1513089974690)]
再次启动后, 稳定运行, 问题解决.
实际上通过JVM本身-Xrs
的参数应该也能控制忽略SIGHUP信号的, 但是时间关系, 我没去实验..
3.4 总结
这里案例也让我学到了很多JVM的处理细节.
同时也有了一些思考:
排查线上故障的基本步骤无非就是
- 看现象, 初步判断故障的原因或范围(如果能直接确定问题当然是最好)
- 看异常日志, 判断异常是否发生以及发生的代码位置, 从而确定具体的原因
- 结合监控(Marvin等), 观察机器的运行情况, 辅助排查问题
- 第一时间收集现场证据(如jstack, jmap, gc.log等), 以便后面的问题分析
实际上, 前面3步基本上已经能解决大部分的问题了, 剩下的一些疑难问题才会用到第4步. 但是第4步的操作对于实时性的要求是最高的, 必须第一时间搞定, 晚一点可能你就捕捉不到有效的证据了.
在出现故障的情况下, 有时候难免手忙脚乱, 如果有一个可以自动化收集现场证据的脚本, 在出现这种疑难问题的时候将会是一个极大的助力.