============================
背景
============================
在系統生命週期中, 免不了要作升級部署, 對於關鍵服務, 咱們應該能作到不停服務完成升級 (perform a zero downtime upgrade), 對於通常系統, 應該作到優雅地停服務.html
如何作到不停服務的升級? 須要作到下面兩點:
1. 服務自己應該部署多份, 前面應該有 LVS/Haproxy 層或者服務註冊組件.
2. 每一份服務能被優雅停機, 即: 在 kill pid 命令發出後, 程序應該能拒絕新的請求, 但應該繼續完成已有請求的處理.java
本文重點關注如何支持優雅停機.git
============================
Linux kill 命令
============================
kill 命令經常使用的信號選項:
(1) kill -2 pid 向指定 pid 發送 SIGINT 中斷信號, 等同於 ctrl+c.
(2) kill -9 pid, 向指定 pid 發送 SIGKILL 當即終止信號.
(3) kill -15 pid, 向指定 pid 發送 SIGTERM 終止信號.
(4) kill pid 等同於 kill 15 pidgithub
SIGINT/SIGKILL/SIGTERM 信號的區別:
(1) SIGINT (ctrl+c) 信號 (信號編號爲 2), 信號會被當前進程樹接收到, 也就說, 不只當前進程會收到該信號, 並且它的子進程也會收到.
(2) SIGKILL 信號 (信號編號爲 9), 程序不能捕獲該信號, 最粗暴最快速結束程序的方法.
(3) SIGTERM 信號 (信號編號爲 15), 信號會被當前進程接收到, 但它的子進程不會收到, 若是當前進程被 kill 掉, 它的的子進程的父進程將變成 init 進程 (init 進程是那個 pid 爲 1 的進程)web
通常要結束某個進程, 咱們應該優先使用 kill pid , 而不是 kill -9 pid. 若是對應程序提供優雅關閉機制的話, 在徹底退出以前, 先能夠作一些善後處理.spring
============================
Java 對於優雅停機的底層支持
============================
Java 語言底層有機制能捕獲到 OS 的 SIGINT/ SIGTERM 中止指令的, 具體是經過 Runtime.getRuntime().addShutdownHook() 向 JVM 中註冊一個 Shutdown hook 線程, 當 JVM 收到中止信號後, 該線程將被激活運行, 這時候咱們就能夠向其餘線程發出中斷指令, 進而快速而優雅地關閉整個程序.docker
public class Test { public static void main(String[] args){ System.out.println("1: Main start"); Thread mainThread = Thread.currentThread(); //註冊一個 ShutdownHook ShutdownSampleHook thread=new ShutdownSampleHook(mainThread); Runtime.getRuntime().addShutdownHook(thread); try { Thread.sleep(30*1000); } catch (InterruptedException e) { System.out.println("3: mainThread get interrupt signal."); } System.out.println("4: Main end"); } } class ShutdownSampleHook extends Thread { private Thread mainThread; @Override public void run() { System.out.println("2: Shut down signal received."); mainThread.interrupt();//給主線程發送一箇中斷信號 try { mainThread.join(); //等待 mainThread 正常運行完畢 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("5: Shut down complete."); } public ShutdownSampleHook(Thread mainThread) { this.mainThread=mainThread; } }
關於 mainThread.interrupt() 方法的說明, 該方法將給主線程發送一箇中斷信號. 若是主線程沒有進入阻塞狀態, interrupt() 其實沒有什麼做用; 若是主線程處於阻塞狀態, 該線程將獲得一個 InterruptedException 異常. 在調用 mainThread.join() 或 mainThread.wait() 以前, 仍能夠經過調用 mainThread.interrupted() 來清除中斷信號.
一個線程有三種進入阻塞狀態的方法, 分別是調用 Thread.wait() 或 Thread.join() 或 Thread.sleep().apache
正常狀況下, 程序須要運行 30 秒, 程序的輸出是:瀏覽器
若是在程序啓動後, 按下 Ctrl+C, 程序很快就結束了, 最終的輸出是:tomcat
============================
SpringBoot Web 項目的優雅停機
============================
Java web 服務器一般也支持優雅退出, 好比 tomcat, 提供以下命令:
catalina.sh stop n , 先等 n 秒後, 而後中止 tomcat.
catalina.sh stop n -force , 先等 n 秒後, 而後 kill -9 tomcat.
SpringBoot Web 項目, 若是使用的是外置 tomcat, 能夠直接使用上面 tomcat 命令完成優雅停機. 但一般使用的是內置 tomcat 服務器, 這時就須要編寫代碼來支持優雅中止.
網上不少文章都說起 Actuator 的 shutdown 提供優雅停機功能, 官方文檔也是這麼宣傳的, 其實並無實現優雅停機功能, 至少在 SpringBoot 2.1.0, 在 github issues/4657 也有說起, 也許未來會實現, https://github.com/spring-projects/spring-boot/issues/4657
本節全部的代碼摘自 https://dzone.com/articles/graceful-shutdown-spring-boot-applications
下面是一個簡單的測試代碼:
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class LongProcessController { @RequestMapping("/long-process") public String pause() throws InterruptedException { Thread.sleep(20*1000); System.out.println("Process finished"); return "Process finished"; } }
appication.properties 文件內容:
management.endpoint.shutdown.enabled=true management.endpoints.web.exposure.include=*
瀏覽器訪問 GET http://localhost:8080/long-process , 緊接訪問actuator shutdown 端點: POST http://localhost:8080/actuator/shutdown , 當應用程序中止時, GET請求並無獲得返回值, 可見 Actuator 並無提供優雅停機功能.
------------------------------------
增長 GracefulShutdown Connector 監聽類
------------------------------------
當 tomcat 收到 kill 信號後, web程序先關閉新的請求, 而後等待 30 秒, 最後結束整個程序.
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.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> { private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class); private volatile Connector connector; @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; threadPoolExecutor.shutdown(); if (!threadPoolExecutor.awaitTermination(30, 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(); } } } }
------------------------------------
註冊自定義的 Connector 監聽器
------------------------------------
在 @SpringBootApplication 入口類中, 增長下面的代碼, 註冊以前定義的 Connector 監聽器.
@Bean public GracefulShutdown gracefulShutdown() { return new GracefulShutdown(); } @Bean public ConfigurableServletWebServerFactory webServerFactory(final GracefulShutdown gracefulShutdown) { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(gracefulShutdown); return factory; }
============================
graceful shutdown-down spring-boot starters
============================
上面的示例代碼基本能知足咱們的須要, github上甚至有幾個專門處理 graceful shutdown-down 的 starter 庫, 使用這些 starter 包就不須要上編寫 Tomcat Connector 監聽類.
https://github.com/jihor/hiatus-spring-boot , 支持SpringBoot 2
https://github.com/Askerlve/grace-shutdown , 支持SpringBoot 2
https://github.com/gesellix/graceful-shutdown-spring-boot , 有一些有關 docker 的信息.
hiatus-spring-boot 庫是一個頗有意思的庫, 它並無實現一個 Tomcat Connector 監聽類, 因此直接 kill pid, 將不會有善後處理過程, 它而僅僅是修改 actuator/health 狀態爲 OUT_OF_SERVICE, 因此要想截流量功能必須配合 discovery server. 項目取名爲 hiatus , 該單詞和 pause 意思相近, 項目取名是很準確的, 僅僅是暫停服務, 後續能夠從新開啓服務.
hiatus-spring-boot 的特色是:
1. actuator metrics 端點只能提供OS/JVM/Tomcat類的指標, hiatus 引入一個 @UnitOfWork 計數器註解, 加在視圖方法上, 能夠做爲一個業務方面的 metrics.
2. 實現了三個 actuator 端點
/actuator/hiatus-on 端點(POST), 停歇 springBoot 程序,
/actuator/hiatus-off 端點(POST), 恢復 springBoot 程序.
/actuator/hiatus 端點(GET), 查詢暫停狀態和正在處理的request數量.
進入 hiatus 狀態後, actuator/health 端點查詢的結果是 "status":"OUT_OF_SERVICE".
項目中的推薦的作法是, 引入 hiatus-spring-boot, 並參考上面示例編寫一個Tomcat Connector 監聽類, 這樣既能優雅應對 kill pid, 又能作到主動截留.
============================
docker 微服務的優雅關閉
============================
詳見 https://www.cnblogs.com/harrychinese/p/springboot_Dockerize_SpringBoot_App.html 的"docker 微服務的優雅關閉"章節.
============================ 參考 ============================ https://www.jianshu.com/p/0c49eb23c627 https://www.jianshu.com/p/073a0da36d48 https://dzone.com/articles/graceful-shutdown-spring-boot-applications