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 -2 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 -2 xxxPID,pid可以通過文件或命令根據端口查找什么的,手頭上暫沒有linux來測試,后期有了后補上完整腳本。
  • 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才能關閉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM