详解Spring Boot最新版优雅停机的方法

什么是优雅停机
先来一段简单的代码,如下:

@RestController
public class DemoController {
 @GetMapping("/demo")
 public String demo() throws InterruptedException {
 // 模拟业务耗时处理流程
 Thread.sleep(20 * 1000L);
 return "hello";
 }
}

当我们流量请求到此接口执行业务逻辑的时候,若服务端此时执行关机 (kill),spring boot 默认情况会直接关闭容器(tomcat 等),导致此业务逻辑执行失败。在一些业务场景下:会出现数据不一致的情况,事务逻辑不会回滚。

开源项目:

分布式监控(Gitee GVP最有价值开源项目 ):https://gitee.com/sanjiankethree/cubic

摄像头视频流采集:https://gitee.com/sanjiankethree/cubic-video

优雅停机

目前Spring Boot已经发展到了2.3.4.RELEASE,伴随着2.3版本的到来,优雅停机机制也更加完善了。

目前版本的Spring Boot 优雅停机支持Jetty, Reactor Netty, Tomcat和 Undertow 以及反应式和基于 Servlet 的 web 应用程序都支持优雅停机功能。

优雅停机的目的:

如果没有优雅停机,服务器此时直接直接关闭(kill -9),那么就会导致当前正在容器内运行的业务直接失败,在某些特殊的场景下产生脏数据。

增加了优雅停机配置后:

在服务器执行关闭(kill -2)时,会预留一点时间使容器内部业务线程执行完毕,此时容器也不允许新的请求进入。新请求的处理方式跟web服务器有关,Reactor Netty、 Tomcat将停止接入请求,Undertow的处理方式是返回503.

新版配置

YAML配置

新版本配置非常简单,server.shutdown=graceful 就搞定了(注意,优雅停机配置需要配合Tomcat 9.0.33(含)以上版本)

server:
 port: 6080
 shutdown: graceful #开启优雅停机
spring:
 lifecycle:
 timeout-per-shutdown-phase: 20s #设置缓冲时间 默认30s

在设置了缓冲参数timeout-per-shutdown-phase 后,在规定时间内如果线程无法执行完毕则会被强制停机。

下面我们来看下停机时,加了优雅停日志和不加的区别:

//未加优雅停机配置
Disconnected from the target VM, address: '127.0.0.1:49754', transport: 'socket'
Process finished with exit code 130 (interrupted by signal 2: SIGINT)

加了优雅停机配置后,可明显发现的日志 Waiting for active requests to cpmplete,此时容器将在ShutdownHook执行完毕后停止。

关闭方式

1、 一定不要使用kill -9 操作,使用kill -2 来关闭容器。这样才会触发java内部ShutdownHook操作,kill -9不会触发ShutdownHook。

2、可以使用端点监控 POST 请求 /actuator/shutdown 来执行优雅关机。

添加ShutdownHook

通过上面的日志我们发现Druid执行了自己的ShutdownHook,那么我们也来添加下ShutdownHook,有几种简单的方式:

1、实现DisposableBean接口,实现destroy方法

@Slf4j
@Service
public class DefaultDataStore implements DisposableBean {

 private final ExecutorService executorService = new ThreadPoolExecutor(OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo"));

 @Override
 public void destroy() throws Exception {
 log.info("准备优雅停止应用使用 DisposableBean");
 executorService.shutdown();
 }
}

2、使用@PreDestroy注解

@Slf4j
@Service
public class DefaultDataStore {

 private final ExecutorService executorService = new ThreadPoolExecutor(OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo"));

 @PreDestroy
 public void shutdown() {
 log.info("准备优雅停止应用 @PreDestroy");
 executorService.shutdown();
 }

}

这里注意,@PreDestroy 比 DisposableBean 先执行

关闭原理

1、使用kill pid关闭,源码很简单,大家可以看下GracefulShutdown

	private void doShutdown(GracefulShutdownCallback callback) {
		List<Connector> connectors = getConnectors();
		connectors.forEach(this::close);
		try {
			for (Container host : this.tomcat.getEngine().findChildren()) {
				for (Container context : host.findChildren()) {
					while (isActive(context)) {
						if (this.aborted) {
							logger.info("Graceful shutdown aborted with one or more requests still active");
							callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
							return;
						}
						Thread.sleep(50);
					}
				}
			}

		}
		catch (InterruptedException ex) {
			Thread.currentThread().interrupt();
		}
		logger.info("Graceful shutdown complete");
		callback.shutdownComplete(GracefulShutdownResult.IDLE);
	}

2、使用端点监控 POST 请求 /actuator/shutdown关闭

因为actuator 都使用了SPI的扩展方式,所以我们看下AutoConfiguration,可以看到关键点就是ShutdownEndpoint

@Configuration(
 proxyBeanMethods = false
)
@ConditionalOnAvailableEndpoint(
 endpoint = ShutdownEndpoint.class
)
public class ShutdownEndpointAutoConfiguration {
 public ShutdownEndpointAutoConfiguration() {
 }

