纯洁的微笑 今天

项目在重新发布的过程中,如果有的请求时间比较长,还没执行完成,此时重启的话就会导致请求中断,影响业务功能,优雅重启可以保证在停止的时候,不接收外部的新的请求,等待未完成的请求执行完成,这样可以保证数据的完整性。

Spring Boot 1.X

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
/**
* Spring Boot1.X Tomcat容器优雅停机
* @author yinjihuan
*
*/
@Configuration
public class ShutdownConfig {
   /**
    * 用于接受shutdown事件
    * @return
    */
   @Bean
   public GracefulShutdown gracefulShutdown() {
       return new GracefulShutdown();
   }
   /**
    * 用于注入 connector
    * @return
    */
   @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 static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
       private volatile Connector connector;
       private final int waitTime = 120;
       @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;
                   log.info("shutdown start");
                   threadPoolExecutor.shutdown();
                   log.info("shutdown end");
                   if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                       log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
                   }
                   log.info("shutdown success");
               } catch (InterruptedException ex) {
                   Thread.currentThread().interrupt();
               }
           }
       }
   }
}

Spring Boot 2.X

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
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.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
/**
* Spring Boot2.X Tomcat容器优雅停机
* @author yinjihuan
*
*/
@Configuration
public class ShutdownConfig {
   /**
    * 用于接受shutdown事件
    * @return
    */
   @Bean
   public GracefulShutdown gracefulShutdown() {
       return new GracefulShutdown();
   }
   @Bean
   public ServletWebServerFactory servletContainer() {
     TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
     tomcat.addConnectorCustomizers(gracefulShutdown());
     return tomcat;
   }
   private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
       private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
       private volatile Connector connector;
       private final int waitTime = 120;
       @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;
                   log.info("shutdown start");
                   threadPoolExecutor.shutdown();
                   log.info("shutdown end");
                   if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                       log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
                   }
                   log.info("shutdown success");
               } catch (InterruptedException ex) {
                   Thread.currentThread().interrupt();
               }
           }
       }
   }
}

重启服务脚本:

NG="zh_CN.UTF-8"
pid=`ps ax | grep fangjia-youfang-web | grep java | head -1 | awk '{print $1}'`
echo $pid
#kill $pid
curl -X POST http://127.0.0.1:8086/shutdown?token=认证信息
while [[ $pid != "" ]]; do
   echo '服务停止中...'
   sleep 1
   pid=`ps ax | grep fangjia-youfang-web | grep java | head -1 | awk '{print $1}'`
done
echo '服务停止成功,开始重启服务...'
java -jar xxx.jar

在重启之前首先发送重启命令到endpoint,或者用kill 进程ID的方式,千万不要用kill -9。

然后循环检测进程是否存在,如果服务正常停止了,进程也就不存在了,如果进程还在,证明还有未处理完的请求,停止1秒,继续检测。

关于重启服务,建议用kill方式,这样就不用依赖spring-boot-starter-actuator,如果用endpoint方式,则需要控制好权限,不然随时都有可能被人重启了,可以用security来控制权限,我这边是自己用过滤器来控制的。

如果用actuator方式重启的话需要配置启用重启功能:
1.x配置如下:

endpoints.shutdown.enabled=true

2.x配置就比较多了,默认只暴露了几个常用的,而且访问地址也有变化,比如health, 以前是直接访问/health,现在需要 /actuator/health才能访问。我们可以通过配置来兼容之前的访问地址。

shutdown默认是不暴露的,可以通过配置暴露并开始,配置如下:

#访问路径,配置后就和1.x版本路径一样
management.endpoints.web.base-path=/
# 暴露所有,也可以暴露单个或多个
management.endpoints.web.exposure.include=*
# 开启shutdown
management.endpoint.shutdown.enabled=true

文档请参考:https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#production-ready

如何测试

测试的话我们可以写一个简单的接口,在接口中等待,然后执行脚本停止项目,如果正常的话会输出服务停止中,等到你的接口执行完成,进程才会消失掉,但是如果超过了你配置的等待时间就会强行退出。

@GetMapping("/hello")
public String hello() {
   System.out.println("req.........");
   try {
       Thread.sleep(1000 * 60 * 3);
   } catch (InterruptedException e) {
       e.printStackTrace();
   }
   return "hello";
}

需要注意的问题

