詳解Spring Boot最新版優(yōu)雅停機(jī)的方法
什么是優(yōu)雅停機(jī)
先來一段簡單的代碼,如下:
@RestController
public class DemoController {
@GetMapping("/demo")
public String demo() throws InterruptedException {
// 模擬業(yè)務(wù)耗時(shí)處理流程
Thread.sleep(20 * 1000L);
return "hello";
}
}
當(dāng)我們流量請求到此接口執(zhí)行業(yè)務(wù)邏輯的時(shí)候,若服務(wù)端此時(shí)執(zhí)行關(guān)機(jī) (kill),spring boot 默認(rèn)情況會(huì)直接關(guān)閉容器(tomcat 等),導(dǎo)致此業(yè)務(wù)邏輯執(zhí)行失敗。在一些業(yè)務(wù)場景下:會(huì)出現(xiàn)數(shù)據(jù)不一致的情況,事務(wù)邏輯不會(huì)回滾。

開源項(xiàng)目:
分布式監(jiān)控(Gitee GVP最有價(jià)值開源項(xiàng)目 ):https://gitee.com/sanjiankethree/cubic
攝像頭視頻流采集:https://gitee.com/sanjiankethree/cubic-video
優(yōu)雅停機(jī)
目前Spring Boot已經(jīng)發(fā)展到了2.3.4.RELEASE,伴隨著2.3版本的到來,優(yōu)雅停機(jī)機(jī)制也更加完善了。
目前版本的Spring Boot 優(yōu)雅停機(jī)支持Jetty, Reactor Netty, Tomcat和 Undertow 以及反應(yīng)式和基于 Servlet 的 web 應(yīng)用程序都支持優(yōu)雅停機(jī)功能。
優(yōu)雅停機(jī)的目的:
如果沒有優(yōu)雅停機(jī),服務(wù)器此時(shí)直接直接關(guān)閉(kill -9),那么就會(huì)導(dǎo)致當(dāng)前正在容器內(nèi)運(yùn)行的業(yè)務(wù)直接失敗,在某些特殊的場景下產(chǎn)生臟數(shù)據(jù)。
增加了優(yōu)雅停機(jī)配置后:
在服務(wù)器執(zhí)行關(guān)閉(kill -2)時(shí),會(huì)預(yù)留一點(diǎn)時(shí)間使容器內(nèi)部業(yè)務(wù)線程執(zhí)行完畢,此時(shí)容器也不允許新的請求進(jìn)入。新請求的處理方式跟web服務(wù)器有關(guān),Reactor Netty、 Tomcat將停止接入請求,Undertow的處理方式是返回503.
新版配置
YAML配置
新版本配置非常簡單,server.shutdown=graceful 就搞定了(注意,優(yōu)雅停機(jī)配置需要配合Tomcat 9.0.33(含)以上版本)
server: port: 6080 shutdown: graceful #開啟優(yōu)雅停機(jī) spring: lifecycle: timeout-per-shutdown-phase: 20s #設(shè)置緩沖時(shí)間 默認(rèn)30s
在設(shè)置了緩沖參數(shù)timeout-per-shutdown-phase 后,在規(guī)定時(shí)間內(nèi)如果線程無法執(zhí)行完畢則會(huì)被強(qiáng)制停機(jī)。
下面我們來看下停機(jī)時(shí),加了優(yōu)雅停日志和不加的區(qū)別:
//未加優(yōu)雅停機(jī)配置 Disconnected from the target VM, address: '127.0.0.1:49754', transport: 'socket' Process finished with exit code 130 (interrupted by signal 2: SIGINT)
加了優(yōu)雅停機(jī)配置后,可明顯發(fā)現(xiàn)的日志 Waiting for active requests to cpmplete,此時(shí)容器將在ShutdownHook執(zhí)行完畢后停止。

