愛生活,愛編碼,本文已收錄架構技術專欄關注這個喜歡分享的地方。
開源專案:
-
分散式監控(Gitee GVP最有價值開源專案 ):https://gitee.com/sanjiankethree/cubic
優雅停機
目前Spring Boot已經發展到了2.3.4.RELEASE,伴隨著2.3版本的到來,優雅停機機制也更加完善了。
目前版本的Spring Boot 優雅停機支援Jetty, Reactor Netty, Tomcat和 Undertow 以及反應式和基於 Servlet 的 web 應用程式都支援優雅停機功能。
優雅停機的目的:
如果沒有優雅停機,伺服器此時直接直接關閉(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執行完畢後停止。
關閉方式
1、 一定不要使用kill -9 操作,使用kill -2 來關閉容器。這樣才會觸發java內部ShutdownHook操作,kill -9不會觸發ShutdownHook。
2、可以使用端點監控 POST 請求 /actuator/shutdown 來執行優雅關機。
新增ShutdownHook
通過上面的日誌我們發現Druid執行了自己的ShutdownHook,那麼我們也來新增下ShutdownHook,有幾種簡單的方式:
1、實現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();
}
}
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("準備優雅停止應用 @PreDestroy");
executorService.shutdown();
}
}
這裡注意,@PreDestroy 比 DisposableBean 先執行
關閉原理
1、使用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);
}
2、使用端點監控 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 的分散式服務框架,需要關注的是怎麼能在服務停止前,先將提供者在註冊中心進行反註冊,然後在停止服務提供者,這樣才能保證業務系統不會產生各種503、timeout等現象。
好在當前Spring Boot 結合Kubernetes已經幫我們搞定了這一點,也就是Spring Boot 2.3版本新功能Liveness(存活狀態) 和Readiness(就緒狀態)
簡單的提下這兩個狀態:
- Liveness(存活狀態):Liveness 狀態來檢視內部情況可以理解為health check,如果Liveness失敗就就意味著應用處於故障狀態並且目前無法恢復,這種情況就重啟吧。此時Kubernetes如果存活探測失敗將殺死Container。
- Readiness(就緒狀態):用來告訴應用是否已經準備好接受客戶端請求,如果Readiness未就緒那麼k8s就不能路由流量過來。