背景

最近在搞云化项目的启动脚本,觉得以往kill方式关闭服务项目太粗暴了,这种kill关闭应用的方式会让当前应用将所有处理中的请求丢弃,响应失败。这种形式的响应失败在处理重要业务逻辑中是要极力避免的,所以我们需要一种更加优雅的方式关闭springBoot应用。

基本思路

首先我们关闭一个微服务应用可以分为两大步骤

  • 关闭web应用服务器
  • 关闭spring容器

我项目中使用的是内置的tomcat服务器,所以本文描述的是如何平滑的关闭tomcat应用。SpringBoot Actuator中提供了shutdown端点,利用此端点可以http的方式远程关闭spring 容器,下文讲述了如何使用SpringBoot Actuator的shutdown。

开启Shutdown Endpoint

Spring Boot Actuator 是 Spring Boot 的一大特性,它提供了丰富的功能来帮助我们监控和管理生产环境中运行的 Spring Boot 应用。我们可以通过 HTTP 或者 JMX 方式来对我们应用进行管理,除此之外,它为我们的应用提供了审计,健康状态和度量信息收集的功能,能帮助我们更全面地了解运行中的应用。

引入Actuator

本项目基于gradle构建,引入 " spring-boot-starter-actuator "如下

api('org.springframework.boot:spring-boot-starter-actuator:2.2.5.RELEASE')

开放端口

Spring Boot Actuator 采用向外部暴露 Endpoint (端点)的方式来让我们与应用进行监控和管理,引入 spring-boot-starter-actuator 之后,就需要启用我们需要的 Shutdown Endpoint。在application.yml中添加如下配置。

management:
  endpoints:
    web:
      exposure:
        include: "httptrace,health,shutdown"
        ## 健康检查根路径
      base-path: "/actuator"
  endpoint:
    shutdown:
      enabled: true
    health:
      show-details: always

建议在include中根据自己的需要开放对应的端口,最好不要直接写“*”。这里由于项目中需要健康检查,所以添加了health,。

添加shutdown过滤器

一般来说使用shutdown端口是需要做权限控制的,但是由于这个项目有部署的时候,有对应的网关,所以这里就比较简单的增加了一个白名单功能。根据配置文件,来控制对应的ip是否可以访问此端口。

  1. 添加ActuatorFilter 

@Slf4j
@RefreshScope
public class ActuatorFilter implements Filter {

  public static final String UNKNOWN = "unknown";

  @Value("${shutdown.whitelist}")
  private String[] shutdownIpWhitelist;

  @Override
  public void destroy() {
  }

  @Override
  public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain filterChain)
      throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) srequest;

    String ip = this.getIpAddress(request);

    log.info("访问shutdown的机器的原始IP:{}", ip);

    if (!isMatchWhiteList(ip)) {
      sresponse.setContentType("application/json");
      sresponse.setCharacterEncoding("UTF-8");
      PrintWriter writer = sresponse.getWriter();
      writer.write("{\"code\":401,\"error\":\"IP access forbidden\"}");
      writer.flush();
      writer.close();
      log.warn("ip:{}禁止shutdown", ip);
      return;
    }

    filterChain.doFilter(srequest, sresponse);
  }

  @Override
  public void init(FilterConfig arg0) throws ServletException {
    log.info("Actuator filter is init.....");
  }

  /**
   * 匹配是否是白名单
   */
  private boolean isMatchWhiteList(String ip) {
    List<String> list = Arrays.asList(shutdownIpWhitelist);
    return list.stream().anyMatch(item -> ip.startsWith(item));
  }

  /**
   * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
   * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
   * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
   *
   * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100
   *
   * 用户真实IP为: 192.168.1.110
   */
  private String getIpAddress(HttpServletRequest request) {
    String ip = request.getHeader("x-forwarded-for");
    if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
      ip = request.getHeader("Proxy-Client-IP");
    }
    if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
      ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
      ip = request.getHeader("HTTP_CLIENT_IP");
    }
    if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
      ip = request.getHeader("HTTP_X_FORWARDED_FOR");
    }
    if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
      ip = request.getRemoteAddr();
    }
    return ip;
  }

}

这里注意不能在类ActuatorFilter 上加注解@Component,加上改过滤器会过滤所有url。

  2.添加过滤器Config

@Configuration
public class WebFilterConfig extends WebMvcConfigurationSupport {

  @Bean
  public ActuatorFilter getActuatorFilter() {
    return new ActuatorFilter();
  }

  @Bean
  public FilterRegistrationBean setShutdownFilter(ActuatorFilter actuatorFilter) {
    FilterRegistrationBean<ActuatorFilter> registrationBean = new FilterRegistrationBean<>();

    registrationBean.setFilter(actuatorFilter);
    registrationBean.setName("actuatorFilter");
    registrationBean.addUrlPatterns("/actuator/shutdown");

    return registrationBean;
  }

}

3.添加白名单配置

application.yml中添加如下配置

shutdown:
  whitelist: 0:0:0:0:0:0:0:1,127.0.0.1

