Spring boot 2.0 之優(yōu)雅停機

spring boot 框架在生產(chǎn)環(huán)境使用的有一段時間了,它“約定大于配置”的特性骚烧,體現(xiàn)了優(yōu)雅流暢的開發(fā)過程浸赫,它的部署啟動方式(java -jar xxx.jar)也很優(yōu)雅。但是我使用的停止應用的方式是 kill -9 進程號赃绊,即使寫了腳本既峡,還是顯得有些粗魯。這樣的應用停止方式碧查,在停止的那一霎那运敢,應用中正在處理的業(yè)務邏輯會被中斷,導致產(chǎn)生業(yè)務異常情形忠售。這種情況如何避免传惠,本文介紹的優(yōu)雅停機,將完美解決該問題稻扬。

00 前言

什么叫優(yōu)雅停機卦方?簡單說就是,在對應用進程發(fā)送停止指令之后泰佳,能保證正在執(zhí)行的業(yè)務操作不受影響盼砍。應用接收到停止指令之后的步驟應該是,停止接收訪問請求逝她,等待已經(jīng)接收到的請求處理完成衬廷,并能成功返回,這時才真正停止應用汽绢。

這種完美的應用停止方式如何實現(xiàn)呢吗跋?就Java語言生態(tài)來說,底層的技術是支持的,所以我們才能實現(xiàn)在Java語言之上的各個web容器的優(yōu)雅停機跌宛。

在普通的外置的tomcat中酗宋,有shutdown腳本提供優(yōu)雅的停機機制,但是我們在使用Spring boot的過程中發(fā)現(xiàn)web容器都是內置(當然也可使用外置疆拘,但是不推薦)蜕猫,這種方式提供簡單的應用啟動方式,方便的管理機制哎迄,非常適用于微服務應用中回右,但是默認沒有提供優(yōu)雅停機的方式。這也是本文探索這個問題的根本原因漱挚。

應用是否是實現(xiàn)了優(yōu)雅停機翔烁,如何才能驗證呢?這需要一個處理時間較長的業(yè)務邏輯旨涝,模擬這樣的邏輯應該很簡單蹬屹,使用線程sleep或者長時間循環(huán)。我的模擬業(yè)務邏輯代碼如下:

@GetMapping(value = "/sleep/one", produces = "application/json")
public ResultEntity<Long> sleepOne(String systemNo){
    logger.info("模擬業(yè)務處理1分鐘白华,請求參數(shù):{}", systemNo);
    Long serverTime = System.currentTimeMillis();
//        try {
//            Thread.sleep(60*1000L);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
    while (System.currentTimeMillis() < serverTime + (60 * 1000)){
        logger.info("正在處理業(yè)務慨默,當前時間:{},開始時間:{}", System.currentTimeMillis(), serverTime);
    }
    ResultEntity<Long> resultEntity = new ResultEntity<>(serverTime);
    logger.info("模擬業(yè)務處理1分鐘弧腥,響應參數(shù):{}", resultEntity);
    return resultEntity;
}

驗證方式就是厦取,在觸發(fā)這個接口的業(yè)務處理之后,業(yè)務邏輯處理時間長達1分鐘管搪,需要在處理結束前蒜胖,發(fā)起停止指令,驗證是否能夠正常返回抛蚤。驗證時所使用的kill指令:kill -2(Ctrl + C)kill -15寻狂、kill -9岁经。

01 Java 語言的優(yōu)雅停機

從上面的介紹中我們發(fā)現(xiàn),Java語言本身是支持優(yōu)雅停機的蛇券,這里就先介紹一下普通的java應用是如何實現(xiàn)優(yōu)雅停止的缀壤。

當我們使用kill PID的方式結束一個Java應用的時候,JVM會收到一個停止信號纠亚,然后執(zhí)行shutdownHook的線程塘慕。一個實現(xiàn)示例如下:

public class ShutdownHook extends Thread {
    private Thread mainThread;
    private boolean shutDownSignalReceived;

    @Override
    public void run() {
        System.out.println("Shut down signal received.");
        this.shutDownSignalReceived=true;
        mainThread.interrupt();
        try {
            mainThread.join(); //當收到停止信號時,等待mainThread的執(zhí)行完成
        } catch (InterruptedException e) {
        }
        System.out.println("Shut down complete.");
    }

    public ShutdownHook(Thread mainThread) {
        super();
        this.mainThread = mainThread;
        this.shutDownSignalReceived = false;
        Runtime.getRuntime().addShutdownHook(this);
    }

    public boolean shouldShutDown(){
        return shutDownSignalReceived;
    }

}

其中關鍵語句Runtime.getRuntime().addShutdownHook(this);蒂胞,注冊一個JVM關閉的鉤子图呢,這個鉤子可以在以下幾種場景被調用:

  1. 程序正常退出
  2. 使用System.exit()
  3. 終端使用Ctrl+C觸發(fā)的中斷
  4. 系統(tǒng)關閉
  5. 使用Kill pid命令干掉進程

