springboot优雅关机
Spring boot 2.0 之优雅停机
2018.05.20 18:41* 字数 1794 阅读 2638评论 0喜欢 22
spring boot 框架在生产环境使用的有一段时间了,它“约定大于配置”的特性,体现了优雅流畅的开发过程,它的部署启动方式(
java -jar xxx.jar
)也很优雅。但是我使用的停止应用的方式是kill -9 进程号
,即使写了脚本,还是显得有些粗鲁。这样的应用停止方式,在停止的那一霎那,应用中正在处理的业务逻辑会被中断,导致产生业务异常情形。这种情况如何避免,本文介绍的优雅停机,将完美解决该问题。
00 前言
什么叫优雅停机?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。应用接收到停止指令之后的步骤应该是,停止接收访问请求,等待已经接收到的请求处理完成,并能成功返回,这时才真正停止应用。
这种完美的应用停止方式如何实现呢?就Java语言生态来说,底层的技术是支持的,所以我们才能实现在Java语言之上的各个web容器的优雅停机。
在普通的外置的tomcat中,有shutdown脚本提供优雅的停机机制,但是我们在使用Spring boot的过程中发现web容器都是内置(当然也可使用外置,但是不推荐),这种方式提供简单的应用启动方式,方便的管理机制,非常适用于微服务应用中,但是默认没有提供优雅停机的方式。这也是本文探索这个问题的根本原因。
应用是否是实现了优雅停机,如何才能验证呢?这需要一个处理时间较长的业务逻辑,模拟这样的逻辑应该很简单,使用线程sleep或者长时间循环。我的模拟业务逻辑代码如下:
-
@GetMapping(value = "/sleep/one", produces = "application/json")
-
public ResultEntity<Long> sleepOne(String systemNo){
-
logger.info("模拟业务处理1分钟,请求参数:{}", systemNo);
-
Long serverTime = System.currentTimeMillis();
-
// try {
-
// Thread.sleep(60*1000L);
-
// } catch (InterruptedException e) {
-
// e.printStackTrace();
-
// }
-
while (System.currentTimeMillis() < serverTime + (60 * 1000)){
-
logger.info("正在处理业务,当前时间:{},开始时间:{}", System.currentTimeMillis(), serverTime);
-
}
-
ResultEntity<Long> resultEntity = new ResultEntity<>(serverTime);
-
logger.info("模拟业务处理1分钟,响应参数:{}", resultEntity);
-
return resultEntity;
-
}
验证方式就是,在触发这个接口的业务处理之后,业务逻辑处理时间长达1分钟,需要在处理结束前,发起停止指令,验证是否能够正常返回。验证时所使用的kill指令:kill -2(Ctrl + C)
、kill -15
、kill -9
。
01 Java 语言的优雅停机
从上面的介绍中我们发现,Java语言本身是支持优雅停机的,这里就先介绍一下普通的java应用是如何实现优雅停止的。
当我们使用kill PID
的方式结束一个Java应用的时候,JVM会收到一个停止信号,然后执行shutdownHook的线程。一个实现示例如下:
-
public class ShutdownHook extends Thread {
-
private Thread mainThread;
-
private boolean shutDownSignalReceived;
-
-
@Override
-
public void run() {
-
System.out.println("Shut down signal received.");
-
this.shutDownSignalReceived=true;
-
mainThread.interrupt();
-
try {
-
mainThread.join(); //当收到停止信号时,等待mainThread的执行完成
-
} catch (InterruptedException e) {
-
}
-
System.out.println("Shut down complete.");
-
}
-
-
public ShutdownHook(Thread mainThread) {
-
super();
-
this.mainThread = mainThread;
-
this.shutDownSignalReceived = false;
-
Runtime.getRuntime().addShutdownHook(this);
-
}
-
-
public boolean shouldShutDown(){
-
return shutDownSignalReceived;
-
}
-
-
}
其中关键语句Runtime.getRuntime().addShutdownHook(this);
,注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:
- 程序正常退出
- 使用System.exit()
- 终端使用Ctrl+C触发的中断
- 系统关闭
- 使用Kill pid命令干掉进程
测试shutdownHook的功能,代码示例:
-
public class TestMain {
-
private ShutdownHook shutdownHook;
-
public static void main( String[] args ) {
-
TestMain app = new TestMain();
-
System.out.println( "Hello World!" );
-
app.execute();
-
System.out.println( "End of main()" );
-
}
-
public TestMain(){
-
this.shutdownHook = new ShutdownHook(Thread.currentThread());
-
}
-
public void execute(){
-
while(!shutdownHook.shouldShutDown()){
-
System.out.println("I am sleep");
-
try {
-
Thread.sleep(1*1000);
-
} catch (InterruptedException e) {
-
System.out.println("execute() interrupted");
-
}
-
System.out.println("I am not sleep");
-
}
-
System.out.println("end of execute()");
-
}
-
}
启动测试代码,之后再发送一个中断信号,控制台输出:
-
I am sleep
-
I am not sleep
-
I am sleep
-
I am not sleep
-
I am sleep
-
I am not sleep
-
I am sleep
-
Shut down signal received.
-
execute() interrupted
-
I am not sleep
-
end of execute()
-
End of main()
-
Shut down complete.
-
-
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
可以看出,在接收到中断信号之后,整个main函数是执行完成的。
02 actuator/shutdown of Spring boot
我们知道了java本身在支持优雅停机上的能力,然后在Spring boot中又发现了actuator/shutdown
的管理端点。于是我把优雅停机的功能寄希望于此,开始配置测试,开启配置如下:
-
management:
-
server:
-
port: 10212
-
servlet:
-
context-path: /
-
ssl:
-
enabled: false
-
endpoints:
-
web:
-
exposure:
-
include: "*"
-
endpoint:
-
health:
-
show-details: always
-
shutdown:
-
enabled: true #启用shutdown端点
测试结果很失望,并没有实现优雅停机的功能,就是将普通的kill命令,做成了HTTP端点。于是开始查看Spring boot的官方文档和源代码,试图找到它的原因。
在官方文档上对shutdown端点的介绍:
shutdown Lets the application be gracefully shutdown.
从此介绍可以看出,设计上应该是支持优雅停机的。但是为什么现在还不够优雅,在github上托管的Spring boot项目中发现,有一个issue一直处于打开状态,已经两年多了,里面很多讨论,看完之后发现在Spring boot中完美的支持优雅停机不是一件容易的事,首先Spring boot支持web容器很多,其次对什么样的实现才是真正的优雅停机,讨论了很多。想了解更多的同学,把这个issue好好阅读一下。
这个issue中还有一个重要信息,就是这个issue曾经被加入到2.0.0的milestone中,后来由于没有完成又移除了,现在状态是被添加在2.1.0的milestone中。我测试的版本是2.0.1,期待官方给出完美的优雅停机方案。
03 Spring boot 优雅停机
虽然官方暂时还没有提供优雅停机的支持,但是我们为了减少进程停止对业务的影响,还是要给出能满足基本需求的方案来。
针对tomcat的解决方案是:
-
package com.epay.demox.unipay.provider;
-
-
import org.apache.catalina.connector.Connector;
-
import org.slf4j.Logger;
-
import org.slf4j.LoggerFactory;
-
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
-
import org.springframework.context.ApplicationListener;
-
import org.springframework.context.event.ContextClosedEvent;
-
import org.springframework.stereotype.Component;
-
-
import java.util.concurrent.Executor;
-
import java.util.concurrent.ThreadPoolExecutor;
-
import java.util.concurrent.TimeUnit;
-
-
/**
-
* @Author: guoyankui
-
* @DATE: 2018/5/20 12:59 PM
-
*
-
* 优雅关闭 Spring Boot tomcat
-
*/
-
-
@Component
-
public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
-
private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
-
private volatile Connector connector;
-
private final int waitTime = 30;
-
@Override
-
public void customize(Connector connector) {
-
this.connector = connector;
-
}
-
@Override
-
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
-
this.connector.pause();
-
Executor executor = this.connector.getProtocolHandler().getExecutor();
-
if (executor instanceof ThreadPoolExecutor) {
-
try {
-
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
-
threadPoolExecutor.shutdown();
-
if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
-
log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
-
}
-
} catch (InterruptedException ex) {
-
Thread.currentThread().interrupt();
-
}
-
}
-
}
-
}
-
public class UnipayProviderApplication {
-
public static void main(String[] args) {
-
SpringApplication.run(UnipayProviderApplication.class);
-
}
-
-
@Autowired
-
private GracefulShutdownTomcat gracefulShutdownTomcat;
-
-
@Bean
-
public ServletWebServerFactory servletContainer() {
-
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
-
tomcat.addConnectorCustomizers(gracefulShutdownTomcat);
-
return tomcat;
-
}
-
}
该方案的代码来自官方issue中的讨论,添加这些代码到你的Spring boot项目中,然后再重新启动之后,发起测试请求,然后发送kill停止指令(kill -2(Ctrl + C)
、kill -15
)。测试结果:
- Spring boot的健康检查,为
UP
。 - 正在执行操作不会终止,直到执行完成。
- 不再接收新的请求,客户端报错信息为:
Connection reset by peer
。 - 最后正常终止进程(业务执行完成后,立即进程停止)。
从测试结果来看,是满足我们的需求的。当然如果发送指令kill -9
,进程会立即停止。
针对undertow的解决方案是:
-
package com.epay.demox.unipay.provider;
-
-
import io.undertow.Undertow;
-
import io.undertow.server.ConnectorStatistics;
-
import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServer;
-
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
-
import org.springframework.context.ApplicationListener;
-
import org.springframework.context.event.ContextClosedEvent;
-
import org.springframework.stereotype.Component;
-
-
import java.lang.reflect.Field;
-
import java.util.List;
-
-
/**
-
* @Author: guoyankui
-
* @DATE: 2018/5/20 5:47 PM
-
*
-
* 优雅关闭 Spring Boot undertow
-
*/
-
@Component
-
public class GracefulShutdownUndertow implements ApplicationListener<ContextClosedEvent> {
-
-
@Autowired
-
private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
-
-
@Autowired
-
private ServletWebServerApplicationContext context;
-
-
@Override
-
public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
-
gracefulShutdownUndertowWrapper.getGracefulShutdownHandler().shutdown();
-
try {
-
UndertowServletWebServer webServer = (UndertowServletWebServer)context.getWebServer();
-
Field field = webServer.getClass().getDeclaredField("undertow");
-
field.setAccessible(true);
-
Undertow undertow = (Undertow) field.get(webServer);
-
List<Undertow.ListenerInfo> listenerInfo = undertow.getListenerInfo();
-
Undertow.ListenerInfo listener = listenerInfo.get(0);
-
ConnectorStatistics connectorStatistics = listener.getConnectorStatistics();
-
while (connectorStatistics.getActiveConnections() > 0){}
-
}catch (Exception e){
-
// Application Shutdown
-
}
-
}
-
}
-
package com.epay.demox.unipay.provider;
-
-
import io.undertow.server.HandlerWrapper;
-
import io.undertow.server.HttpHandler;
-
import io.undertow.server.handlers.GracefulShutdownHandler;
-
import org.springframework.stereotype.Component;
-
-
/**
-
* @Author: guoyankui
-
* @DATE: 2018/5/20 5:50 PM
-
*/
-
@Component
-
public class GracefulShutdownUndertowWrapper implements HandlerWrapper {
-
private GracefulShutdownHandler gracefulShutdownHandler;
-
@Override
-
public HttpHandler wrap(HttpHandler handler) {
-
if(gracefulShutdownHandler == null) {
-
this.gracefulShutdownHandler = new GracefulShutdownHandler(handler);
-
}
-
return gracefulShutdownHandler;
-
}
-
public GracefulShutdownHandler getGracefulShutdownHandler() {
-
return gracefulShutdownHandler;
-
}
-
}
-
public class UnipayProviderApplication {
-
public static void main(String[] args) {
-
SpringApplication.run(UnipayProviderApplication.class);
-
}
-
@Autowired
-
private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
-
@Bean
-
public UndertowServletWebServerFactory servletWebServerFactory() {
-
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
-
factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.addOuterHandlerChainWrapper(gracefulShutdownUndertowWrapper));
-
factory.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_STATISTICS, true));
-
return factory;
-
}
-
}
该方法参考文章,采用与tomcat同样的测试方案,测试结果:
- Spring boot的健康检查,为
UP
。 - 正在执行操作不会终止,直到执行完成。
- 不再接收新的请求,客户端报错信息为:
503 Service Unavailable
。 - 最后正常终止进程(在业务执行完成后的一分钟进程停止)。
04 结束
到此为止,对Java和Spring boot应用的优雅停机机制有了基本的认识。虽然实现了需求,但是这其中还有很多知识点需要探索,比如Spring上下文监听器,上下文关闭事件等,还有undertow提供的GracefulShutdownHandler
的原理是什么,为什么是1分钟之后进程再停止,这些问题等研究明白,再来一篇续。如果又哪位同学能解答我的疑惑,请在评论区留言。
springboot优雅关机的更多相关文章
- TOC 1. TODO springboot优雅关机
TODO start and stop as a linux service web container(tomcat ,undertow) gracefully shutdown gracefull ...
- SpringBoot优雅的全局异常处理
前言 本篇文章主要介绍的是SpringBoot项目进行全局异常的处理. SpringBoot全局异常准备 说明:如果想直接获取工程那么可以直接跳到底部,通过链接下载工程代码. 开发准备 环境要求 JD ...
- Go实现优雅关机与平滑重启
前言 优雅关机就是服务端关机命令发出后不是立即关机,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的关机方式.而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出 ...
- Springboot 优雅停止服务的几种方法
在使用Springboot的时候,都要涉及到服务的停止和启动,当我们停止服务的时候,很多时候大家都是kill -9 直接把程序进程杀掉,这样程序不会执行优雅的关闭.而且一些没有执行完的程序就会直接退出 ...
- SpringBoot优雅地配置日志
本文主要给大家介绍SpringBoot中如何通过sl4j日志组件优雅地记录日志.其实,我们入门 JAVA 的第一行代码就是一行日志,那你现在还在使用System.out.println("H ...
- SpringBoot 优雅配置跨域多种方式及Spring Security跨域访问配置的坑
前言 最近在做项目的时候,基于前后端分离的权限管理系统,后台使用 Spring Security 作为权限控制管理, 然后在前端接口访问时候涉及到跨域,但我怎么配置跨域也没有生效,这里有一个坑,在使用 ...
- Dubbo优雅关机原理
Dubbo是通过JDK的ShutdownHook来完成优雅停机的 所以如果用户使用 kill -9 PID 等强制关闭命令,是不会执行优雅停机的 只有通过 kill PID时,才会执行 原理: · 服 ...
- springboot优雅的关闭应用
使用actuator,通过发送http请求关闭 将应用注册为linux服务,通过service xxx stop关闭 具体这两种方式如何实现,这里就不说了,网上百度一堆,主要讲一下在这两种情况下web ...
- 四、springBoot 优雅的创建定时任务
前言 好几天没写了,工作有点忙,最近工作刚好做一个定时任务统计的,所以就将springboot 如何创建定时任务整理了一下. 总的来说,springboot创建定时任务是非常简单的,不用像spring ...
随机推荐
- MSP430:串口输出
初始化 void Uart_Init(void) { BCSCTL1 = CALBC1_1MHZ; // Set DCO DCOCTL = CALDCO_1MHZ; P1SEL = BIT1 + BI ...
- etcd磁盘清理步骤
etcd默认的空间配额限制为2G,超出空间配额限制就会影响服务,所以需要定期清理 以下是etcd磁盘清理的步骤: 1. 显示空间配额: ETCDCTL_API=3 etcdctl --endpoint ...
- JAVA基础(多线程Thread和Runnable的使用区别(转载)
转自:http://jinguo.iteye.com/blog/286772 Runnable是Thread的接口,在大多数情况下“推荐用接口的方式”生成线程,因为接口可以实现多继承,况且Runnab ...
- PCB CE工具取Genesis JOB与STEP内存地址 方法分享
今天无意中在硬盘上找到了<CE工具取Genesis JOB与STEP内存地址 >视频, 这是2013年初由郭兄(永明)远程时录制的一段视频,特别感谢郭兄指引与帮助, 想当初要不是你推出全行 ...
- [Swift通天遁地]六、智能布局-(3)添加edges/top/bottom/leading/trailing的约束
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...
- openpyxl python操作Excel表格,
这里openpyxl只支持xlsx格式的Excel,openpyxl使用起来会更方便一些,所以如果只操作小流水线文件的话,那么可以优先选择openpyxl,如果要兼容xls的话,就使用xlrd/xlw ...
- ssh项目导入报the import javax.servlet cannot be resolved
在做javaWeb项目时,我们经常会出现丢失包的情况,如下图所示的错误,我们应该怎么解决呢? 根据网上教程向工程中加入tomcat的servlet-api.jar和jsp-api.jar的包 此时项目 ...
- 常用animation动画
/*编辑动画名*/ animation-name: myDemo; /*动画持续时间*/ animation-duration: 6s; /*动画方向*/ /*reverse 反向*/ /*alter ...
- 黑马程序员 关于c# windows窗体关闭时线程未能完全退出问题(专题一)
<a href="http://edu.csdn.net"target="blank">ASP.Net+Android+IO开发S</a> ...
- JS高级——变量提升
JS执行过程 1.首先是预解析:预解析过程最重要的是提升,在JavaScript代码在预解析阶段,会对以var声明的变量名,和function开头的语句块,进行提升操作 2.执行操作 全局中解析和执行 ...