Spring Boot 1.X和2.X優雅重啓實戰

項目在重新發布的過程中,如果有的請求時間比較長,還沒執行完成,此時重啟的話就會導致請求中斷,影響業務功能,優雅重啟可以保證在停止的時候,不接收外部的新的請求,等待未完成的請求執行完成,這樣可以保證數據的完整性。

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  { 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  *  */@Configurationpublic 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 { 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 $pidcurl -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}'`doneecho '服務停止成功,開始重啟服務...'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=*# 開啟shutdownmanagement.endpoint.shutdown.enabled=true

#訪問路徑,配置後就和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,說已經關閉了,這塊還沒找到原因,有研究出來的小夥伴還請分享出來。


分享到:


相關文章: