如何優(yōu)雅地停止 Spring Boot 應(yīng)用凄贩?

首先來介紹下什么是優(yōu)雅地停止,簡而言之袱讹,就是對應(yīng)用進(jìn)程發(fā)送停止指令之后疲扎,能保證正在執(zhí)行的業(yè)務(wù)操作不受影響,可以繼續(xù)完成已有請求的處理捷雕,但是停止接受新請求椒丧。

在 Spring Boot 2.3 中增加了新特性優(yōu)雅停止,目前 Spring Boot 內(nèi)置的四個(gè)嵌入式 Web 服務(wù)器(Jetty救巷、Reactor Netty瓜挽、Tomcat 和 Undertow)以及反應(yīng)式和基于 Servlet 的 Web 應(yīng)用程序都支持優(yōu)雅停止。

下面征绸,我們先用新版本嘗試下:

Spring Boot 2.3 優(yōu)雅停止

首先創(chuàng)建一個(gè) Spring Boot 的 Web 項(xiàng)目,版本選擇 2.3.0.RELEASE俄占,Spring Boot 2.3.0.RELEASE 版本內(nèi)置的 Tomcat 為 9.0.35管怠。

然后需要在 application.yml 中添加一些配置來啟用優(yōu)雅停止的功能:

# 開啟優(yōu)雅停止 Web 容器,默認(rèn)為 IMMEDIATE:立即停止
server:
  shutdown: graceful

# 最大等待時(shí)間
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

其中缸榄,平滑關(guān)閉內(nèi)置的 Web 容器(以 Tomcat 為例)的入口代碼在 org.springframework.boot.web.embedded.tomcatGracefulShutdown 里渤弛,大概邏輯就是先停止外部的所有新請求,然后再處理關(guān)閉前收到的請求甚带,有興趣的可以自己去看下她肯。

內(nèi)嵌的 Tomcat 容器平滑關(guān)閉的配置已經(jīng)完成了,那么如何優(yōu)雅關(guān)閉 Spring 容器了鹰贵,就需要 Actuator 來實(shí)現(xiàn) Spring 容器的關(guān)閉了晴氨。

然后加入 actuator 依賴,依賴如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后接著再添加一些配置來暴露 actuator 的 shutdown 接口:

# 暴露 shutdown 接口
management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: shutdown

其中通過 Actuator 關(guān)閉 Spring 容器的入口代碼在 org.springframework.boot.actuate.context 包下 ShutdownEndpoint 類中碉输,主要的就是執(zhí)行 doClose() 方法關(guān)閉并銷毀 applicationContext籽前,有興趣的可以自己去看下。

配置搞定后,然后在 controller 包下創(chuàng)建一個(gè) WorkController 類枝哄,并有一個(gè) work 方法肄梨,用來模擬復(fù)雜業(yè)務(wù)耗時(shí)處理流程,具體代碼如下:

@RestController
public class WorkController {

    @GetMapping("/work")
    public String work() throws InterruptedException {
        // 模擬復(fù)雜業(yè)務(wù)耗時(shí)處理流程
        Thread.sleep(10 * 1000L);
        return "success";
    }
}

然后挠锥,我們啟動(dòng)項(xiàng)目众羡,先用 Postman 請求 http://localhost:8080/work 處理業(yè)務(wù):

然后在這個(gè)時(shí)候,調(diào)用 http://localhost:8080/actuator/shutdown 就可以執(zhí)行優(yōu)雅地停止蓖租,返回結(jié)果如下:

{
    "message": "Shutting down, bye..."
}

如果在這個(gè)時(shí)候粱侣,發(fā)起新的請求 http://localhost:8080/work,會(huì)沒有反應(yīng):

再回頭看第一個(gè)請求菜秦,返回了結(jié)果:success甜害。

其中有幾條服務(wù)日志如下:

2020-05-20 23:05:15.163  INFO 102724 --- [     Thread-253] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
2020-05-20 23:05:15.287  INFO 102724 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete
2020-05-20 23:05:15.295  INFO 102724 --- [     Thread-253] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

從日志中也可以看出來,當(dāng)調(diào)用 shutdown 接口的時(shí)候球昨,會(huì)先等待請求處理完畢后再優(yōu)雅地停止尔店。

到此為止,Spring Boot 2.3 的優(yōu)雅關(guān)閉就講解完了主慰,是不是很簡單呢嚣州?如果是在之前不支持優(yōu)雅關(guān)閉的版本如何去做呢?

