spring 优雅停机

为什么 spring 要做优雅停机

我们现在的服务一般都是在 spring 容器运行,如果不做优雅停机,会有以下问题

1、程序中的任务运行到一半,被强行结束,影响到正常业务

2、出现 spring 容器已经关闭,但任务仍在运行的情况,这个时候用到 spring 的部分就会报错

所以理想状态下,停机的时候,先停止我们自己的任务,然后再关闭 spring 的容器

spring 怎么做优雅停机

在用  kill  pid 进行停机时,会触发 jvm 钩子函数,spring 很好的利用了这个特性,来看下源码

注册一个钩子函数

// 注册一个钩子,org.springframework.context.support.AbstractApplicationContext#registerShutdownHook

public void registerShutdownHook() {

   if (this.shutdownHook == null) {

      // No shutdown hook registered yet.

      this.shutdownHook = new Thread() {

         @Override

         public void run() {

            synchronized (startupShutdownMonitor) {

               doClose();

            }

         }

      };

      Runtime.getRuntime().addShutdownHook(this.shutdownHook);

   }

}

在 doClose 方法里面,会先发布 ContextClosedEvent 事件,然后关闭 spring 容器

// org.springframework.context.support.AbstractApplicationContext#doClose

protected void doClose() {

   // Check whether an actual close attempt is necessary...

   if (this.active.get() && this.closed.compareAndSet(falsetrue)) {

      // 省略相关代码

      try {

         // Publish shutdown event.

         // spring 的事件默认是同步执行

         publishEvent(new ContextClosedEvent(this));

      }

      catch (Throwable ex) {

         logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);

      }

      // 省略关闭 spring 容器相关代码 。。。

   }

}

因此,我们可以利用 ContextClosedEvent 事件,对应用的一些任务做优雅关停

@Component

public class SpringContextClosedListener implements ApplicationListener<ContextClosedEvent> {

  @Override

  public void onApplicationEvent(ContextClosedEvent event) {

    // 关闭应用的任务,比如关闭 kafka 消费,关闭定时任务等等

  }

}

spring-boot 优雅停机

spring boot 的 actuator 组件提供了shutdown 端点,可以让我们通过调接口的方式提前对容器进行关闭,而不必等到 jvm 关闭的钩子函数触发时再关闭

直接通过 http 调用 curl localhost:8080/actuator/shutdown  即可

我们来看下源码

// org.springframework.boot.actuate.context.ShutdownEndpoint

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 {

      Thread.sleep(500L);

   }

   catch (InterruptedException ex) {

      Thread.currentThread().interrupt();

   }

   // 主动调用 ApplicationContext 的关闭方法

   this.context.close();

}


主动调用 ApplicationContext 的关闭方法, 如果之前注册过关闭事件的钩子函数,会取消掉

// org.springframework.context.support.AbstractApplicationContext#close

public void close() {

   synchronized (this.startupShutdownMonitor) {

      doClose();

      // 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 钩子函数执行的方法一样,因此,我们依然可以用 ContextClosedEvent 监听到,然后自定义自己的逻辑

我可以直接用 Runtime.getRuntime().addShutdownHook 自定义自己的关停逻辑吗

可以,但不建议,因为 jvm 的钩子函数是 并发+ 无序 执行的,你保证不了你的钩子函数和 spring 钩子函数的顺序

除非你的任务逻辑不依赖任何上下文,也不依赖 spring ,否则还是建议放到 spring 的 ContextClosedEvent 事件

多个 ContextClosedEvent 事件的顺序问题

如果有多个 ContextClosedEvent 事件,并且事件有相互依赖关系,请使用 spring 的 order 指定顺序

之前的血泪教训 20200928-上线后cdimond动态配置不生效bug

ContextClosedEvent 事件为什么不生效

1、首先 springboot 的话都会自己注册 spring 关闭事件钩子,所以都是生效的

2、但非 springboot 并且非 web 项目默认没有注册 spring 的关闭钩子,因此需要自己注册下,否则 ContextClosedEvent 事件不会生效

Core Technologies

spring 优雅停机

 

上一篇:源码解读Spring如何解决循环依赖


下一篇:spring bean的生命周期和作用范围