到这里我们的shutdown配置工作就算完成了。当启动应用后,只能本地以POST 方式请求对应路径的“ http://host:port/actuator/shutdown “”来实现springboot容器的关闭。

关闭Tomcat

要平滑关闭 Spring Boot 应用的前提就是首先要关闭其内置的 Web 容器,不再处理外部新进入的请求。为了能让应用接受关闭事件通知的时候,保证当前 Tomcat 处理所有已经进入的请求,我们需要实现 TomcatConnectorCustomizer 接口,此接口是实现自定义 Tomcat Connector 行为的回调接口。

自定义 Connector

Connector 属于 Tomcat 抽象组件,功能就是用来接收外部请求、内部传递,并返回响应内容,是Tomcat 中请求处理和响应的重要组。Connector 具体实现有 HTTP Connector 和 AJP Connector。

通过定制 Connector 的行为,我们就可以允许在请求处理完毕后进行 Tomcat 线程池的关闭,具体实现代码如下:

@Slf4j
public class CustomShutdown implements TomcatConnectorCustomizer,
    ApplicationListener<ContextClosedEvent> {

  private static final int TIME_OUT = 30;

  private volatile Connector connector;

  @Override
  public void customize(Connector connector) {
    this.connector = connector;
  }

  @Override
  public void onApplicationEvent(ContextClosedEvent event) {

    /* Suspend all external requests*/
    this.connector.pause();
    /* Get ThreadPool For current connector */
    Executor executor = this.connector.getProtocolHandler().getExecutor();
    if (executor instanceof ThreadPoolExecutor) {
      log.warn("当前Web应用准备关闭");
      try {
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
        /* Initializes a shutdown task after the current one has been processed task*/
        threadPoolExecutor.shutdown();
        if (!threadPoolExecutor.awaitTermination(TIME_OUT, TimeUnit.SECONDS)) {
          log.warn("当前应用等待超过最大时长{}秒,将强制关闭", TIME_OUT);
          /* Try shutDown Now*/
          threadPoolExecutor.shutdownNow();
          if (!threadPoolExecutor.awaitTermination(TIME_OUT, TimeUnit.SECONDS)) {
            log.error("强制关闭失败", TIME_OUT);
          }
        }
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
      }
    }
  }
}

上述代码定义的 TIMEOUT 变量为 Tomcat 线程池延时关闭的最大等待时间,一旦超过这个时间就会强制关闭线程池,所以我们可以通过控制 Tomcat 线程池的关闭时间,(当然了这个也可以写成可配的) 来实现优雅关闭 Web 应用的功能。同时 CustomShutdown 实现了 ApplicationListener<ContextClosedEvent> 接口,意味着我们会监听着 Spring 容器关闭的事件,即当前的 ApplicationContext 执行 close 方法。

添加 Connector 回调

在启动过程中将定制的Connetor回调添加到内嵌的 Tomcat 容器中,然后等待执行。

@Configuration
public class ShutdownConfig {

  @Bean
  public CustomShutdown customShutdown() {
    return new CustomShutdown();
  }

  @Bean
  public ConfigurableServletWebServerFactory webServerFactory(final CustomShutdown customShutdown) {
    TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
    tomcatServletWebServerFactory.addConnectorCustomizers(customShutdown);
    return tomcatServletWebServerFactory;
  }
}

这里的 TomcatServletWebServerFactory 是 Spring Boot 实现内嵌 Tomcat 的工厂类。其他的 Web 容器,也有对应的工厂类如 JettyServletWebServerFactory,UndertowServletWebServerFactory。他们共都是继承抽象类 AbstractServletWebServerFactory。AbstractServletWebServerFactory提供了 Web 容器默认的公共实现,如应用上下文设置,会话管理等。  到这里我们的Tomcat平滑关闭就ok了

添加启动脚本

实际生产中我都会制作jar 然后发布。通常应用的启动和关闭操作流程是固定且重复的,以避免出现人为的差错,并且方便使用,提高操作效率,一般会配上对应的程序启动脚本来控制程序的启动和关闭。

对应关闭操作的shell脚本部分如下所示。

SEVER_PORT=
export START_JAR_NAME="test-*.jar"

START_JAR=$(ls $PRG_HOME | grep $START_JAR_NAME)

stop() {
    echo  $"Stoping : "

    boot_id=$(pgrep -f "$START_JAR")
    count=$(pgrep -f "$START_JAR" | wc -l)
     ];then
       curl -X POST "http://localhost:$SEVER_PORT/actuator/shutdown"

       ))
        do
         kill $boot_id

         count=$(pgrep -f "$START_JAR" | wc -l)
        done
       echo  "服务已停止: "
    else
        echo  "服务未在运行"
    fi
}

总结

本文主要探究了如何对优雅关闭基于Spring Boot 内嵌 Tomcat 的 Web 应用的实现,如果采用其他 Web 容器也类似方式,希望这边文章有所帮助,若有错误或者不当之处,还请大家批评指正,一起学习交流。

参考链接

https://www.cnblogs.com/one12138/p/11241274.html