關(guān)閉方式
1、 一定不要使用kill -9 操作,使用kill -2 來關(guān)閉容器。這樣才會(huì)觸發(fā)java內(nèi)部ShutdownHook操作,kill -9不會(huì)觸發(fā)ShutdownHook。
2、可以使用端點(diǎn)監(jiān)控 POST 請求 /actuator/shutdown 來執(zhí)行優(yōu)雅關(guān)機(jī)。
添加ShutdownHook
通過上面的日志我們發(fā)現(xiàn)Druid執(zhí)行了自己的ShutdownHook,那么我們也來添加下ShutdownHook,有幾種簡單的方式:
1、實(shí)現(xiàn)DisposableBean接口,實(shí)現(xiàn)destroy方法
@Slf4j
@Service
public class DefaultDataStore implements DisposableBean {
private final ExecutorService executorService = new ThreadPoolExecutor(OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo"));
@Override
public void destroy() throws Exception {
log.info("準(zhǔn)備優(yōu)雅停止應(yīng)用使用 DisposableBean");
executorService.shutdown();
}
}
2、使用@PreDestroy注解
@Slf4j
@Service
public class DefaultDataStore {
private final ExecutorService executorService = new ThreadPoolExecutor(OSUtil.getAvailableProcessors(), OSUtil.getAvailableProcessors() + 1, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(200), new DefaultThreadFactory("UploadVideo"));
@PreDestroy
public void shutdown() {
log.info("準(zhǔn)備優(yōu)雅停止應(yīng)用 @PreDestroy");
executorService.shutdown();
}
}
這里注意,@PreDestroy 比 DisposableBean 先執(zhí)行
關(guān)閉原理
1、使用kill pid關(guān)閉,源碼很簡單,大家可以看下GracefulShutdown
private void doShutdown(GracefulShutdownCallback callback) {
List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
while (isActive(context)) {
if (this.aborted) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
Thread.sleep(50);
}
}
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
2、使用端點(diǎn)監(jiān)控 POST 請求 /actuator/shutdown關(guān)閉
因?yàn)閍ctuator 都使用了SPI的擴(kuò)展方式,所以我們看下AutoConfiguration,可以看到關(guān)鍵點(diǎn)就是ShutdownEndpoint
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnAvailableEndpoint(
endpoint = ShutdownEndpoint.class
)
public class ShutdownEndpointAutoConfiguration {
public ShutdownEndpointAutoConfiguration() {
}
@Bean(
destroyMethod = ""
)
@ConditionalOnMissingBean
public ShutdownEndpoint shutdownEndpoint() {
return new ShutdownEndpoint();
}
}
ShutdownEndpoint,為了節(jié)省篇幅只留了一點(diǎn)重要的
@Endpoint(
id = "shutdown",
enableByDefault = false
)
public class ShutdownEndpoint implements ApplicationContextAware {
@WriteOperation
public Map<String, String> shutdown() {
if (this.context == null) {
return NO_CONTEXT_MESSAGE;
} else {
boolean var6 = false;
Map var1;
try {
var6 = true;
var1 = SHUTDOWN_MESSAGE;
var6 = false;
} finally {
if (var6) {
Thread thread = new Thread(this::performShutdown);
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
}
}
Thread thread = new Thread(this::performShutdown);
thread.setContextClassLoader(this.getClass().getClassLoader());
thread.start();
return var1;
}
}
private void performShutdown() {
try {
Thread.sleep(500L);
} catch (InterruptedException var2) {
Thread.currentThread().interrupt();
}
this.context.close(); //這里才是核心
}
}
在調(diào)用了 this.context.close() ,其實(shí)就是AbstractApplicationContext 的close() 方法 (重點(diǎn)是其中的doClose())
/**
* Close this application context, destroying all beans in its bean factory.
* <p>Delegates to {@code doClose()} for the actual closing procedure.
* Also removes a JVM shutdown hook, if registered, as it's not needed anymore.
* @see #doClose()
* @see #registerShutdownHook()
*/
@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose(); //重點(diǎn):銷毀bean 并執(zhí)行jvm shutdown hook
// If we registered a JVM shutdown hook, we don't need it anymore now:
// We've already explicitly closed the context.
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
catch (IllegalStateException ex) {
// ignore - VM is already shutting down
}
}
}
}
后記
到這里,關(guān)于單機(jī)版本的Spring Boot優(yōu)雅停機(jī)就說完了。為什么說單機(jī)?因?yàn)榇蠹乙材馨l(fā)現(xiàn),在關(guān)閉時(shí),其實(shí)只是保證了服務(wù)端內(nèi)部線程執(zhí)行完畢,調(diào)用方的狀態(tài)是沒關(guān)注的。
不論是Dubbo還是Cloud 的分布式服務(wù)框架,需要關(guān)注的是怎么能在服務(wù)停止前,先將提供者在注冊中心進(jìn)行反注冊,然后在停止服務(wù)提供者,這樣才能保證業(yè)務(wù)系統(tǒng)不會(huì)產(chǎn)生各種503、timeout等現(xiàn)象。
好在當(dāng)前Spring Boot 結(jié)合Kubernetes已經(jīng)幫我們搞定了這一點(diǎn),也就是Spring Boot 2.3版本新功能Liveness(存活狀態(tài)) 和Readiness(就緒狀態(tài))
簡單的提下這兩個(gè)狀態(tài):
- Liveness(存活狀態(tài)):Liveness 狀態(tài)來查看內(nèi)部情況可以理解為health check,如果Liveness失敗就就意味著應(yīng)用處于故障狀態(tài)并且目前無法恢復(fù),這種情況就重啟吧。此時(shí)Kubernetes如果存活探測失敗將殺死Container。
- Readiness(就緒狀態(tài)):用來告訴應(yīng)用是否已經(jīng)準(zhǔn)備好接受客戶端請求,如果Readiness未就緒那么k8s就不能路由流量過來。
到此這篇關(guān)于Spring Boot最新版優(yōu)雅停機(jī)的文章就介紹到這了,更多相關(guān)Spring Boot優(yōu)雅停機(jī)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringMVC xml文件路徑在web.xml中的配置方式
這篇文章主要介紹了SpringMVC xml文件路徑在web.xml中的配置方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
java中創(chuàng)建寫入文件的6種方式詳解與源碼實(shí)例
這篇文章主要介紹了java中創(chuàng)建寫入文件的6種方式詳解與源碼實(shí)例,Files.newBufferedWriter(Java 8),Files.write(Java 7 推薦),PrintWriter,File.createNewFile,FileOutputStream.write(byte[] b) 管道流,需要的朋友可以參考下2022-12-12
Java微信公眾平臺(tái)開發(fā)(5) 文本及圖文消息回復(fù)的實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了Java微信公眾平臺(tái)開發(fā)第五步,回文本及圖文消息回復(fù)的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
Java基礎(chǔ)篇_有關(guān)接口和抽象類的幾道練習(xí)題(分享)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)篇_有關(guān)接口和抽象類的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06