如果你的项目中有用到其他的线程池,比如Spring的ThreadPoolTaskExecutor,不熟悉的同学可以参考我的这篇文章《Spring Boot Async异步执行》

在发送停止命令后如果ThreadPoolTaskExecutor有线程还没处理完的话,这个时候进程是不会自动关闭的。这个时候我们需要对线程池进行关闭处理,增加代码如下:

AsyncTaskExecutePool asyncTaskExecutePool = event.getApplicationContext().getBean(AsyncTaskExecutePool.class);
Executor executors = asyncTaskExecutePool.getAsyncExecutor();
try {
     if (executors instanceof ThreadPoolTaskExecutor) {
          ThreadPoolTaskExecutor threadPoolExecutor = (ThreadPoolTaskExecutor) executors;
          log.info("Async shutdown start");
          threadPoolExecutor.setWaitForTasksToCompleteOnShutdown(true);
          threadPoolExecutor.setAwaitTerminationSeconds(waitTime);
          threadPoolExecutor.shutdown();
     }
} catch (Exception ex) {
    Thread.currentThread().interrupt();
}

ThreadPoolTaskExecutor只有shutdown方法,没有awaitTermination方法,通过查看源码,在shutdown之前设置setWaitForTasksToCompleteOnShutdown和setAwaitTerminationSeconds同样能实现awaitTermination。

源码如下:

public void shutdown() {
       if (logger.isInfoEnabled()) {
           logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
       }
       if (this.executor != null) {
           if (this.waitForTasksToCompleteOnShutdown) {
               this.executor.shutdown();
           }
           else {
               for (Runnable remainingTask : this.executor.shutdownNow()) {
                   cancelRemainingTask(remainingTask);
               }
           }
           awaitTerminationIfNecessary(this.executor);
       }
   }

当waitForTasksToCompleteOnShutdown为true的时候就直接调用executor.shutdown();,最后执行awaitTerminationIfNecessary方法。

