Spring Boot 优雅停机原理详解

Java
917
0
0
2023-03-23
标签   SpringBoot
目录
  • 正文
  • 如何开启优雅停机
  • 优雅停机原理
  • shutdown hook
  • SpringBoot注册 Shutdown Hook
  • 注册实现smartLifecycle的Bean
  • smartLifecycle的工作原理
  • 优雅停机超时时间如何控制
  • 优雅停机的执行流程总结:

正文


SpringBoot 从2.3.0.RELEASE 开始支持 web 服务器的优雅停机

看看官方文档是怎么介绍这一新特性的

“ Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. It occurs as part of closing the application context and is performed in the earliest phase of stopping SmartLifecycle beans. This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted. The exact way in which new requests are not permitted varies depending on the web server that is being used. Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer. Undertow will accept requests but respond immediately with a service unavailable (503) response."

四种内嵌 web 服务器(Jetty、Reactor Netty、Tomcat 和 Undertow)以及 reactive 和基于 servlet 的 web 应用程序都支持优雅停机,它作为关闭应用程序上下文的一部分发生,并且是SmartLifecyclebean里最早进行关闭的。此停止处理会有个超时机制,该超时提供了一个宽限期,在此期间允许完成现有请求,但不允许新请求。具体实现取决于所使用的web服务器。Jetty、Reactor Netty 和 Tomcat 将停止接受网络层的请求。Undertow 将接受请求,但立即响应服务不可用(503)。

如何开启优雅停机

server:
  # 设置关闭方式为优雅关闭
  shutdown: graceful
spring:
  lifecycle:
    # 优雅关闭超时时间, 默认30s
    timeout-per-shutdown-phase: 30s

优雅停机原理

shutdown hook

在 Java 程序中可以通过添加钩子,在程序退出时会执行钩子方法,从而实现关闭资源、平滑退出等功能。

public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行 ShutdownHook ...");
            }
        }));
    }

覆盖以下场景:

  • 代码主动关闭:如System.exit()
  • 捕获kill信号: kill -1(HUP), kill - 2(INT), kill -15(TERM)
  • 用户线程结束: 会在最后一个非守护线程结束时被 JNI 的DestroyJavaVM方法调用

说明: kill -9 会直接杀死进程不会触发 shutdownhook 方法执行,shutdownhook 回调方法会启动新线程,注册多个钩子会并发执行。

SpringBoot注册 Shutdown Hook

SpringBoot 在启动过程中,则会默认注册一个 Shutdown Hook,在应用被关闭的时候,会触发钩子调用 doClose()方法,去关闭容器。(也可以通过 actuate 来优雅关闭应用,不在本文讨论范围)

org.springframework.boot.SpringApplication#refreshContext

private void refreshContext(ConfigurableApplicationContext context) {
   // 默认为true
   if (this.registerShutdownHook) {
      try {
         context.registerShutdownHook();
      }
      catch (AccessControlException ex) {
         // Not allowed in some environments.
      }
   }
   refresh((ApplicationContext) context);
}

org.springframework.context.support.AbstractApplicationContext

@Override
public void registerShutdownHook() {
   if (this.shutdownHook == null) {
      // No shutdown hook registered yet.
      this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
         @Override
         public void run() {
            synchronized (startupShutdownMonitor) {
               // 回调去关闭容器
               doClose();
            }
         }
      };
      // 注册钩子
      Runtime.getRuntime().addShutdownHook(this.shutdownHook);
   }
}

注册实现smartLifecycle的Bean

在创建 webserver 的时候,会创建一个实现smartLifecycle的 bean,用来支撑 server 的优雅关闭。

org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext

private void createWebServer() {
      // 省略其他无关代码
      this.webServer = factory.getWebServer(getSelfInitializer());
      // 注册webServerGracefulShutdown用来实现server优雅关闭
      getBeanFactory().registerSingleton("webServerGracefulShutdown",new WebServerGracefulShutdownLifecycle(this.webServer));
      // 省略其他无关代码
}

