Spring Boot实现优雅关闭

有时候我们需要关闭或重启Java程序,但是可能某个业务流程还未执行结束或占用的资源还未释放,如果使用kill -9这种暴力的方式关闭应用,有可能会导致我们的业务逻辑混乱,数据不一致等问题,因此我们希望使用一种温柔的方式来关闭程序

在了解优雅关闭前,先了解下什么是shutdown hook


JVM提供的shutdown hook

  1. 创建主程序,添加钩子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class JVMTest {

    public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    @Override
    public void run() {

    System.out.println("this is a custom shutdown hook");

    }
    }));

    System.out.println("JVM terminated...");
    }

    }
  2. 主程序正常结束,shutdown hook正常被调用

  3. 主程序主动退出,shutdown hook也会正常被调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    @Override
    public void run() {

    System.out.println("this is a custom shutdown hook");

    }
    }));

    System.out.println("JVM terminated...");

    System.exit(0);
    }
  4. 修改主程序,让主程序sleep 20s

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class JVMTest {

    public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    @Override
    public void run() {

    System.out.println("this is a custom shutdown hook");

    }
    }));

    System.out.println("JVM terminated after 20 seconds");

    try {
    TimeUnit.SECONDS.sleep(20);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }

    }
  5. 通过Linux的kill命令终止主程序,使用kill -9命令,shutdown hook并不会被调用,使用kill -15会被调用

简单来说,shutdown hook是jvm在关闭时会调用的一个钩子


JVM处理信号量通知

  1. System#initializeSystemClass()初始化,调用Terminator.setup()设置信号量处理

    1
    2
    3
    4
    5
    6
    7
    8
    // Initialize the system class. Called after thread initialization.
    private static void initializeSystemClass() {
    ...
    // 对于程序 挂断、终止、中断的处理
    // Setup Java signal handlers for HUP, TERM, and INT (where available).
    Terminator.setup();
    ...
    }
  2. Terminator#setup()设置信号量处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static void setup() {
    if (handler != null) return;
    // 设置信号量处理器
    SignalHandler sh = new SignalHandler() {
    public void handle(Signal sig) {
    // 调用Shutdown.exit()方法
    Shutdown.exit(sig.getNumber() + 0200);
    }
    };
    handler = sh;

    try {
    // 程序中断处理
    Signal.handle(new Signal("INT"), sh);
    } catch (IllegalArgumentException e) {
    }
    try {
    // 程序终止处理
    Signal.handle(new Signal("TERM"), sh);
    } catch (IllegalArgumentException e) {
    }
    }
  3. Runtime#addShutdownHook()添加shutdown hook

    1
    2
    3
    4
    5
    public void addShutdownHook(Thread hook) {
    ...

    ApplicationShutdownHooks.add(hook);
    }
  4. ApplicationShutdownHooks#add()添加钩子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    // 保存钩子线程的集合
    private static IdentityHashMap<Thread, Thread> hooks;
    // 静态代码块初始化时,向Shutdown添加一个钩子线程
    static {
    try {
    Shutdown.add(1 /* shutdown hook invocation order */,
    false /* not registered if shutdown in progress */,
    new Runnable() {
    public void run() {
    runHooks();
    }
    }
    );
    hooks = new IdentityHashMap<>();
    } catch (IllegalStateException e) {
    // application shutdown hooks cannot be added if
    // shutdown is in progress.
    hooks = null;
    }
    }

    // 添加钩子线程
    static synchronized void add(Thread hook) {
    ...
    //
    hooks.put(hook, hook);
    }

    // 运行钩子线程
    static void runHooks() {
    Collection<Thread> threads;
    // 获取所有的钩子线程
    synchronized(ApplicationShutdownHooks.class) {
    threads = hooks.keySet();
    hooks = null;
    }

    // 执行
    for (Thread hook : threads) {
    hook.start();
    }

    // 等待线程执行结束
    for (Thread hook : threads) {
    while (true) {
    try {
    hook.join();
    break;
    } catch (InterruptedException ignored) {
    }
    }
    }
    }
  5. Shutdown#add()添加钩子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

    static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
    synchronized (lock) {
    ...
    // 添加钩子线程
    hooks[slot] = hook;
    }
    }
  6. System#exit()终止JVM,带程序退出状态code,非0为异常

    1
    2
    3
    public static void exit(int status) {
    Runtime.getRuntime().exit(status);
    }
  7. Runtime#exit()

    1
    2
    3
    4
    public void exit(int status) {
    ...
    Shutdown.exit(status);
    }
  8. Shutdown#exit()Shutdown#shutdown()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 由Runtime.exit()触发调用
    static void exit(int status) {
    ...
    synchronized (Shutdown.class) {
    /* Synchronize on the class object, causing any other thread
    * that attempts to initiate shutdown to stall indefinitely
    */
    beforeHalt();
    sequence();
    halt(status);
    }
    }

    private static void sequence() {
    ...
    runHooks();
    ...
    }

    // 当最后一个非daemon线程执行结束后,由JNI DestroyJavaVM调用,不会关闭JVM
    static void shutdown() {
    synchronized (lock) {
    switch (state) {
    case RUNNING: /* Initiate shutdown */
    state = HOOKS;
    break;
    case HOOKS: /* Stall and then return */
    case FINALIZERS:
    break;
    }
    }
    synchronized (Shutdown.class) {
    sequence();
    }
    }

    // 实际执行勾子线程的方法
    private static void runHooks() {
    for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
    try {
    Runnable hook;
    synchronized (lock) {
    // acquire the lock to make sure the hook registered during
    // shutdown is visible here.
    currentRunningHook = i;
    hook = hooks[i];
    }
    // 执行勾子线程的run方法
    if (hook != null) hook.run();
    } catch(Throwable t) {
    if (t instanceof ThreadDeath) {
    ThreadDeath td = (ThreadDeath)t;
    throw td;
    }
    }
    }
    }