測試shutdownHook的功能,代碼示例:

public class TestMain {
    private ShutdownHook shutdownHook;
    public static void main( String[] args ) {
        TestMain app = new TestMain();
        System.out.println( "Hello World!" );
        app.execute();
        System.out.println( "End of main()" );
    }
    public TestMain(){
        this.shutdownHook = new ShutdownHook(Thread.currentThread());
    }
    public void execute(){
        while(!shutdownHook.shouldShutDown()){
            System.out.println("I am sleep");
            try {
                Thread.sleep(1*1000);
            } catch (InterruptedException e) {
                System.out.println("execute() interrupted");
            }
            System.out.println("I am not sleep");
        }
        System.out.println("end of execute()");
    }
}

啟動測試代碼,之后再發(fā)送一個中斷信號蛤织,控制臺輸出:

I am sleep
I am not sleep
I am sleep
I am not sleep
I am sleep
I am not sleep
I am sleep
Shut down signal received.
execute() interrupted
I am not sleep
end of execute()
End of main()
Shut down complete.

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

可以看出赴叹,在接收到中斷信號之后,整個main函數(shù)是執(zhí)行完成的指蚜。

02 actuator/shutdown of Spring boot

我們知道了java本身在支持優(yōu)雅停機上的能力乞巧,然后在Spring boot中又發(fā)現(xiàn)了actuator/shutdown的管理端點。于是我把優(yōu)雅停機的功能寄希望于此摊鸡,開始配置測試绽媒,開啟配置如下:

management:
  server:
    port: 10212
    servlet:
      context-path: /
    ssl:
      enabled: false
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
    shutdown:
      enabled: true #啟用shutdown端點

測試結果很失望,并沒有實現(xiàn)優(yōu)雅停機的功能免猾,就是將普通的kill命令是辕,做成了HTTP端點。于是開始查看Spring boot的官方文檔和源代碼掸刊,試圖找到它的原因免糕。

在官方文檔上對shutdown端點的介紹:

shutdown    Lets the application be gracefully shutdown.

從此介紹可以看出,設計上應該是支持優(yōu)雅停機的忧侧。但是為什么現(xiàn)在還不夠優(yōu)雅石窑,在github上托管的Spring boot項目中發(fā)現(xiàn),有一個issue一直處于打開狀態(tài)蚓炬,已經(jīng)兩年多了松逊,里面很多討論,看完之后發(fā)現(xiàn)在Spring boot中完美的支持優(yōu)雅停機不是一件容易的事肯夏,首先Spring boot支持web容器很多经宏,其次對什么樣的實現(xiàn)才是真正的優(yōu)雅停機,討論了很多驯击。想了解更多的同學烁兰,把這個issue好好閱讀一下。

這個issue中還有一個重要信息徊都,就是這個issue曾經(jīng)被加入到2.0.0的milestone中沪斟,后來由于沒有完成又移除了,現(xiàn)在狀態(tài)是被添加在2.1.0的milestone中暇矫。我測試的版本是2.0.1主之,期待官方給出完美的優(yōu)雅停機方案。

03 Spring boot 優(yōu)雅停機

雖然官方暫時還沒有提供優(yōu)雅停機的支持李根,但是我們?yōu)榱藴p少進程停止對業(yè)務的影響槽奕,還是要給出能滿足基本需求的方案來。

針對tomcat的解決方案是:

package com.epay.demox.unipay.provider;

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 org.springframework.stereotype.Component;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Author: guoyankui
 * @DATE: 2018/5/20 12:59 PM
 *
 * 優(yōu)雅關閉 Spring Boot tomcat
 */

@Component
public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
    private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
    private volatile Connector connector;
    private final int waitTime = 30;
    @Override
    public void customize(Connector connector) {
        this.connector = connector;
    }
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        this.connector.pause();
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if (executor instanceof ThreadPoolExecutor) {
            try {
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();
                if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}
public class UnipayProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(UnipayProviderApplication.class);
    }

    @Autowired
    private GracefulShutdownTomcat gracefulShutdownTomcat;

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addConnectorCustomizers(gracefulShutdownTomcat);
        return tomcat;
    }
}

該方案的代碼來自官方issue中的討論房轿,添加這些代碼到你的Spring boot項目中粤攒,然后再重新啟動之后所森,發(fā)起測試請求,然后發(fā)送kill停止指令(kill -2(Ctrl + C)琼讽、kill -15)必峰。測試結果:

  1. Spring boot的健康檢查,為UP钻蹬。
  2. 正在執(zhí)行操作不會終止吼蚁,直到執(zhí)行完成。
  3. 不再接收新的請求问欠,客戶端報錯信息為:Connection reset by peer肝匆。
  4. 最后正常終止進程(業(yè)務執(zhí)行完成后,立即進程停止)顺献。

從測試結果來看旗国,是滿足我們的需求的。當然如果發(fā)送指令kill -9注整,進程會立即停止能曾。

針對undertow的解決方案是:

