世界上最快的捷徑,就是腳踏實地java
架構技術專欄:http://www.jiagoujishu.comgit
開源項目:web
-
分佈式監控(Gitee GVP最有價值開源項目 ):https://gitee.com/sanjiankethree/cubicspring
-
攝像頭視頻流採集:https://gitee.com/sanjiankethree/cubic-videoc#
優雅停機
目前Spring Boot已經發展到了2.3.4.RELEASE,伴隨着2.3版本的到來,優雅停機機制也更加完善了。tomcat
目前版本的Spring Boot 優雅停機支持Jetty, Reactor Netty, Tomcat和 Undertow 以及反應式和基於 Servlet 的 web 應用程序都支持優雅停機功能。springboot
優雅停機的目的:服務器
若是沒有優雅停機,服務器此時直接直接關閉(kill -9),那麼就會致使當前正在容器內運行的業務直接失敗,在某些特殊的場景下產生髒數據。微信
增長了優雅停機配置後:多線程
在服務器執行關閉(kill -2)時,會預留一點時間使容器內部業務線程執行完畢,此時容器也不容許新的請求進入。新請求的處理方式跟web服務器有關,Reactor Netty、 Tomcat將中止接入請求,Undertow的處理方式是返回503.
新版配置
YAML配置
新版本配置很是簡單,server.shutdown=graceful 就搞定了(注意,優雅停機配置須要配合Tomcat 9.0.33(含)以上版本)
server:
port: 6080
shutdown: graceful #開啓優雅停機
spring:
lifecycle:
timeout-per-shutdown-phase: 20s #設置緩衝時間 默認30s
在設置了緩衝參數timeout-per-shutdown-phase 後,在規定時間內若是線程沒法執行完畢則會被強制停機。
下面咱們來看下停機時,加了優雅停日誌和不加的區別:
//未加優雅停機配置
Disconnected from the target VM, address: '127.0.0.1:49754', transport: 'socket'
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
加了優雅停機配置後,可明顯發現的日誌 Waiting for active requests to cpmplete,此時容器將在ShutdownHook執行完畢後中止。
![優雅停機 -2](www.jiagoujishu.com/loads/公衆號圖片/springboot/優雅停機 -2.jpg)
關閉方式
一、 必定不要使用kill -9 操做,使用kill -2 來關閉容器。這樣纔會觸發java內部ShutdownHook操做,kill -9不會觸發ShutdownHook。
二、能夠使用端點監控 POST 請求 /actuator/shutdown 來執行優雅關機。
添加ShutdownHook
經過上面的日誌咱們發現Druid執行了本身的ShutdownHook,那麼咱們也來添加下ShutdownHook,有幾種簡單的方式:
一、實現DisposableBean接口,實現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("準備優雅中止應用使用 DisposableBean");
executorService.shutdown();
}
}
二、使用@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("準備優雅中止應用 @PreDestroy");
executorService.shutdown();
}
}
這裏注意,@PreDestroy 比 DisposableBean 先執行
關閉原理
一、使用kill pid關閉,源碼很簡單,你們能夠看下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);
}
二、使用端點監控 POST 請求 /actuator/shutdown關閉
由於actuator 都使用了SPI的擴展方式,因此咱們看下AutoConfiguration,能夠看到關鍵點就是ShutdownEndpoint
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnAvailableEndpoint(
endpoint = ShutdownEndpoint.class
)
public class ShutdownEndpointAutoConfiguration {
public ShutdownEndpointAutoConfiguration() {
}
@Bean(
destroyMethod = ""
)
@ConditionalOnMissingBean
public ShutdownEndpoint shutdownEndpoint() {
return new ShutdownEndpoint();
}
}
ShutdownEndpoint,爲了節省篇幅只留了一點重要的
@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(); //這裏纔是核心
}
}
在調用了 this.context.close() ,其實就是AbstractApplicationContext 的close() 方法 (重點是其中的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(); //重點:銷燬bean 並執行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
}
}
}
}
後記
到這裏,關於單機
版本的Spring Boot優雅停機就說完了。爲何說單機
?由於你們也能發現,在關閉時,其實只是保證了服務端內部線程執行完畢,調用方的狀態是沒關注的。
不管是Dubbo仍是Cloud 的分佈式服務框架,須要關注的是怎麼能在服務中止前,先將提供者在註冊中心進行反註冊,而後在中止服務提供者,這樣才能保證業務系統不會產生各類50三、timeout等現象。
好在當前Spring Boot 結合Kubernetes已經幫咱們搞定了這一點,也就是Spring Boot 2.3版本新功能Liveness(存活狀態) 和Readiness(就緒狀態)
簡單的提下這兩個狀態:
-
Liveness(存活狀態):Liveness 狀態來查看內部狀況能夠理解爲health check,若是Liveness失敗就就意味着應用處於故障狀態而且目前沒法恢復,這種狀況就重啓吧。此時Kubernetes若是存活探測失敗將殺死Container。 -
Readiness(就緒狀態):用來告訴應用是否已經準備好接受客戶端請求,若是Readiness未就緒那麼k8s就不能路由流量過來。
往期推薦
本文分享自微信公衆號 - 架構技術專欄(jiagoujishu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。