spring boot不同版本的优雅关闭(graceful shutdown)和在windows下winsw服务方式运行的配置

起因

  • spring boot默认是不会优雅关闭的,这样就导致在重启时会将正在运行的程序打断,导致故障发生。

当前解决方式

  • 引入spring-boot-starter-actuator监控类库,它其中一个功能支持优雅关闭。
  • spring boot 2.3版本开始,自己集成了优雅关闭,无需再引入上方类库即可实现优雅关闭。

坑爹的地方

  • spring-boot-starter-actuator文档中说是支持优雅关闭,但仅仅是spring层面上的,不和tomcat等容器挂钩,直到spring boot 2.3开启自带的优雅关闭后才真正能实现,也就是说2.3之前的版本根本实现不了优雅关闭,需要自己来进一步按照使用的容器做处理才行,参考这个issue:Allow the embedded web server to be shut down gracefully
  • 2.3以上版本,如果是在linux下,发送命令kill -15 xxx可以触发优雅关闭,但是在windows下,只有ctrl+c才能触发(可以在idea下用run里面的exit按钮模拟),但windows下我们一般是用服务来运行,所以永远无法触发ctrl+c,因此即便是2.3版本后,windows下也必须安装spring-boot-starter-actuator来通过发送http请求来实现优雅关闭。

2.3以上版本的处理方法(如果是用tomcat容器需要9.0.33以上)

  • application.yml中增加:
    # 开启优雅关闭
    server:
      shutdown: graceful
    # 配置强制结束时间,不配置的话默认30s
    spring:
      lifecycle:
        timeout-per-shutdown-phase: 30s
    
  • 配置好后就支持优雅关闭了,linux端只需要在systemctl的配置文件中设置关闭命令是kill -15 $MAINPID,例如:
    [Unit]
    Description=xxx
    After=syslog.target
    
    [Service]
    WorkingDirectory=/xxx
    ExecStart=/usr/bin/java -jar -Xms128m -Xmx512m /xxx/xxx.jar --spring.config.additional-location=/xxx/bootstrap.yml
    ExecStop=/usr/bin/kill -15 $MAINPID
    Restart=on-failure
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target
    
  • widows端虽然也支持,但是如果用服务方式运行是没法触发ctrl+c相同的效果的,所以还是不行。