package com.epay.demox.unipay.provider;

import io.undertow.Undertow;
import io.undertow.server.ConnectorStatistics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.List;

/**
 * @Author: guoyankui
 * @DATE: 2018/5/20 5:47 PM
 *
 * 優(yōu)雅關閉 Spring Boot undertow
 */
@Component
public class GracefulShutdownUndertow implements ApplicationListener<ContextClosedEvent> {

    @Autowired
    private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;

    @Autowired
    private ServletWebServerApplicationContext context;

    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
        gracefulShutdownUndertowWrapper.getGracefulShutdownHandler().shutdown();
        try {
            UndertowServletWebServer webServer = (UndertowServletWebServer)context.getWebServer();
            Field field = webServer.getClass().getDeclaredField("undertow");
            field.setAccessible(true);
            Undertow undertow = (Undertow) field.get(webServer);
            List<Undertow.ListenerInfo> listenerInfo = undertow.getListenerInfo();
            Undertow.ListenerInfo listener = listenerInfo.get(0);
            ConnectorStatistics connectorStatistics = listener.getConnectorStatistics();
            while (connectorStatistics.getActiveConnections() > 0){}
        }catch (Exception e){
            // Application Shutdown
        }
    }
}
package com.epay.demox.unipay.provider;

import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import org.springframework.stereotype.Component;

/**
 * @Author: guoyankui
 * @DATE: 2018/5/20 5:50 PM
 */
@Component
public class GracefulShutdownUndertowWrapper implements HandlerWrapper {
    private GracefulShutdownHandler gracefulShutdownHandler;
    @Override
    public HttpHandler wrap(HttpHandler handler) {
        if(gracefulShutdownHandler == null) {
            this.gracefulShutdownHandler = new GracefulShutdownHandler(handler);
        }
        return gracefulShutdownHandler;
    }
    public GracefulShutdownHandler getGracefulShutdownHandler() {
        return gracefulShutdownHandler;
    }
}
public class UnipayProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(UnipayProviderApplication.class);
    }
    @Autowired
    private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
    @Bean
    public UndertowServletWebServerFactory servletWebServerFactory() {
        UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
        factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.addOuterHandlerChainWrapper(gracefulShutdownUndertowWrapper));
        factory.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_STATISTICS, true));
        return factory;
    }
}

該方法參考文章,采用與tomcat同樣的測試方案肿轨,測試結果:

  1. Spring boot的健康檢查寿冕,為UP
  2. 正在執(zhí)行操作不會終止椒袍,直到執(zhí)行完成驼唱。
  3. 不再接收新的請求,客戶端報錯信息為:503 Service Unavailable驹暑。
  4. 最后正常終止進程(在業(yè)務執(zhí)行完成后的一分鐘進程停止)玫恳。

04 結束

到此為止,對Java和Spring boot應用的優(yōu)雅停機機制有了基本的認識优俘。雖然實現(xiàn)了需求京办,但是這其中還有很多知識點需要探索,比如Spring上下文監(jiān)聽器帆焕,上下文關閉事件等惭婿,還有undertow提供的GracefulShutdownHandler的原理是什么,為什么是1分鐘之后進程再停止视搏,這些問題等研究明白,再來一篇續(xù)县袱。如果又哪位同學能解答我的疑惑浑娜,請在評論區(qū)留言。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末式散,一起剝皮案震驚了整個濱河市筋遭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖漓滔,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件编饺,死亡現(xiàn)場離奇詭異,居然都是意外死亡响驴,警方通過查閱死者的電腦和手機透且,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豁鲤,“玉大人秽誊,你說我怎么就攤上這事×章猓” “怎么了锅论?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長楣号。 經(jīng)常有香客問我最易,道長,這世上最難降的妖魔是什么炫狱? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任藻懒,我火速辦了婚禮,結果婚禮上毕荐,老公的妹妹穿的比我還像新娘束析。我一直安慰自己,他們只是感情好憎亚,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布员寇。 她就那樣靜靜地躺著,像睡著了一般第美。 火紅的嫁衣襯著肌膚如雪蝶锋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天什往,我揣著相機與錄音扳缕,去河邊找鬼。 笑死别威,一個胖子當著我的面吹牛躯舔,可吹牛的內容都是我干的。 我是一名探鬼主播省古,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼粥庄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了豺妓?” 一聲冷哼從身側響起惜互,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤布讹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后训堆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體描验,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年坑鱼,在試婚紗的時候發(fā)現(xiàn)自己被綠了膘流。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡姑躲,死狀恐怖睡扬,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情黍析,我是刑警寧澤卖怜,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站阐枣,受9級特大地震影響马靠,放射性物質發(fā)生泄漏。R本人自食惡果不足惜蔼两,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一甩鳄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧额划,春花似錦妙啃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抑胎,卻和暖如春燥滑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阿逃。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工铭拧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恃锉。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓搀菩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親破托。 傳聞我的和親對象是個殘疾皇子肪跋,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內容