 @Bean(
 destroyMethod = ""
 )
 @ConditionalOnMissingBean
 public ShutdownEndpoint shutdownEndpoint() {
 return new ShutdownEndpoint();
 }
}

ShutdownEndpoint,为了节省篇幅只留了一点重要的

@Endpoint(
 id = "shutdown",
 enableByDefault = false
)
public class ShutdownEndpoint implements ApplicationContextAware {

 @WriteOperation
 public Map<String, String> shutdown() {
 if (this.context == null) {
  return NO_CONTEXT_MESSAGE;
 } else {
  boolean var6 = false;

  Map var1;
  try {
  var6 = true;
  var1 = SHUTDOWN_MESSAGE;
  var6 = false;
  } finally {
  if (var6) {
   Thread thread = new Thread(this::performShutdown);
   thread.setContextClassLoader(this.getClass().getClassLoader());
   thread.start();
  }
  }

  Thread thread = new Thread(this::performShutdown);
  thread.setContextClassLoader(this.getClass().getClassLoader());
  thread.start();
  return var1;
 }
 }

 private void performShutdown() {
 try {
  Thread.sleep(500L);
 } catch (InterruptedException var2) {
  Thread.currentThread().interrupt();
 }

 this.context.close(); //这里才是核心
 }
}

在调用了 this.context.close() ,其实就是AbstractApplicationContext 的close() 方法 (重点是其中的doClose())

/**
	 * Close this application context, destroying all beans in its bean factory.
	 * <p>Delegates to {@code doClose()} for the actual closing procedure.
	 * Also removes a JVM shutdown hook, if registered, as it's not needed anymore.
	 * @see #doClose()
	 * @see #registerShutdownHook()
	 */
	@Override
	public void close() {
		synchronized (this.startupShutdownMonitor) {
			doClose(); //重点:销毁bean 并执行jvm shutdown hook
			// If we registered a JVM shutdown hook, we don't need it anymore now:
			// We've already explicitly closed the context.
			if (this.shutdownHook != null) {
				try {
					Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
				}
				catch (IllegalStateException ex) {
					// ignore - VM is already shutting down
				}
			}
		}
	}

后记

到这里,关于单机版本的Spring Boot优雅停机就说完了。为什么说单机?因为大家也能发现,在关闭时,其实只是保证了服务端内部线程执行完毕,调用方的状态是没关注的。

不论是Dubbo还是Cloud 的分布式服务框架,需要关注的是怎么能在服务停止前,先将提供者在注册中心进行反注册,然后在停止服务提供者,这样才能保证业务系统不会产生各种503、timeout等现象。

好在当前Spring Boot 结合Kubernetes已经帮我们搞定了这一点,也就是Spring Boot 2.3版本新功能Liveness(存活状态) 和Readiness(就绪状态)

简单的提下这两个状态:

  • Liveness(存活状态):Liveness 状态来查看内部情况可以理解为health check,如果Liveness失败就就意味着应用处于故障状态并且目前无法恢复,这种情况就重启吧。此时Kubernetes如果存活探测失败将杀死Container。
  • Readiness(就绪状态):用来告诉应用是否已经准备好接受客户端请求,如果Readiness未就绪那么k8s就不能路由流量过来。

到此这篇关于Spring Boot最新版优雅停机的文章就介绍到这了,更多相关Spring Boot优雅停机内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • spring boot2.0实现优雅停机的方法

    前期踩的坑 (spring boot 1.x) 1. 添加mavne依赖 <!-- springboot监控 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> 2. 启用shutdown 在配置文件里添加下面的配置 #

  • SpringBoot2.3新特性优雅停机详解

