纯洁的微笑 今天

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

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. socket通信原理三次握手和四次握手详解

    对TCP/IP.UDP.Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵.那么我想问: 1.         什么是TCP/IP.UDP?2.         Sock ...

  2. 重构客户注册-基于ActiveMQ实现短信验证码生产者

    重构目标:将bos_fore项目中的CustomerAction作为短信消息生产者,将消息发给ActiveMQ,创建一个单独的SMS项目,作为短信息的消费者,从ActiveMQ获取短信消息,调用第三方 ...

  3. Hbase API

  4. java学习之—递归实现变位字

    /** * 递归实现变位字 * Create by Administrator * 2018/6/20 0020 * 上午 10:23 **/ public class AnagramApp { st ...

  5. 洛谷 p1219 八皇后

    刚参加完蓝桥杯 弱鸡错了好几道..回头一看确实不难 写起来还是挺慢的 于是开始了刷题的道路 蓝桥杯又名搜索杯 暴力杯...于是先从dfs刷起 八皇后是很经典的dfs问题 洛谷的这道题是这样的 上面的布 ...

  6. Python学习之路——day05

    今日内容:1.可变与不可变类型:可变类型:值可以改变,但是id不变,证明就是在改变原值,是可变类型不可变类型:值改变,但是id也跟着改变,证明是残生了新的值,是不可变类型 2.数字类型2.1整型:记录 ...

  7. How to sign app

    codesign --display --verbose=4 /applications/qq.app codesign --display --entitlements - /application ...

  8. Running Web API using Docker and Kubernetes

    Context As companies are continuously seeking ways to become more Agile and embracing DevOps culture ...

  9. 安装.Net Standard 2.0, Impressive

    此版本的.NET Standard现在支持大约33K的API,与.NET Standard 1.x支持的14K API相比.好的是大部分API来自.NET Framework.这使得生活更容易将代码移 ...

  10. vue的 v-for 循环中图片加载路径问题

    先看一下产品需求,如下图所示, 产品要求图片和它的名称一一对应,本来是非常简单的需求,后台直接返回图片路径和名称,前台直接读取就可以了,但是我们没有存储图片的服务器,再加上是一个实验性的需求,图片需要 ...