你的SpringBoot应用是怎么停机的?
以往我们在使用Tomcat部署war应用时,服务器的启停可以通过Tomcat提供的startups.sh和shutdown.sh脚本来完成,现如今SpringBoot盛行,直接使用java -jar xxx.jar命令就可以轻松启动应用,但是该如何优雅的停止SpringBoot应用呢?
首先什么叫优雅停机?简单的讲就是对应用发出停机指令后,新的请求不会进入这台应用,已经接收到的请求要能够正确的返回响应,能够实现对用户的无感。
要想达到这个目的,第一步也是最重要的一步就是将应用从集群中剥离,也就是从Eureka上下线,停止接收新的请求;然后应用内部也要等所有请求处理完毕后再执行服务器的停机。
几种常见停机方式分析
kill -9 pid
这种方式应该是最为常见的停机方式,也是最不推荐的方式,它会强行终止JVM进程,导致应用没能从集群中剥离,Eureka上依然保留了它的注册信息,只能通过Eureka定时的健康检查来发现服务已不可用,这期间必然会导致大量的交易失败。
kill pid、 kill -15 pid、/shutdown端点
这三种方式可以归为一类,kill pid 等同于 kill -15 pid,都是向进程发送终止信号(kill -9是发送立即终止信号,注意两者区别),而/shutdown端点的效果与前面两个命令效果相同;
使用这三种方式停机时,应用会立即从注册中心下线,看起来比kill -9这种粗暴的方式要好很多,但是在执行完下线之后会立即终止服务器线程,导致已经接收到的请求没能处理完成,用户端或者是其他的调用方不能正确得到响应信息。
docker stop
对于那些使用容器部署SpringBoot应用的,粗暴的使用docker stop来停止容器以到达停止应用的目的,这种方式犹如直接拔电源,比kill -9还要粗暴;使用此种方式停机的后果请参照kill -9的方式。
调用Eureka服务端API使应用下线,再kill进程
这种方式一般情况下是可以使用的,但是Eureka Server的URL结构复杂,调用也比较麻烦。
贴一个Eureka服务端API介绍:
说了这么多,那么到底怎样才能优雅的停机呢?
我们这里主要介绍github上issue里SpringBoot开发者提供的一种方案;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown();
}
public EmbeddedServletContainerCustomizer tomcatCustomizer() {
return new EmbeddedServletContainerCustomizer() {
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
((TomcatEmbeddedServletContainerFactory) container)
.addConnectorCustomizers(gracefulShutdown());
}
}
};
}
private static class GracefulShutdown implements TomcatConnectorCustomizer,
ApplicationListener<ContextClosedEvent> {
private volatile Connector connector;
public void customize(Connector connector) {
this.connector = connector;
}
public void onApplicationEvent(ContextClosedEvent event) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
System.out.println("Tomcat线程池在30s内未能优雅关闭,将强行关闭!");
} else {
System.out.println("Tomcat线程池已优雅关闭!");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
}
主要是增加一个ContextClosedEvent监视器,当应用收到停机指令时,该监视器会将Tomcat线程池挂起,停止处理新请求(其实此时应用已经从注册中心下线,不会有新的请求进来),然后等待线程池正常终止,并设置超时时间,最多等待30s,然后终止服务器。
有了这个监视器后,我们就可以放心的使用 kill pid的方式来停止SpringBoot应用啦。
点击阅读原文可查看GitHub上对于该问题的讨论。
喜欢就点个关注吧 ^_^