    什么是优雅停机 先来一段简单的代码,如下: @RestController public class DemoController { @GetMapping("/demo") public String demo() throws InterruptedException { // 模拟业务耗时处理流程 Thread.sleep(20 * 1000L); return "hello"; } } 当我们流量请求到此接口执行业务逻辑的时候,若服务端此时执行关机 (ki

  • 详解Spring Boot最新版优雅停机的方法

    什么是优雅停机 先来一段简单的代码,如下: @RestController public class DemoController { @GetMapping("/demo") public String demo() throws InterruptedException { // 模拟业务耗时处理流程 Thread.sleep(20 * 1000L); return "hello"; } } 当我们流量请求到此接口执行业务逻辑的时候,若服务端此时执行关机 (ki

  • 详解Spring Boot 项目启动时执行特定方法

    Springboot给我们提供了两种"开机启动"某些方法的方式:ApplicationRunner和CommandLineRunner. 这两种方法提供的目的是为了满足,在项目启动的时候立刻执行某些方法.我们可以通过实现ApplicationRunner和CommandLineRunner,来实现,他们都是在SpringApplication 执行之后开始执行的. CommandLineRunner接口可以用来接收字符串数组的命令行参数,ApplicationRunner 是使用App

  • 详解Spring boot Admin 使用eureka监控服务

    前言 最近刚好有空,来学习一下如何搭建spring boot admin环境.其中遇到很多的坑. 网上大多都是使用admin-url的方式直接来监控的,感觉一点也不灵活,这不是我想要的结果,所以本篇介绍借助eureka服务注册和发现功能来灵活监控程序. 本文主要记录spring boot admin的搭建过程,希望能有所帮助.其实非常的简单,不要被使用常规方式的误导! 环境介绍 IDE:intellij idea jdk: java8 maven:3.3.9 spring boot:1.5.6

  • 详解spring boot jpa整合QueryDSL来简化复杂操作

    前言 使用过spring data jpa的同学,都很清楚,对于复杂的sql查询,处理起来还是比较复杂的,而本文中的QueryDSL就是用来简化JPA操作的. Querydsl定义了一种常用的静态类型语法,用于在持久域模型数据之上进行查询.JDO和JPA是Querydsl的主要集成技术.本文旨在介绍如何使用Querydsl与JPA组合使用.JPA的Querydsl是JPQL和Criteria查询的替代方法.QueryDSL仅仅是一个通用的查询框架,专注于通过Java API构建类型安全的SQL查

  • 详解spring boot starter redis配置文件

    spring-boot-starter-Redis主要是通过配置RedisConnectionFactory中的相关参数去实现连接redis service. RedisConnectionFactory是一个接口,有如下4个具体的实现类,我们通常使用的是JedisConnectionFactory. 在spring boot的配置文件中redis的基本配置如下: # Redis服务器地址 spring.redis.host=192.168.0.58 # Redis服务器连接端口 spring.

  • 实例详解Spring Boot实战之Redis缓存登录验证码

    本章简单介绍redis的配置及使用方法,本文示例代码在前面代码的基础上进行修改添加,实现了使用redis进行缓存验证码,以及校验验证码的过程. 1.添加依赖库(添加redis库,以及第三方的验证码库) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency&

  • 详解Spring Boot中使用Flyway来管理数据库版本

    如果没有读过上面内容的读者,有兴趣的可以一阅.在上面的使用JdbcTemplate一文中,主要通过spring提供的JdbcTemplate实现对用户表的增删改查操作.在实现这个例子的时候,我们事先在MySQL中创建了用户表.创建表的过程我们在实际开发系统的时候会经常使用,但是一直有一个问题存在,由于一个系统的程序版本通过git得到了很好的版本控制,而数据库结构并没有,即使我们通过Git进行了语句的版本化,那么在各个环境的数据库中如何做好版本管理呢?下面我们就通过本文来学习一下在Spring B

  • 详解spring boot rest例子

    简介:本文将帮助您使用 Spring Boot 创建简单的 REST 服务. 你将学习 什么是 REST 服务? 如何使用 Spring Initializr 引导创建 Rest 服务应用程序? 如何创建获取 REST 服务以检索学生注册的课程? 如何为学生注册课程创建 Post REST 服务? 如何利用 postman 执行 rest 服务? 本教程使用的 rest 服务 在本教程中,我们将使用适当的 URI 和 HTTP 方法创建三个服务: @GetMapping("/ students

  • 详解Spring Boot 目录文件结构

    1.目录结构 src/main/java:存放代码 src/main/resources resources:(Spring Boot 默认的)存放资源文件 static:(Spring Boot 默认的)存放静态文件,比如 css.js.image, (访问方式 http://localhost:8080/js/main.js) public:(Spring Boot 默认的)存放公共文件 templates:(用户自己定义的,可以随便取名,但这里使用公认的文件名)存放静态页面,比如 jsp.

  • 详解Spring Boot 打包分离依赖JAR 和配置文件

    1:自定义路径 <properties> <!--自定义路径--> <directory>d:/im/</directory> </properties> 2:把配置文件打包出来 <build> <plugins> <!--上线部署 JAR启动分离依赖lib和配置--> <!--打包jar--> <plugin> <groupId>org.apache.maven.plugi

随机推荐