可以看到 WebServerGracefulShutdownLifecycle 类实现SmartLifecycle接口,重写了 stop 方法,stop 方法会触发 webserver 的优雅关闭方法(取决于具体使用的 webserver 如 tomcatWebServer)。

org.springframework.boot.web.servlet.context.WebServerGracefulShutdownLifecycle

class WebServerGracefulShutdownLifecycle implements SmartLifecycle {
   @Override
   public void stop(Runnable callback) {
      this.running = false;
      // 优雅关闭server
      this.webServer.shutDownGracefully((result) -> callback.run());
   }
}

org.springframework.boot.web.embedded.tomcat.TomcatWebServer

public class TomcatWebServer implements WebServer {
   public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
      this.tomcat = tomcat;
      this.autoStart = autoStart;
      // 如果SpringBoot开启了优雅停机配置,shutdown = Shutdown.GRACEFUL
      this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
      initialize();
   }
   @Override
   public void shutDownGracefully(GracefulShutdownCallback callback) {
      if (this.gracefulShutdown == null) {
         // 如果没有开启优雅停机,会立即关闭tomcat服务器
         callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
         return;
      }
      // 优雅关闭服务器
      this.gracefulShutdown.shutDownGracefully(callback);
   }
}

smartLifecycle的工作原理

上文提到钩子方法被调用后会执行 doColse()方法,在关闭容器之前,会通过 lifecycleProcessor 调用 lifecycle 的方法。

org.springframework.context.support.AbstractApplicationContext

protected void doClose() {
   if (this.active.get() && this.closed.compareAndSet(false, true)) {
      LiveBeansView.unregisterApplicationContext(this);
      // 发布 ContextClosedEvent 事件
      publishEvent(new ContextClosedEvent(this));
      // 回调所有实现Lifecycle 接口的Bean的stop方法
      if (this.lifecycleProcessor != null) {
            this.lifecycleProcessor.onClose();
      }
      // 销毁bean, 关闭容器
      destroyBeans();
      closeBeanFactory();
      onClose();
      if (this.earlyApplicationListeners != null) {
         this.applicationListeners.clear();
         this.applicationListeners.addAll(this.earlyApplicationListeners);
      }
      // Switch to inactive.
      this.active.set(false);
   }
}

关闭 Lifecycle Bean 的入口: org.springframework.context.support.DefaultLifecycleProcessor

