Skip to main content

如何实现 Spring Boot 服务的优雅关闭

· 6 min read
Dylan Li

原生 JVM 进程的优雅退出 - ShutdownHook

下面一段代码是最常用的一种优雅关闭实现,使用 Runtime.getRuntime().addShutdownHook() 可以向 JVM Runtime 注册一条 Hook Thread 实例,用于执行我们的优雅关闭逻辑, 值得注意的是,这个 Thread 实例并不会立刻被调度执行。 当我们从 OS 层面向 JVM 进程传递某些 Signal(信号)后,这个 Hook 线程才会被运行。


public class ShutdownHookExample {

private static volatile boolean running = true;

public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Shutdown Hook is activated");
//close resources...
//change running signal to false.
running = false;
}
}));
System.out.println("The Java Virtual Machine is running!");
while (running) {
//jvm is running.
}
//when running signal is false, main thread will println this line.
System.out.println("The Java Virtual Machine shut down gracefully");
}
}

那么前面所说的 Signal 到底是什么呢?

Signal 是 OS 与进程间进行沟通的一种机制,这种机制常常用来触发进程的某些动作。在 Linux 中,Signal 通过 kill 命令或其他方式来向进程传递。 运行 kill -l 可以看到标准 Signal 在 Linux OS 中是怎样的。

linux-kill-l

1) SIGHUB 为例,SIGHUB 是一种命名为 HUB 的信号,前面的 1 则是使用 kill 命令时要传入的值(命令格式为 kill -[Signal Num] [Process ID] )。

大部分场景下会用到的 Signal 并不是很多:

SignalDescribeLinux Command
HUB在进程运行时通知其重载配置kill -1
INT终止信号(正常退出,允许进程自行清除状态后退出)^C
KILL终止信号(强制退出,立刻回收内存并剥离CPU)kill -9
TERM终止信号(优雅退出,允许进程自行清除状态后退出)kill -15

Java 默认对 HUBINTTERM 三种信号提供了支持,开发者可以有效地利用这些机制完成我们的某些需求。至于 JVM 进程是如何做到捕获这些信号并执行所需逻辑的, 我们可以从 JDK 源代码中找到其出处,核心的类有 java.lang.Systemjava.lang.Runtimejava.lang.Shutdownjava.lang.Terminatorjava.lang.ApplicationShutdownHookssun.misc.Signal 以及 sun.misc.SignalHandler ,感兴趣的可以看一下源代码,重点的方法有:

  • Terminator.setup():设置 Java 对 HUBINTTERM 信号的捕获处理实例。
  • Shutdown.add(int slot, boolean registerShutdownInProgress, Runnable hook):向 hooks 集合中追加 Hook 线程。
  • Shutdown.exit(int status):在 Runtime.exit 时调用,执行内置的、用户自定义的 Shutdown 逻辑,其中包括执行 hooks 集合中的 Hook 线程。
  • Shutdown.runHooks():运行所有已注册的 Shutdown Hook。

Spring Boot 如何做到

从 Spring Boot 2.3 版本起,开发者可以通过 Stater 方式在配置文件中设置,之后通过向其发送 SIGINTSIGTERM 信号便可触发服务的优雅关闭。重点有两个属性:

  • server.shutdown=graceful:设置 Web Server 的 shutdown 方式,graceful 为优雅关闭,immediate 为立即关闭。
  • spring.lifecycle.timeout-per-shutdown-phase=${duration}:在触发优雅关闭后,留给 Spring 容器关闭资源的超时时间, 超过这个时间,即使 Spring 中还有没关闭的资源,进程也会立刻终止并输出相应日志

能实现这样的优雅关闭,内部依然是依赖 Java 的原生支持。Spring Boot 中对于 Web Server、Application Context 和 Bean 都提供了基于 Shutdown Hook 的关闭机制。

在实际编码中,我们通过SpringApplication.run(Class<?> primarySource, String... args)静态方法来运行我们的入口类,其内部最终 调用了ConfigurableApplicationContext run(String... args)

public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
......
refreshContext(context);//关于Spring容器ShutdownHook的注册逻辑在这里
......
return context;
}

Spring 容器默认情况下会开启 Shutdown Hook 用于容器和 Bean 的销毁,而server.shutdown=graceful则是在 Web Server 层面设置了关闭方式,在触发关闭的同时, Web Server 会关闭新链接的建立并等待已经接受的请求响应完毕,在达到设定的超时时间后进而完成整个进程的主动退出。

常见问题

在向进程传递 INTTERM 终止信号前,正在运行的 jar 文件不允许被修改、迁移或删除,否则会导致退出过程中出现部分 Bean 无法关闭的情况。