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 -15kill -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关闭的钩子,这个钩子可以在以下几种场景被调用:

  1. 程序正常退出
  2. 使用System.exit()
  3. 终端使用Ctrl+C触发的中断
  4. 系统关闭
  5. 使用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)。测试结果:

  1. Spring boot的健康检查,为UP
  2. 正在执行操作不会终止,直到执行完成。
  3. 不再接收新的请求,客户端报错信息为:Connection reset by peer
  4. 最后正常终止进程(业务执行完成后,立即进程停止)。

从测试结果来看,是满足我们的需求的。当然如果发送指令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同样的测试方案,测试结果:

  1. Spring boot的健康检查,为UP
  2. 正在执行操作不会终止,直到执行完成。
  3. 不再接收新的请求,客户端报错信息为:503 Service Unavailable
  4. 最后正常终止进程(在业务执行完成后的一分钟进程停止)。

04 结束

到此为止,对Java和Spring boot应用的优雅停机机制有了基本的认识。虽然实现了需求,但是这其中还有很多知识点需要探索,比如Spring上下文监听器,上下文关闭事件等,还有undertow提供的GracefulShutdownHandler的原理是什么,为什么是1分钟之后进程再停止,这些问题等研究明白,再来一篇续。如果又哪位同学能解答我的疑惑,请在评论区留言。

Spring boot 2.0 之优雅停机的更多相关文章

  1. Spring Boot 2.0系列文章(五):Spring Boot 2.0 项目源码结构预览

    关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/04/15/springboot2_code/ 项目结构 结构分析: Spring-boot-pr ...

  2. Spring Boot 2.0 教程 - 深入SpringAplication

    原文连接:https://www.codemore.top/cates/Backend/post/2018-05-20/spring-boot-SpringApplication 可以通过Spring ...

  3. Spring Boot 2.0正式发布,新特性解读

    作者|翟永超 Spring Boot 2.0 来啦,有哪些新特性?升级吗? 写在前面 北京时间 3 月 1 日,经过漫长的等待之后,Spring Boot 2.0 正式发布.作为 Spring 生态中 ...

  4. Spring Boot 2.0官方文档之 Actuator(转)

    执行器(Actuator)的定义 执行器是一个制造业术语,指的是用于移动或控制东西的一个机械装置,一个很小的改变就能让执行器产生大量的运动. An actuator is a manufacturin ...

  5. Spring Boot 2.0 Intellij Idea 中图文详解打包成可执行Jar

    我们使用Spring Boot 2.0 创建好我们的项目后,我们一般需要打包,然后部署到服务器上. 打包步骤: 1. 选中项目,右键——> Open Module Settings. 2. 切换 ...

  6. springboot2.0(一):【重磅】Spring Boot 2.0权威发布

    就在昨天Spring Boot2.0.0.RELEASE正式发布,今天早上在发布Spring Boot2.0的时候还出现一个小插曲,将Spring Boot2.0同步到Maven仓库的时候出现了错误, ...

  7. 业余草分享 Spring Boot 2.0 正式发布的新特性

    就在昨天Spring Boot2.0.0.RELEASE正式发布,今天早上在发布Spring Boot2.0的时候还出现一个小插曲,将Spring Boot2.0同步到Maven仓库的时候出现了错误, ...

  8. Spring Boot 2.0(二):Spring Boot 2.0尝鲜-动态 Banner

    Spring Boot 2.0 提供了很多新特性,其中就有一个小彩蛋:动态 Banner,今天我们就先拿这个来尝尝鲜. 配置依赖 使用 Spring Boot 2.0 首先需要将项目依赖包替换为刚刚发 ...

  9. Spring Boot 2.0(四):使用 Docker 部署 Spring Boot

    Docker 技术发展为微服务落地提供了更加便利的环境,使用 Docker 部署 Spring Boot 其实非常简单,这篇文章我们就来简单学习下. 首先构建一个简单的 Spring Boot 项目, ...

  10. spring boot 2.0.0由于版本不匹配导致的NoSuchMethodError问题解析

    spring boot升级到2.0.0以后,项目突然报出 NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBu ...

随机推荐

  1. iOS中xib文件维护使用小结

    最近一直在做项目维护,由于项目比较大,开发时间比较早,早期的很多页面都是用xib拖拽页面控件.简单的页面还好,详情页面也是拖拽搭建,项目维护成本可想而知.闲言少叙,下面说一下不是特别复杂的xib页面维 ...

  2. HRM平台的登录页的背景图片- scss

    .login-container {   // 设置背景图片   background-image: url("~@/assets/common/login.jpg");   ba ...

  3. 说一下tcp三次握手

    1. 客户端发送syn请求连接 : 2. 服务器检验syn,然后发送syn和ack确认连接: 3. 客户端接收ack和syn,然后发送ack建立连接 :

  4. 背靠AI,让AI当牛马,解决程序员的烦恼

    开篇问题? 作为程序员的你,写代码累吗?累!苦嘛?苦,想哭嘛?哭不出来. 还在为工作中繁重的编码任务.复杂的调试过程以及不断更新的技术栈而苦恼吗?这些挑战不仅消耗大量的时间和精力,还时常让人陷入思维的 ...

  5. ERQ:32位转5位仅掉些许精度,来看看两段式后训练量化 | ICML 2024

    后训练量化(PTQ)在视觉Transformer(ViTs)领域引起了广泛关注,因为它在模型压缩方面表现出了高效率.然而,现有的方法通常忽视了量化权重和激活之间复杂的相互依赖关系,导致了相当大的量化误 ...

  6. PostgreSQL 17重磅登场——世界上最成功的数据库

    朋友们,万众期待的 PostgreSQL 大版本发布又来了!这一次,PostgreSQL 17 带着全新的性能优化和开发者必备的新功能强势登场.与其说这是一场普通的更新,不如说它是一场专为高并发工作负 ...

  7. Sentinel简单使用(1)

    使用场景 在微服务架构中,服务之间会进行大量的调用.为了防止某个服务被过多的请求压垮,导致整个系统崩溃,就需要对流量进行控制.同时,当某个服务出现故障时,为了防止故障扩散到整个系统,需要进行熔断操作. ...

  8. NDT论文翻译

    The Normal Distributions Transform: A New Approach to Laser Scan Matching 正态分布变换:激光扫描匹配的新方法 摘要:匹配 2D ...

  9. 关于 PyCharm 2024安装使用 (附加永久激活码、补丁)

    第一步:下载安装包 访问 IDEA 官网,下载安装包,下载链接如下 : https://www.jetbrains.com.cn/pycharm/ 第二步,安装完成之后,下载补丁 下载地址(里面包含激 ...

  10. 多进程可以共享内存,那么多进程是否可以共享显存呢?(CPU->内存,GPU->显存)

    多进程可以共享内存,那么多进程是否可以共享显存呢?(CPU->内存,GPU->显存) 答案:不能.多进程可以共享内存,但是多进程不能共享显存(NVIDIA GPU 显存不能被多进程共享). ...