流程图展示(大致流程)

可以看到,JVM在System类初始化时会添加信号量处理(如程序中断、程序终止),接收到信号量通知时会触发Shutdown.exit方法,调用自定义的shutdown hook


Spring Boot中的shutdown hook

  1. SpringApplication#run

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // spring容器启动流程
    public ConfigurableApplicationContext run(String... args) {

    ...
    refreshContext(context);
    ...

    }

    static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();

    // 刷新容器,如果registerShutdownHook为true,调用SpringApplicationShutdownHook#registerApplicationContext()方法注册应用上下文
    private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
    shutdownHook.registerApplicationContext(context);
    }
    refresh(context);
    }
  2. SpringApplicationShutdownHook#registerApplicationContext()SpringApplicationShutdownHook本身实现了Runnable接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    // 注册应用上下文
    // 1.注册spring容器关闭事件监听器
    // 2.调用Runtime#addShutdownHook()方法添加shutdown hook
    void registerApplicationContext(ConfigurableApplicationContext context) {
    // 添加shutdown hook
    addRuntimeShutdownHookIfNecessary();
    synchronized (SpringApplicationShutdownHook.class) {
    assertNotInProgress();
    // 注册容器关闭事件监听器
    context.addApplicationListener(this.contextCloseListener);
    this.contexts.add(context);
    }
    }

    private void addRuntimeShutdownHookIfNecessary() {
    if (this.shutdownHookAdded.compareAndSet(false, true)) {
    addRuntimeShutdownHook();
    }
    }

    // 实际调用Runtime#addShutdownHook()方法添加钩子线程
    void addRuntimeShutdownHook() {
    try {
    Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
    }
    catch (AccessControlException ex) {
    // Not allowed in some environments
    }
    }

    // 钩子线程最终执行的方法
    @Override
    public void run() {
    Set<ConfigurableApplicationContext> contexts;
    Set<ConfigurableApplicationContext> closedContexts;
    Set<Runnable> actions;
    synchronized (SpringApplicationShutdownHook.class) {
    this.inProgress = true;
    contexts = new LinkedHashSet<>(this.contexts);
    closedContexts = new LinkedHashSet<>(this.closedContexts);
    actions = new LinkedHashSet<>(this.handlers.getActions());
    }
    // 关闭容器并等待容器停止
    contexts.forEach(this::closeAndWait);
    closedContexts.forEach(this::closeAndWait);

    // 执行handlers中的action
    actions.forEach(Runnable::run);
    }

    private void closeAndWait(ConfigurableApplicationContext context) {
    if (!context.isActive()) {
    return;
    }

    // 关闭容器
    context.close();
    try {
    // 等待一段时间
    int waited = 0;
    while (context.isActive()) {
    if (waited > TIMEOUT) {
    throw new TimeoutException();
    }
    Thread.sleep(SLEEP);
    waited += SLEEP;
    }
    }
    catch (InterruptedException ex) {
    Thread.currentThread().interrupt();
    logger.warn("Interrupted waiting for application context " + context + " to become inactive");
    }
    catch (TimeoutException ex) {
    logger.warn("Timed out waiting for application context " + context + " to become inactive", ex);
    }
    }
  3. 容器关闭时,会调用AbstractApplicationContext#close()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    // 容器关闭
    @Override
    public void close() {
    synchronized (this.startupShutdownMonitor) {
    doClose();

    // 移除shutdown hook
    if (this.shutdownHook != null) {
    try {
    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
    }
    catch (IllegalStateException ex) {
    // ignore - VM is already shutting down
    }
    }
    }
    }

    // 实际关闭容器的方法
    protected void doClose() {
    // Check whether an actual close attempt is necessary...
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
    ...

    // 发布容器关闭事件,所有监听了该事件的监听器都会被通知,上面的SpringApplicationShutdownHook类就监听了该事件
    publishEvent(new ContextClosedEvent(this));

    ...

    // 销毁BeanFactory中的单例bean,可以通过实现Disposable接口重写destroy方法,会在这里被调用
    destroyBeans();

    // 关闭BeanFactory
    closeBeanFactory();

    // 留给子类实现
    onClose();

    ...
    }
    }