Spring Boot 舊版本優(yōu)雅停止

在這里介紹 GitHub 上 issue 里 Spring Boot 開發(fā)者提供的一種方案:

選取的 Spring Boot 版本為 2.2.6.RELEASE共螺,首先要實(shí)現(xiàn) TomcatConnectorCustomizer 接口该肴,該接口是自定義 Connector 的回調(diào)接口:

@FunctionalInterface
public interface TomcatConnectorCustomizer {

    void customize(Connector connector);
}

除了定制 Connector 的行為,還要實(shí)現(xiàn) ApplicationListener<ContextClosedEvent> 接口藐不,因?yàn)橐O(jiān)聽 Spring 容器的關(guān)閉事件匀哄,即當(dāng)前的 ApplicationContext 執(zhí)行 close() 方法,這樣我們就可以在請求處理完畢后進(jìn)行 Tomcat 線程池的關(guān)閉雏蛮,具體的實(shí)現(xiàn)代碼如下:

@Bean
public GracefulShutdown gracefulShutdown() {
    return new GracefulShutdown();
}

private static 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 回調(diào)涎嚼,還需要在啟動(dòng)過程中添加到內(nèi)嵌的 Tomcat 容器中,然后等待監(jiān)聽到關(guān)閉指令時(shí)執(zhí)行挑秉,addConnectorCustomizers 方法可以把定制的 Connector 行為添加到內(nèi)嵌的 Tomcat 中法梯,具體代碼如下:

@Bean
public ConfigurableServletWebServerFactory tomcatCustomizer() {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.addConnectorCustomizers(gracefulShutdown());
    return factory;
}

到此為止,內(nèi)置的 Tomcat 容器平滑關(guān)閉的操作就完成了犀概,Spring 容器優(yōu)雅停止上面已經(jīng)說過了立哑,再次就不再贅述了。

通過測試姻灶,同樣可以達(dá)到上面那樣優(yōu)雅停止的效果铛绰。

總結(jié)

本文主要講解了 Spring Boot 2.3 版本和舊版本的優(yōu)雅停止,避免強(qiáng)制停止導(dǎo)致正在處理的業(yè)務(wù)邏輯會(huì)被中斷产喉,進(jìn)而導(dǎo)致產(chǎn)生業(yè)務(wù)異常的情形至耻。

另外使用 Actuator 的同時(shí)要注意安全問題若皱,比如可以通過引入 security 依賴,打開安全限制并進(jìn)行身份驗(yàn)證尘颓,設(shè)置單獨(dú)的 Actuator 管理端口并配置只對內(nèi)網(wǎng)開放等走触。

本文的完整代碼在 https://github.com/wupeixuan/SpringBoot-Learngraceful-shutdown 目錄下。

最好的關(guān)系就是互相成就疤苹,大家的在看互广、轉(zhuǎn)發(fā)、留言三連就是我創(chuàng)作的最大動(dòng)力卧土。

參考

https://github.com/spring-projects/spring-boot/issues/4657

https://github.com/wupeixuan/SpringBoot-Learn

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惫皱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子尤莺,更是在濱河造成了極大的恐慌旅敷,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颤霎,死亡現(xiàn)場離奇詭異媳谁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)友酱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門晴音,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缔杉,你說我怎么就攤上這事锤躁。” “怎么了或详?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵系羞,是天一觀的道長。 經(jīng)常有香客問我霸琴,道長椒振,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任沈贝,我火速辦了婚禮,結(jié)果婚禮上勋乾,老公的妹妹穿的比我還像新娘宋下。我一直安慰自己,他們只是感情好辑莫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布学歧。 她就那樣靜靜地躺著,像睡著了一般各吨。 火紅的嫁衣襯著肌膚如雪枝笨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機(jī)與錄音横浑,去河邊找鬼剔桨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛徙融,可吹牛的內(nèi)容都是我干的洒缀。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼欺冀,長吁一口氣:“原來是場噩夢啊……” “哼树绩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起隐轩,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤饺饭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后职车,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘫俊,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年提鸟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了军援。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡称勋,死狀恐怖胸哥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赡鲜,我是刑警寧澤空厌,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站银酬,受9級特大地震影響嘲更,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜揩瞪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一赋朦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧李破,春花似錦宠哄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妇菱,卻和暖如春承粤,著一層夾襖步出監(jiān)牢的瞬間暴区,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工辛臊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仙粱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓浪讳,卻偏偏與公主長得像缰盏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子淹遵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351