掘金 后端 ( ) • 2024-04-26 16:44

SpringBoot优雅停机 actuator/shutdown

springboot 优雅停机 一般有两种方案 ,今天讲下 ``- actuator/shutdown 实现原理

  • kill -15 $pid
  • actuator/shutdown
/**
 * {@link Endpoint @Endpoint} to shutdown the {@link ApplicationContext}.
 *
 * @author Dave Syer
 * @author Christian Dupuis
 * @author Andy Wilkinson
 * @since 2.0.0
 */
@Endpoint(id = "shutdown", enableByDefault = false)
public class ShutdownEndpoint implements ApplicationContextAware {

   private static final Map<String, String> NO_CONTEXT_MESSAGE = Collections
      .unmodifiableMap(Collections.singletonMap("message", "No context to shutdown."));

   private static final Map<String, String> SHUTDOWN_MESSAGE = Collections
      .unmodifiableMap(Collections.singletonMap("message", "Shutting down, bye..."));

   private ConfigurableApplicationContext context;

   @WriteOperation
   public Map<String, String> shutdown() {
      if (this.context == null) {
         return NO_CONTEXT_MESSAGE;
      }
      try {
         return SHUTDOWN_MESSAGE;
      }
      finally {
      // 开一个异步线程 
         Thread thread = new Thread(this::performShutdown);
         thread.setContextClassLoader(getClass().getClassLoader());
         thread.start();
      }
   }

   private void performShutdown() {
      try {
      // 休眠500毫秒
         Thread.sleep(500L);
      }
      catch (InterruptedException ex) {
         Thread.currentThread().interrupt();
      }
      // 关闭spring 容器
      this.context.close();
   }

   @Override
   public void setApplicationContext(ApplicationContext context) throws BeansException {
      if (context instanceof ConfigurableApplicationContext) {
         this.context = (ConfigurableApplicationContext) context;
      }
   }

}

底层原理其实就是关闭 spring ConfigurableApplicationContext.close(); 上下文。如果我们有web 服务只这样设置其实是不行的 比如我们 http请求正在执行 此时 spring 被关闭 那么正在执行的请求可能无法完成 所以还要 设置 web优雅停机

SpringBoot优雅停机Web Server

Springboot-2.3.0开始提供了官方的优雅停机方案,当配置 server.shutdown=graceful 时,Spring Boot 应用在接收到停机信号后,将尝试完成当前正在处理的请求,同时停止接收新的请求,然后再关闭应用程序。这样做的目的是为了减少停机对用户和业务的影响。 配置spring.lifecycle.timeout-per-shutdown-phase=30s 这个配置表示在强制关闭之前,Spring Boot会等待30秒以完成当前活跃的请求。

优雅停机是微服务架构中非常重要的一个特性,因为它可以在服务需要升级、重新部署或者维护时,最小化对正在进行的业务操作的影响。通过优雅停机,可以减少服务不可用的时间,提高用户体验。那我们首先来看下需要怎么使用呢?首先需要在配置文件中配置优雅停机,如下:

server:
  shutdown: graceful   ## 开启优雅停机
spring:
  lifecycle:
    timeout-per-shutdown-phase: 5s    ## 优雅停机等待时间,默认30s

配置完成后,我们就可以启动项目来进行试验了。具体的项目代码示例我就不贴了,需要注意的是我们在停机的时候不能使用kill -9来强制关闭,这样优雅停机是不起作用的。我们可以使用actuator提供的/shutdown端口来关闭服务。此时应用日志中应该会有如下的日志:

[SpringContextShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
[tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete

如果在等待30s后还有请求没有处理完成,那么日志中也会体现出来,如下:

[SpringContextShutdownHook] o.s.c.support.DefaultLifecycleProcessor  : Failed to shut down 1 bean with phase value 2147483647 within timeout of 5000: [webServerGracefulShutdown]
[tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown aborted with one or more requests still active

源码解析

image.png 优雅停机后 执行ConfigurableApplicationContext.close() 方法

image.png org.springframework.context.support.AbstractApplicationContext#close

image.png org.springframework.context.support.DefaultLifecycleProcessor#onClose 方法 默认实现

@Override
public void onClose() {
   stopBeans();
   this.running = false;
}

核心方法

private void stopBeans() {
    // 得到 三个 Lifecycle
   // 1.  "webServerGracefulShutdown"
   //2.  "springBootLoggingLifecycle"
   //3.  "webServerStartStop"
   Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
   Map<Integer, LifecycleGroup> phases = new HashMap<>();
   lifecycleBeans.forEach((beanName, bean) -> {
      int shutdownPhase = getPhase(bean);
      LifecycleGroup group = phases.get(shutdownPhase);
      if (group == null) {
      // 构造 LifecycleGroup =3
         group = new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false);
         phases.put(shutdownPhase, group);
      }
      group.add(beanName, bean);
   });
   if (!phases.isEmpty()) {
      List<Integer> keys = new ArrayList<>(phases.keySet());
      // 排序
      keys.sort(Collections.reverseOrder());
      for (Integer key : keys) {
         phases.get(key).stop();
      }
   }
}
  • "webServerGracefulShutdown
    • 处理wbe 容器 优雅停机逻辑
    • org.springframework.boot.web.context.WebServerGracefulShutdownLifecycle
  • "springBootLoggingLifecycle"
    • 处理日志关闭
    • org.springframework.boot.context.logging.LoggingApplicationListener.Lifecycle#stop
  • "webServerStartStop" ->
    • 处理容器关闭
    • org.springframework.boot.web.embedded.tomcat.TomcatWebServer

进入 stop方法org.springframework.context.support.DefaultLifecycleProcessor.LifecycleGroup#stop

public void stop() {
   if (this.members.isEmpty()) {
      return;
   }
   if (logger.isDebugEnabled()) {
      logger.debug("Stopping beans in phase " + this.phase);
   }
   this.members.sort(Collections.reverseOrder());
   CountDownLatch latch = new CountDownLatch(this.smartMemberCount);
   Set<String> countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>());
   Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet());
   for (LifecycleGroupMember member : this.members) {
      if (lifecycleBeanNames.contains(member.name)) {
      //  核心方法
         doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames);
      }
      else if (member.bean instanceof SmartLifecycle) {
         // Already removed: must have been a dependent bean from another phase
         latch.countDown();
      }
   }
   try {
      latch.await(this.timeout, TimeUnit.MILLISECONDS);
      if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) {
         logger.info("Failed to shut down " + countDownBeanNames.size() + " bean" +
               (countDownBeanNames.size() > 1 ? "s" : "") + " with phase value " +
               this.phase + " within timeout of " + this.timeout + "ms: " + countDownBeanNames);
      }
   }
   catch (InterruptedException ex) {
      Thread.currentThread().interrupt();
   }
}

image.png

这个bean org.springframework.boot.web.context.WebServerGracefulShutdownLifecycle#stop(java.lang.Runnable)

image.png org.springframework.boot.web.embedded.tomcat.TomcatWebServer#shutDownGracefully 这里的容器我用的 tomcat org.springframework.boot.web.embedded.tomcat.GracefulShutdown#shutDownGracefully

private void doShutdown(GracefulShutdownCallback callback) {
   List<Connector> connectors = getConnectors();
   // 获取所有连接器给关闭
   connectors.forEach(this::close);
   try {
     // 遍历Tomcat引擎中所有的Host(虚拟主机)
      for (Container host : this.tomcat.getEngine().findChildren()) {
      // 遍历每个Host中的所有Context(Web应用程序)
         for (Container context : host.findChildren()) {
         // 检查当前Context是否仍然有活跃的请求
            while (isActive(context)) {
            // 如果在关闭过程中决定中断优雅关闭
               if (this.aborted) {
                  logger.info("Graceful shutdown aborted with one or more requests still active");
                  // 调用回调函数,通知调用者优雅关闭未能完成,因为还有活跃的请求
                  callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
                  return;
               }
               // 如果还有活跃的请求,线程等待50毫秒后再次检查
               Thread.sleep(50);
            }
         }
      }

   }
   catch (InterruptedException ex) {
      Thread.currentThread().interrupt();
   }
   logger.info("Graceful shutdown complete");
   callback.shutdownComplete(GracefulShutdownResult.IDLE);
}

优雅停机关闭后 才会处理关闭 bean beanFactory