public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
   @Override
   public void onClose() {
      stopBeans();
      this.running = false;
   }
   private void stopBeans() {
      //获取所有的 Lifecycle bean
      Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
      //按Phase值对bean分组, 如果没有实现 Phased 接口则认为 Phase 是 0
      Map<Integer, LifecycleGroup> phases = new HashMap<>();
      lifecycleBeans.forEach((beanName, bean) -> {
         int shutdownPhase = getPhase(bean);
         LifecycleGroup group = phases.get(shutdownPhase);
         if (group == null) {
            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());
         //按照 Phase 值倒序
         keys.sort(Collections.reverseOrder());
         // Phase值越大优先级越高,先执行
         for (Integer key : keys) {
            phases.get(key).stop();
         }
      }
   }

DefaultLifecycleProcessor 的 stop 方法执行流程:

  • 获取容器中的所有实现了 Lifecycle 接口的 Bean。(smartLifecycle 接口继承了 Lifecycle)
  • 再对包含所有 bean 的 List 分组按 phase 值倒序排序,值大的排前面。 (没有实现 Phased 接口, Phase 默认为0)
  • 依次调用各分组的里 bean 的 stop 方法 ( Phase 越大 stop 方法优先执行)

优雅停机超时时间如何控制

从上文我们已经可以梳理出,优雅停机的执行流程,下面可以看下停机超时时间是如何控制的。

org.springframework.context.support.DefaultLifecycleProcessor

// DefaultLifecycleProcessor内部类
private class LifecycleGroup {
   public void stop() {
      this.members.sort(Collections.reverseOrder());
      // count值默认为该组smartLifeCycel bean的数量
      CountDownLatch latch = new CountDownLatch(this.smartMemberCount);
      // 用于日志打印,打印等待超时未关闭成功的beanName
      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)) {
            // bean如果还没关闭,执行关闭方法
            doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames);
         }
         else if (member.bean instanceof SmartLifecycle) {
            // 如果是SmartLifecycle bean 并且已经被提前处理了(依赖其他更优先关闭的bean,会提前关闭)
            latch.countDown();
         }
      }
      try {
         // 等待该组 所有smartLifeCycel bean成功关闭 或者 超时
         // 等待时间默认30s, 如果没有配置timeout-per-shutdown-phase
         latch.await(this.timeout, TimeUnit.MILLISECONDS);
      }
      catch (InterruptedException ex) {
         Thread.currentThread().interrupt();
      }
   }
}
private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
      final CountDownLatch latch, final Set<String> countDownBeanNames) {
   // 从未关闭的bean List中移除
   Lifecycle bean = lifecycleBeans.remove(beanName);
   if (bean != null) {
      String[] dependentBeans = getBeanFactory().getDependentBeans(beanName);
      // 如果该bean被其他bean依赖,优先关闭那些bean
      for (String dependentBean : dependentBeans) {
         doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames);
      }
      // Lifecycel#isRunning 需要为true才会执行stop方法
      if (bean.isRunning()) {
           if (bean instanceof SmartLifecycle) {
              // 关闭之前先记录,如果超时没关闭成功 用于打印日志提醒
              countDownBeanNames.add(beanName);
              ((SmartLifecycle) bean).stop(() -> {
                 // 执行成功countDown
                 latch.countDown();
                 // 关闭成功移除
                 countDownBeanNames.remove(beanName);
              });
           }
           else {
               // 普通Lifecycle bean直接调用stop方法
               bean.stop();
           }
       }
       else if (bean instanceof SmartLifecycle) {
            // 如何SmartLifecycle不需要关闭,直接countDown
           latch.countDown();
       }
   }
}
  • DefaultLifecycleProcessor 利用 CountDownLatch 来控制等待bean的关闭方法执行完毕,count=本组 SmartLifecycle bean 的数量,只有所有 SmartLifecycle 都执行完,回调执行 latch.countDown(),主线程才会结束等待,否则直到超时。
  • timeout-per-shutdown-phase: 30s, 该配置是针对每一组 Lifecycle bean 分别生效,不是所有的 Lifecycle bean,比如有2组不同puase 值的 bean, 会分别有最长 30s 等待时间。
  • 超时等待只对异步执行 SmartLifecycle #stop(Runnable callback) 方法有效果,同步执行没有效果。
  • 如果不同组的 Lifecycle bean 之间有依赖关系,当前组 bean 被其他组的 bean 依赖,其他组的 bean 会先进行关闭(也会调用本轮生成 latch 对象的 countDown()),导致本轮的 latch.countDown()调用次数会超过初始化的 count 值,导致提前结束等待的情况发生。

优雅停机的执行流程总结:

  • SpringBoot 通过 Shutdown Hook 来注册 doclose() 回调方法,在应用关闭的时候触发执行。
  • SpringBoot 在创建 webserver的时候,会注册实现 smartLifecycel 接口的 bean,用来优雅关闭 tomcat
  • doClose()在销毁 bean, 关闭容器之前会执行所有实现 Lifecycel 接口 bean 的 stop方法,并且会按 Phase 值分组, phase 大的优先执行。
  • WebServerGracefulShutdownLifecycle,Phase=Inter.MAX_VALUE,处于最优先执行序列,所以 tomcat 会先触发优雅关闭,并且tomcat 关闭方法是异步执行的,主线会继续调用执行本组其他 bean 的关闭方法,然后等待所有 bean 关闭完毕,超过等待时间,会执行下一组 Lifecycle bean 的关闭。