流程展示

可以看到,SpringBoot启动时会添加一个SpringApplicationShutdownHook对象,该类实现了Runnable接口,run方法中调用context.close来关闭容器,从而可以在JVM关闭前进行一些通知或清理工作


何为优雅关闭

优雅关闭,就是指程序彻底停止前,当前正在处理的任务需要继续执行、服务器不再接受新的请求、应用从注册中心下线等


如何优雅关闭Spring Boot服务

优雅关闭服务的关键在于我们需要找到一个缓冲点,这个缓冲点可以让我们在JVM关闭前执行一系列资源清理工作,根据上面的分析,可以知道SpringBoot给我们提供了两个缓冲点,一个是容器关闭事件监听,一个是实现Disposable接口重写destroy()方法

  1. 添加缓冲点

    • 监听Spring容器关闭事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Component
      public class SpringCloseEventListener implements ApplicationListener<ContextClosedEvent> {

      @Override
      public void onApplicationEvent(ContextClosedEvent event) {
      System.out.println("执行资源清理工作...");
      }

      }
    • 实现Disposable接口或在方法上使用@PreDestroy注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      @Component
      public class DisposableClass implements DisposableBean {

      @Override
      public void destroy() throws Exception {
      System.out.println("DisposableClass#destroy");
      }

      }

      @Component
      public class PreDestroyBean {

      @PreDestroy
      public void preDestroy() {
      System.out.println("执行PreDestroy()方法...");
      }

      }
  2. 关闭服务

    • 通过System.exit()直接退出

    • 通过AbstractApplicationContext#close关闭服务

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @RequestMapping(value = "/service")
      @RestController
      public class ShutdownController implements ApplicationContextAware {

      private ApplicationContext context;

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

      @GetMapping(value = "/shutdown")
      public String shutdown() {
      ((ConfigurableApplicationContext) context).close();

      return "ok";
      }
      }
    • 通过ApplicationPidFileWriter将服务pid写入指定文件,使用脚本kill对应pid的进程

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @SpringBootApplication
      public class UserApplication {

      public static void main(String[] args) {
      SpringApplication application = new SpringApplication(UserApplication.class);
      application.addListeners(new ApplicationPidFileWriter("/home/service/user-service.pid"));
      application.run(args);
      }

      }
      1
      cat /home/service/user-service.pid | xargs kill