2.3以下的处理方法(或者是2.3版本以上的windows端)

  • 引入spring-boot-starter-actuator的maven类库
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
  • application.yml中增加:
    #监控相关配置
    management:
      endpoint:
        # 开启
        shutdown:
          enabled: true
      endpoints:
        web:
          # 只允许shutdown,为了安全,其它想要监控自行配置
          exposure:
            include: "shutdown"
          # 自定义请求路径,为了安全
          base-path: /xxx
      server:
        #自定义请求端口,为了安全
        port: 7080
    
    发送请求的路径是这样的:curl -X POST http://localhost:自定义端口/自定义路径/shutdown,由于路径和端口都是自定义的,所以安全性方面不用太过担心。
  • 分支1:如果是2.3版本以上的windows端,再开启自带的优雅关闭,就可以通过http请求来实现了
    # 开启优雅关闭
    server:
      shutdown: graceful
    # 配置强制结束时间,不配置的话默认30s
    spring:
      lifecycle:
        timeout-per-shutdown-phase: 30s
    
  • 分支2:如果是2.3以下版本,此时虽然可以发送http请求来关闭,但实际上不会等待正在执行的程序,而是会直接关闭,还应该配置容器相关,以tomcat容器为例:
    • 创建相关类:
        package xxx.xxx.xxx;
      
        import lombok.extern.slf4j.Slf4j;
        import org.apache.catalina.connector.Connector;
        import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
        import org.springframework.context.ApplicationListener;
        import org.springframework.context.event.ContextClosedEvent;
      
        import java.util.concurrent.Executor;
        import java.util.concurrent.ThreadPoolExecutor;
        import java.util.concurrent.TimeUnit;
      
        /**
         * 优雅关闭
         */
        @Slf4j
        public class GracefulShutdown implements TomcatConnectorCustomizer,
                ApplicationListener<ContextClosedEvent> {
      
            private volatile Connector connector;
      
            /**
             * 30s强制关闭
             */
            private static final int TIMEOUT = 30;
      
            /**
             * 自定义链接
             *
             * @param connector
             */
            @Override
            public void customize(Connector connector) {
                this.connector = connector;
            }
      
            /**
             * 关闭时触发
             *
             * @param event
             */
            @Override
            public void onApplicationEvent(ContextClosedEvent event) {
                if (this.connector == null) {
                    return;
                }
                this.connector.pause();
                Executor executor = this.connector.getProtocolHandler().getExecutor();
                if (executor instanceof ThreadPoolExecutor) {
                    try {
                        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                        threadPoolExecutor.shutdown();
                        if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {
                            log.warn("Tomcat thread pool did not shut down gracefully within "
                                    + "30 seconds. Proceeding with forceful shutdown");
                        }
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
      
      • 在启动类中引入:
        package xx.xxx.xxx;
      
        import org.springframework.beans.factory.annotation.Qualifier;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
        import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
        import org.springframework.context.annotation.Bean;
      
        @SpringBootApplication
        public class XxxxApplication {
      
            /**
             * 优雅关闭bean
             * @return
             */
            @Bean("gracefulShutdown")
            public GracefulShutdown gracefulShutdown() {
                return new GracefulShutdown();
            }
      
            /**
             * tomcat配置优雅关闭
             * @param gracefulShutdown
             * @return
             */
            @Bean
            public ConfigurableServletWebServerFactory webServerFactory(@Qualifier("gracefulShutdown") GracefulShutdown gracefulShutdown) {
                TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
                factory.addConnectorCustomizers(gracefulShutdown);
                return factory;
            }
      
            public static void main(String[] args) {
                SpringApplication.run(XxxxApplication .class, args);
            }
        }
      
    • 以上配置后再发送http请求才会优雅关闭,但仅适用于tomcat容器,undertow容器可以参考这个:Spring boot 2.0 之优雅停机

windows端用winsw设置为服务运行时的配置

  • winsw可以从这里下载:winsw,主要作用就是可以让程序以服务的方式后台运行并能开机启动等
  • 需要下载windows下的curl,地址:curl for Windows
  • 配置winsw的config文件:
    <service>
      <!-- ID of the service. It should be unique across the Windows system-->
      <id>XXX</id>
      <!-- Display name of the service -->
      <name>xxx</name>
      <!-- Service description -->
      <description>xxx名称(powered by WinSW)</description>
      
      <!-- Path to the executable, which should be started -->
      <executable>java</executable>
      <startarguments>-jar -Xms128m -Xmx512m "D:\jar包路径\xxx.jar"</startarguments>
      <!--停止 -->
      <stopexecutable>D:\curl路径\bin\curl.exe</stopexecutable>
      <stoparguments>-X POST http://localhost:7080/xxx/shutdown</stoparguments>
      <!--不配置的话默认15s-->
      <stoptimeout>30 sec</stoptimeout>
      <startmode>Automatic</startmode>
      <logmode>none</logmode>
    </service>
    

结束

  • 在windows下服务方式运行通过http来发送请求关闭有个缺点,如果应用正在启动中的时候发送了关闭请求,那关闭请求是失败的,但服务并不知道你是失败的,所以会卡住,应用还是会正常启动成功,此时只能成功后再手动发送一次请求关闭,或者是用sc queryex 服务名称来找到pid,然后调用taskkill /PID 查询到的pid /F来强制关闭。不知道还有没有更好的方法来实现。
  • 使用过程中还发现了个actuator用http来发送请求关闭的问题,那就是更新部署的时候,原先是直接复制替换掉jar包,然后重启即可,但是用http方式关闭请求时,先用新jar包替代了旧jar包,再请求关闭就会报java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy错误,导致压根不能关闭,必须调整部署顺序为先关闭,然后替换jar包,然后再启动,参考A java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy was thrown when killing my app
  • 最后再加上个通常的无法优雅关闭的原因,那就是程序中有使用线程池但没有shutdown就会出导致无法优雅关闭,除非是用spring自带的ThreadPoolTaskExecutor线程池会自动关闭,其他线程池必须手动调用shutdown才能关闭。

后续

  • 我记得原先测试时windows端在服务中结束不会触发优雅关闭,但是今天心血来潮又测了下,发现竟然可以触发,可能的原因有以下几个:
    • 记忆错了,原先没有测试过?
    • 用的springboot2.3之前的版本测试?
    • 用的旧版本winsw生成的服务存在问题?
  • 有些奇怪,我早先应该是测试过的,不然怎么信誓旦旦说windows下只能用ctrl+C来触发,但是目前用springboot2.5.12WinSW 2.11.0.0测试,在windows服务中关闭确实是触发了优雅关闭,不需要引入spring-boot-starter-actuator也可以正常关闭。

后续的后续

  • 昏了头了,可能是距离写文章的时间太长,忘记最开始是怎么测试的了,反而上方后续中的测试是错的,我只看到有@PreDestroy内的日志就以为是优雅关闭,实际上并不是,可以新建一个controller,在里面写一个for循环5次,每次sleep 5秒,先发起请求,然后在服务中直接关闭,会发现for循环只执行了3次左右就关闭了,但是如果通过actuator发起的关闭则会等待全部循环完成后才会关闭,所以文章中的内容是没有问题的。
posted @ 2021-11-09 13:10  漫游云巅  阅读(6048)  评论(1编辑  收藏  举报