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關閉的鉤子图呢,這個鉤子可以在以下幾種場景被調用:
- 程序正常退出
- 使用System.exit()
- 終端使用Ctrl+C觸發(fā)的中斷
- 系統(tǒng)關閉
- 使用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
)必峰。測試結果:
- Spring boot的健康檢查,為
UP
钻蹬。 - 正在執(zhí)行操作不會終止吼蚁,直到執(zhí)行完成。
- 不再接收新的請求问欠,客戶端報錯信息為:
Connection reset by peer
肝匆。 - 最后正常終止進程(業(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同樣的測試方案肿轨,測試結果:
- Spring boot的健康檢查寿冕,為
UP
。 - 正在執(zhí)行操作不會終止椒袍,直到執(zhí)行完成驼唱。
- 不再接收新的請求,客戶端報錯信息為:
503 Service Unavailable
驹暑。 - 最后正常終止進程(在業(yè)務執(zhí)行完成后的一分鐘進程停止)玫恳。
04 結束
到此為止,對Java和Spring boot應用的優(yōu)雅停機機制有了基本的認識优俘。雖然實現(xiàn)了需求京办,但是這其中還有很多知識點需要探索,比如Spring上下文監(jiān)聽器帆焕,上下文關閉事件等惭婿,還有undertow提供的GracefulShutdownHandler
的原理是什么,為什么是1分鐘之后進程再停止视搏,這些問題等研究明白,再來一篇續(xù)县袱。如果又哪位同學能解答我的疑惑浑娜,請在評論區(qū)留言。