private void awaitTerminationIfNecessary(ExecutorService executor) {
       if (this.awaitTerminationSeconds > 0) {
           try {
               if (!executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)) {
                   if (logger.isWarnEnabled()) {
                       logger.warn("Timed out while waiting for executor" +
                               (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
                   }
               }
           }
           catch (InterruptedException ex) {
               if (logger.isWarnEnabled()) {
                   logger.warn("Interrupted while waiting for executor" +
                           (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate");
               }
               Thread.currentThread().interrupt();
           }
       }
   }

awaitTerminationIfNecessary中会判断属性awaitTerminationSeconds 如果与值的话就执行关闭等待检测逻辑,跟我们处理tomcat关闭的代码是一样的。

发现这样做之后好像没什么效果,于是我换了一种写法,直接通过获取ThreadPoolTaskExecutor中的ThreadPoolExecutor来执行关闭逻辑:

AsyncTaskExecutePool asyncTaskExecutePool = event.getApplicationContext().getBean(AsyncTaskExecutePool.class);
Executor executors = asyncTaskExecutePool.getAsyncExecutor();
try {
     if (executors instanceof ThreadPoolTaskExecutor) {
           ThreadPoolTaskExecutor threadPoolExecutor = (ThreadPoolTaskExecutor) executors;
           log.info("Async shutdown start");
           threadPoolExecutor.getThreadPoolExecutor().shutdown();
           log.info("Async shutdown end"+threadPoolExecutor.getThreadPoolExecutor().isTerminated());
           if (!threadPoolExecutor.getThreadPoolExecutor().awaitTermination(waitTime, TimeUnit.SECONDS)) {
               log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
           }
           log.info("Async shutdown success");
      }
} catch (Exception ex) {
     Thread.currentThread().interrupt();
}

这是方式也没用达到我想要的效果,当我发出kill命令之后,直接就退出了,其实我有一个后台线程在ThreadPoolTaskExecutor中运行,通过输出的日志看到,只要调用了shutdown,isTerminated方法返回的就是true,说已经关闭了,这块还没找到原因,有研究出来的小伙伴还请分享出来。

END

Spring Boot 1.X和2.X优雅重启实战的更多相关文章

  1. .NET CORE与Spring Boot编写控制台程序应有的优雅姿势

    本文分别说明.NET CORE与Spring Boot 编写控制台程序应有的“正确”方法,以便.NET程序员.JAVA程序员可以相互学习与加深了解,注意本文只介绍用法,不会刻意强调哪种语言或哪种框架写 ...

  2. Spring Boot (十五): 优雅的使用 API 文档工具 Swagger2

    1. 引言 各位在开发的过程中肯定遇到过被接口文档折磨的经历,由于 RESTful 接口的轻量化以及低耦合性,我们在修改接口后文档更新不及时,导致接口的调用方(无论是前端还是后端)经常抱怨接口与文档不 ...

  3. Spring Boot 2.3.0正式发布:优雅停机、配置文件位置通配符新特性一览

    当大潮退去,才知道谁在裸泳..关注公众号[BAT的乌托邦]开启专栏式学习,拒绝浅尝辄止.本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈.MyBatis. ...

  4. Spring Boot 2.x(十一):AOP实战--打印接口日志

    接口日志有啥用 在我们日常的开发过程中,我们可以通过接口日志去查看这个接口的一些详细信息.比如客户端的IP,客户端的类型,响应的时间,请求的类型,请求的接口方法等等,我们可以对这些数据进行统计分析,提 ...

  5. Spring Boot 2.x零基础入门到高级实战教程

    一.零基础快速入门SpringBoot2.0 1.SpringBoot2.x课程全套介绍和高手系列知识点 简介:介绍SpringBoot2.x课程大纲章节 java基础,jdk环境,maven基础 2 ...

  6. 如何在 Spring Boot 优雅关闭加入一些自定义机制

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  7. Spring Boot【快速入门】

    Spring Boot 概述 Build Anything with Spring Boot:Spring Boot is the starting point for building all Sp ...

  8. Spring Boot整合Mybatis并完成CRUD操作

    MyBatis 是一款优秀的持久层框架,被各大互联网公司使用,本文使用Spring Boot整合Mybatis,并完成CRUD操作. 为什么要使用Mybatis?我们需要掌握Mybatis吗? 说的官 ...

  9. Springboot 系列(一)Spring Boot 入门篇

    注意:本 Spring Boot 系列文章基于 Spring Boot 版本 v2.1.1.RELEASE 进行学习分析,版本不同可能会有细微差别. 前言 由于 J2EE 的开发变得笨重,繁多的配置, ...

随机推荐

  1. laravel打印sql

    DB::connection()->enableQueryLog(); print_r(DB::getQueryLog());

  2. Js中instanceof 的用法

    在 JavaScript 中,判断一个变量的类型尝尝会用 typeof 运算符,在使用 typeof 运算符时采用引用类型存储值会出现一个问题,无论引用的是什么类型的对象,它都返回 “object”. ...

  3. logback框架之——日志分割所带来的潜在问题

    源码: logback-test.xml文件如下,有2个需要我们重点关注的参数: fileNamePattern:这里的日志文件名变动的部分是年月日时,外加1个文件分割自增变量,警告,年月日时的数值依 ...

  4. django之路由层

    一 Django中路由的作用 二 简单的路由配置 三 有名分组 四 路由分发 五 反向解析 六 名称空间 七 django2.0版的path 一 Django中路由的作用 URL配置(URLconf) ...

  5. 我和我的小伙伴们都惊呆了!基于Canvas的第三方库Three.js

    What is Three.js three + js 表示运行在浏览器上的3D程序 javascript的计算能力因为google的V8引擎得到了迅猛提升 做服务器都没问题了 更别说3D了 哈哈  ...

  6. 如何使用Action.Invoke()触发一个Storyboard

    一般在我们的项目中,最好是将Storyboard放在前台,然后设置Storyboard的x:key值,通过我们的TryFindResource来查找到当前的Storyboard来启动Stroyboar ...

  7. 集合之ArrayList(含JDK1.8源码分析)

    一.ArrayList的数据结构 ArrayList底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据.我们对ArrayList类的实例的所有的操作(增删改查等),其底层都 ...

  8. python数据结构与算法第十天【插入排序】

    1.插入排序的原理 2.代码实现 def insert_sort(alist): # 从第二个位置,即下标为1的元素开始向前插入 for i in range(1, len(alist)): # 从第 ...

  9. vue 思維導圖

    vue概念:vue是一個輕量級的javascript庫:是一種漸進式的框架:vue可以實現數據視圖雙向綁定. vue基礎語法:實例化.條件.循環 vue重頭戲:動畫.組件.過濾.ajax.自定義組件. ...

  10. ABP实践学习

    一.