exec java和java的区别
最近在线上遇到了一个问题:本地写了一个spring-boot服务,想实现一个优雅关闭的逻辑,有两种可能的实现,一种是通过Spring的注解:
@PreDestroy
public void destroy() {
// some exit logic
}
还有一种是注册一个jvm的钩子:
Runtime.getRuntime().addShutdownHook(new Thread(this::destroy));
public void destroy() {
// some exit logic
}
理论上这两种方式都能截获到SIGTERM
或者SIGINT
触发优雅关闭。但是我们推荐使用第一种@PreDestroy
的方式。因为在触发jvm的shutdownhook
时某些bean已经被spring关闭了,此时代码中有些逻辑可能无法正常执行。
本地IDEA运行时,@PreDestroy
能够正确生效,但是发布到kubernetes环境中,执行kill
时发现程序并没有按照预期正常触发@PreDestroy
,而是在30s后被突然结束进程。
后来发现是image镜像里的命令有问题:
# start client-service
java -javaagent:xxxxxxx.jar
这里的命令是直接执行java,会导致起来的java进程是shell的子进程,当pod被kill尝试发送SIGTERM
时,shell会收到SIGTERM
而jvm收不到,从而30秒后达到terminationGracePeriodSeconds
设定的默认值被强制被SIGKILL
。
此处应该修改成:
# start client-service
exec java -javaagent:xxxxxxx.jar
这样就可以避免这个问题,因为exec不会起一个子进程,而是在保持PID不变的情况下,让jvm进程替代shell进程,这样收到SIGTERM
信号的就是jvm本身了,可以正常触发优雅关闭。
我们这里用echo
命令在本地试验一下。
直接运行
先开启一个shell tab,运行echo $$
看一下这个shell的PID:
可以看到PID是63855。
我们tail -f
进入一个长期运行命令:
我们另启一个shell,运行ps -ef | grep tail
看一下有哪些进程:
可以看到有两个进程,一个是PID为63855
的shell进程,另一个是PID为64663
的tail
进程,且通过PPID可以看到,shell
进程是tail
进程的父进程,这符合预期。
我们尝试在第二个shell中SIGINT
也就是kill -2
一下第一个shell进程,看看会发生什么:
可以看到tail
进程并未退出,那么如果直接对tail
进程执行SIGINT
呢:
这次tail
正常结束了,且回到了父进程shell的交互界面。可以看到,以这种方式直接启动的进程是收不到来自系统的信号的。
exec运行
接下来进行第二次试验,回到第一个shell界面,以exec
前缀的方式启动tail
命令:
接下来回到第二个shell并尝试kill -2 63855
(第一个shell在上一个实验中并没有退出,所以PID还是63855
没变):
可以看到这次tail
直接结束了,而且与第一次实验不同的是,不仅tail
结束了,而是直接显示Process completed
,并没有返回到shell的交互界面。这两个实验验证了之前线上的jvm收不到pod被kill的SIGTERM
信号的case和猜测。
额外知识
kill -l
可以查看操作系统支持的kill参数,在mac上运行结果如下:
➜ ~ kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
➜ ~ kill -l | wc -w
31
说明mac上支持31种信号,以下是其中3种常见信号:
INT # kill -2 interrupt 相当于ctrl+c
KILL # kill -9
TERM # kill -15