vlambda博客
学习文章列表

你的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;
@SpringBootApplication@EnableDiscoveryClientpublic class Application {
public static void main(String[] args) { SpringApplication.run(Application.class, args); }
@Bean public GracefulShutdown gracefulShutdown() { return new GracefulShutdown(); }
@Bean public EmbeddedServletContainerCustomizer tomcatCustomizer() { return new EmbeddedServletContainerCustomizer() { @Override 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;
@Override public void customize(Connector connector) { this.connector = connector; }
@Override 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上对于该问题的讨论。


喜欢就点个关注吧 ^_^