如何优雅的关闭基于Spring Boot 内嵌 Tomcat 的 Web 应用的更多相关文章

  1. Spring Boot 内嵌Tomcat的端口号的修改

    操作非常的简单,不过如果从来没有操作过,也是需要查找一下资料的,所以,在此我简单的记录一下自己的操作步骤以备后用! 1:我的Eclipse版本,不同的开发工具可能有所差异,不过大同小异 2:如何进入对 ...

  2. Spring Boot内嵌Tomcat session超时问题

    最近让Spring Boot内嵌Tomcat的session超时问题给坑了一把. 在应用中需要设置session超时时间,然后就习惯的在application.properties配置文件中设置如下, ...

  3. Spring Boot启动过程(四):Spring Boot内嵌Tomcat启动

    之前在Spring Boot启动过程(二)提到过createEmbeddedServletContainer创建了内嵌的Servlet容器,我用的是默认的Tomcat. private void cr ...

  4. Spring Boot 内嵌容器 Tomcat / Undertow / Jetty 优雅停机实现

    Spring Boot 内嵌容器 Tomcat / Undertow / Jetty 优雅停机实现 Anoyi 精讲JAVA 精讲JAVA 微信号 toooooooozi 功能介绍 讲解java深层次 ...

  5. Spring boot 内置tomcat禁止不安全HTTP方法

    Spring boot 内置tomcat禁止不安全HTTP方法 在tomcat的web.xml中可以配置如下内容,让tomcat禁止不安全的HTTP方法 <security-constraint ...

  6. Spring Boot 容器选择 Undertow 而不是 Tomcat Spring Boot 内嵌容器Unde

    Spring Boot 内嵌容器Undertow参数设置 配置项: # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 # 不要设置过大,如果过大,启动 ...

  7. Spring Boot内置Tomcat

    Spring Boot默认支持Tomcat/Jetty/Undertow作为底层容器.在之前实战相关的文章中,可以看到引入spring-boot-starter-web就默认使用tomcat容器,这是 ...

  8. 自定义Spring Boot内置tomcat的404页面

    spring boot 的相关404页面配置都是针对项目路径下的(如果配置了 context-path) 在context-path不为空的情况下,如果访问路径不带context-path,这时候会显 ...

  9. Spring boot内置Tomcat的临时目录被删除导致文件上传不了-问题解析

    目录 1.问题 2.1. 为什么需要使用这个/tmp/tomcat*? 2.2.那个 /tmp/tomcat* 目录为什么不存在? 三.解决办法 修改 springboot 配置,不要在/tmp 下创 ...

随机推荐

  1. Web架构之Nginx基础配置

    目录 1.Nginx 虚拟主机 1.1.基于域名的虚拟主机 1.2.基于端口的虚拟主机 1.3.基于IP的虚拟主机 2.Nginx include 3.Nginx 日志配置 3.1.访问日志 3.2. ...

  2. 作为一个Tester,你在客户环境能保证质量吗?

    公司严格地按照“产品-项目”模式来架构技术部门. 我又测产品,又测项目,所以一方面可以从项目测试的角度发现产品bug,并且给产品提供改进意见,一方面还能测产品为项目赋能,保证项目质量,让项目经理轻松些 ...

  3. CouchDB的简单使用

    一.安装CouchDB 到官网下载CouchDB,在windows下安装CouchDB较为简单,略过. 安装完后,确认CouchDB在运行,然后在浏览器访问http://127.0.0.1:5984/ ...

  4. scrapy启动

    创建项目 在开始爬取之前,您必须创建一个新的Scrapy项目. 进入您打算存储代码的目录中,运行下列命令: scrapy startproject scrapytest 第一种scrapy gensp ...

  5. 一个和与后台数据连接的模板get post put 以及延伸的query

    /* example: require.config({ paths: { "httpClient": "../../core/http-client" } } ...

  6. windows下安装spark-python

    首先需要安装Java 下载安装并配置Spark 从官方网站Download Apache Spark™下载相应版本的spark,因为spark是基于hadoop的,需要下载对应版本的hadoop才行, ...

  7. 计算属性(computed)+侦听器(watch)+ 方法(methods)

    计算属性 computed 当数据改变时,方法的结果也会发生改变.如果多处地方调用计算属性里面的同一个方法时,该方法只会执行一次.如图,在控制台改变data里面的num值时,虽然在多处使用comput ...

  8. 浅析Redis分布式锁---从自己实现到Redisson的实现

    当我们在单机情况下,遇到并发问题,可以使用juc包下的lock锁,或者synchronized关键字来加锁.但是这俩都是JVM级别的锁,如果跨了JVM这两个锁就不能控制并发问题了,也就是说在分布式集群 ...

  9. this.baseInfoList = [...this.baseInfoList] 所有和数组有关的操作,最后一定都展开一次,否则就没有双向绑定!!

    this.baseInfoList = [...this.baseInfoList] 所有和数组有关的操作,最后一定都展开一次,否则就没有双向绑定!! this.baseInfoList = [... ...

  10. File类的构造方法:三种重载形式(新手)

    //导入的包.import java.io.File;//File类的构造方法 三种重载形式//创建的一个类.public class zylx1 { //公共静态的主方法. public stati ...