使用actuator实现优雅关闭

  • SpringBoot 2.3及以上版本
  1. 添加缓冲点,参考这里

  2. 引入actuator依赖,添加配置信息

    • 引入依赖

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
    • 修改application.yml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      server:
      port: 8090
      # 采用优雅停机方式,默认immediate立即停机
      shutdown: graceful

      # 等待超时时间
      spring:
      lifecycle:
      timeout-per-shutdown-phase: 15s

      # 暴露shutdown路径
      management:
      endpoints:
      web:
      exposure:
      include: shutdown
      endpoint:
      shutdown:
      enabled: true
    • 发送shutdown请求(使用POST方式)

      1
      curl -X POST http://localhost:8090/actuator/shutdown
  3. actuator工作原理

    • ShutdownEndpoint类暴露了一个shutdown端点,当请求该端点时,内部调用了AbstractApplicationContext#close方法关闭容器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      private void performShutdown() {
      try {
      Thread.sleep(500L);
      }
      catch (InterruptedException ex) {
      Thread.currentThread().interrupt();
      }
      this.context.close();
      }
  • SpringBoot 2.3版本以下(使用Tomcat容器)
  1. 引入actuator依赖,添加配置信息

    • 引入依赖

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
    • 修改application.yml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      # 暴露shutdown路径
      management:
      endpoints:
      web:
      exposure:
      include: shutdown
      endpoint:
      shutdown:
      enabled: true
  2. 创建优雅关闭配置类

    • 自定义TomcatCustomizer类实现TomcatConnectorCustomizerApplicationListener接口,获取Tomcat连接器和线程池,接收到容器关闭事件后关闭线程池

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      @Configuration
      @Slf4j
      public class GracefulShutdownConfig {

      @Bean
      public TomcatCustomizer tomcatCustomizer() {
      return new TomcatCustomizer();
      }

      @Bean
      public ServletWebServerFactory servletWebServerFactory() {
      TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
      // 将自定义的TomcatConnectorCustomizer添加到Tomcat容器中
      serverFactory.addConnectorCustomizers(tomcatCustomizer());
      return serverFactory;
      }

      public class TomcatCustomizer implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {

      private volatile Connector connector;

      private int timeout = 30;

      @Override
      public void customize(Connector connector) {
      this.connector = connector;
      }

      @Override
      public void onApplicationEvent(ContextClosedEvent event) {
      if (null != connector) {
      this.connector.pause();

      Executor executor = this.connector.getProtocolHandler().getExecutor();
      if (executor instanceof ThreadPoolExecutor) {
      ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
      threadPoolExecutor.shutdown();
      try {
      if (threadPoolExecutor.awaitTermination(timeout, TimeUnit.SECONDS)) {
      log.warn("tomcat didn't close gracefully in {} seconds, it turns to shutdown forcibly", timeout);
      }
      } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      }
      }
      }
      }
      }

      }

注意:暴露shutdown端点存在风险,需要保证该端点不会被其他人随意调用


总结:

  • JVM为我们提供了添加shutdown hook的方法,在JVM关闭前会执行这些hook线程
  • JVM在处理信号量通知的过程中使用了shutdown hook
  • SpringBoot提供了SpringApplicationShutdownHook(实现了Runnable接口)作为shutdown hook,run方法中调用AbstractApplicationContext#close方法关闭容器
  • 容器关闭时会进行一系列资源销毁工作,如发布容器关闭事件、销毁Bean Factory中的bean、关闭Bean Factory、执行子类的onClose方法等
  • 通过监听容器关闭事件或实现DisposableBean接口可以进行资源清理操作,如线程池的销毁、服务从注册中心下线、MQ消费等

上面只是简单描述了如何优雅关闭SpringBoot服务,实际在开发过程中还需要考虑到很多因素,如不同的注册中心、不同的rpc框架、服务部署方式等

道阻且长,行则将至。