Spring Boot 优雅停机原理详解

 更新时间:2023年02月16日 09:23:22   作者:政采云技术团队  
这篇文章主要为大家介绍了Spring Boot 优雅停机原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

正文

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 的关闭。

以上就是Spring Boot 优雅停机原理详解的详细内容,更多关于Spring Boot 停机原理的资料请关注脚本之家其它相关文章!

相关文章

  • Java中try catch的使用和如何抛出异常问题

    Java中try catch的使用和如何抛出异常问题

    这篇文章主要介绍了Java中try catch的使用和如何抛出异常问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • 基于tomcat8 编写字符编码Filter过滤器无效问题的解决方法

    基于tomcat8 编写字符编码Filter过滤器无效问题的解决方法

    下面小编就为大家分享一篇基于tomcat8 编写字符编码Filter过滤器无效问题的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-01-01
  • SpringBoot中实现加载远程配置的代码示例

    SpringBoot中实现加载远程配置的代码示例

    本文章将通过结合consul config来讲解在springboot中如何加载远程配置:通过consul config加载consul server中存储的配置,需要的朋友可以参考下
    2023-06-06
  • java中接口(interface)及使用方法示例

    java中接口(interface)及使用方法示例

    这篇文章主要介绍了java中接口(interface)及使用方法示例,涉及接口定义的简单介绍以及Java语言代码示例,具有一定借鉴价值,需要的朋友可以参考下。
    2017-11-11
  • idea中Stash与Unstash的使用及说明

    idea中Stash与Unstash的使用及说明

    这篇文章主要介绍了idea中Stash与Unstash的使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • 鉴权认证+aop+注解+过滤feign请求的实例

    鉴权认证+aop+注解+过滤feign请求的实例

    这篇文章主要介绍了鉴权认证+aop+注解+过滤feign请求的实例讲解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • Java程序包不存在的3种解决方法总结

    Java程序包不存在的3种解决方法总结

    包存在的,但是启动项目的时候提示包不存在,所以解决下,这篇文章主要给大家介绍了关于Java程序包不存在的3种解决方法,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • 在java的Map集合中,如何更改value的值

    在java的Map集合中,如何更改value的值

    这篇文章主要介绍了在java的Map集合中,如何更改value的值问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • 详解Java 中 RMI 的使用

    详解Java 中 RMI 的使用

    这篇文章主要介绍了Java 中 RMI 的使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-05-05
  • 深入了解Java语言中的并发性选项有何不同

    深入了解Java语言中的并发性选项有何不同

    这篇文章主要介绍了深入了解Java语言中的并发性选项有何不同,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,,需要的朋友可以参考下
    2019-06-06

最新评论