Spring Boot 系列:最新版優雅停機詳解

世界上最快的捷徑,就是腳踏實地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() + 11, 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() + 11, 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就不能路由流量過來。

往期推薦



看懂這篇,才能說了解併發底層技術

垃圾回收器爲何必需要停頓下?

(最新 9000字)  Spring Boot 配置特性解析

Spring Boot 知識清單(一)SpringApplication

什麼時候用多線程?多線程須要加鎖嗎?線程數多少最合理?



本文分享自微信公衆號 - 架構技術專欄(jiagoujishu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索