Spring Boot 优雅停机:别让应用被拔电源式下线

欢迎你来读这篇博客,这篇博客主要是关于 Spring Boot 优雅停机

其中包括了 Spring Boot 应用在关闭时的生命周期、server.shutdown=graceful 配置、timeout-per-shutdown-phase 的作用、
@PreDestroy 的执行时机,以及在生产环境中如何配合线程池、消息队列和 Kubernetes 做更可靠的停机处理。

序言

在开发环境里,我们经常随手停止一个 Spring Boot 应用,IDE 点一下红色按钮,或者直接杀掉进程,好像也没什么问题。

但到了生产环境,停机就不是这么随意的事情了。

假设一个服务正在处理订单、结算、支付回调、消息消费或者文件上传,这时候如果进程被直接强杀,就可能出现请求处理中断、事务未完成、消息未确认、资源未释放等问题。

这类问题最麻烦的地方在于,它们通常不是马上炸,而是变成后续排查时的“历史遗留悬案”。

所以,优雅停机不是一个锦上添花的配置,而是生产服务下线时应该具备的基本能力。

一句话概括:

优雅停机就是让应用在退出前,不再接新活,处理完手上的活,释放资源,然后再体面地下线。

正文

chapter 1:什么是优雅停机

所谓优雅停机,英文叫 Graceful Shutdown

它并不是简单地让应用“慢一点关闭”,而是让应用在收到关闭信号后,按照一个相对安全的顺序退出。

一个比较理想的停机过程应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
收到关闭信号

停止接收新请求

等待正在处理的请求完成

停止后台任务、定时任务、消息消费

关闭线程池

销毁 Spring Bean

执行 @PreDestroy 等销毁逻辑

释放连接池、文件句柄、网络连接等资源

应用退出

如果没有优雅停机,应用可能就是下面这种情况:

1
2
3
4
5
6
7
正在处理请求

进程被强杀

请求中断

事务、消息、资源状态变得不可控

这就像饭还没吃完,服务员直接把桌子抬走了。

你说它效率高吧,确实挺高;你说它文明吧,多少有点抽象。

chapter 2:Spring Boot 中如何开启优雅停机

Spring Boot 中可以通过下面的配置开启优雅停机:

1
2
3
4
5
6
server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 30s

其中:

1
2
server:
shutdown: graceful

表示开启 Web Server 的优雅停机能力。

应用关闭时,内嵌 Web 容器会停止接收新请求,并给已经进入应用的请求一段时间完成处理。

1
2
3
spring:
lifecycle:
timeout-per-shutdown-phase: 30s

表示每个关闭阶段最多等待 30s

如果在这个时间内请求或生命周期组件没有正常完成,Spring 不会一直等下去,而是继续推进关闭流程。

需要注意的是,timeout-per-shutdown-phase 并不等于“整个应用最多只能关闭 30 秒”。它更准确的含义是:Spring
在关闭每个生命周期阶段时的等待时间。

所以,如果项目中存在多个生命周期组件,真实关闭时间要结合实际情况测试。

chapter 3:停机时 Spring Boot 内部大致发生了什么

当 Spring Boot 应用收到正常关闭信号后,大致流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
收到 SIGTERM

JVM Shutdown Hook 被触发

SpringApplication 开始关闭 ApplicationContext

Web Server 开始 graceful shutdown

停止接收新请求

等待已有请求完成

停止 SmartLifecycle Bean

销毁普通 Bean

执行 @PreDestroy、DisposableBean、destroyMethod

应用进程退出

这里有一个非常关键的点:

优雅停机依赖正常关闭信号。

例如:

1
kill <pid>

这种方式默认发送的是 SIGTERM,应用有机会执行关闭流程。

但是:

1
kill -9 <pid>

发送的是 SIGKILL,这是强杀。JVM 没有机会执行 shutdown hook,Spring 也没有机会关闭上下文。

所以,kill -9 不属于优雅停机。

它更像是“别解释了,直接下线”。

在生产环境里,kill -9 应该是最后手段,而不是发布脚本里的日常操作。

chapter 4:@PreDestroy 的作用

很多人一提到 Spring Boot 停机,就会想到 @PreDestroy

例如:

1
2
3
4
5
6
7
8
9
10
11
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class ShutdownLogger {

@PreDestroy
public void destroy() {
System.out.println("应用正在关闭,开始释放资源...");
}
}

@PreDestroy 是 Spring Bean 销毁前执行的回调方法。

它适合做一些资源清理工作,比如:

  • 关闭本地缓存;
  • 刷新内存数据;
  • 关闭自定义客户端;
  • 打印停机日志;
  • 释放非 Spring 管理的资源。

但是,@PreDestroy 不是万能的。

它不应该承担所有停机逻辑。

比如下面这些事情,只靠 @PreDestroy 通常是不够的:

  • 等待 HTTP 请求处理完成;
  • 等待线程池任务执行完成;
  • 停止消息队列继续拉取消息;
  • 等待正在消费的消息 ACK;
  • 停止定时任务;
  • 从注册中心或负载均衡中摘除流量。

所以,更准确地说:

@PreDestroy 是停机过程中的最后收尾动作,而不是整个优雅停机机制本身。

chapter 5:线程池也要优雅关闭

很多业务逻辑并不是直接在 HTTP 请求线程里完成的。

例如:

  • @Async 异步任务;
  • 自定义线程池;
  • 定时任务;
  • 批量计算任务;
  • 文件处理任务;
  • 消息消费后的异步处理。

如果只配置了:

1
2
server:
shutdown: graceful

那么它主要保护的是 Web 请求层面,并不代表所有后台线程都能自动执行完成。

如果项目里使用 Spring 的任务执行器,可以增加下面的配置:

1
2
3
4
5
6
7
8
9
10
spring:
task:
execution:
shutdown:
await-termination: true
await-termination-period: 30s
scheduling:
shutdown:
await-termination: true
await-termination-period: 30s

如果是自定义线程池,可以这样配置:

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class ExecutorConfig {

@Bean
public ThreadPoolTaskExecutor bizExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setThreadNamePrefix("biz-executor-");
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(1000);

// 关闭时等待任务执行完成
executor.setWaitForTasksToCompleteOnShutdown(true);

// 最多等待 30 秒
executor.setAwaitTerminationSeconds(30);

executor.initialize();
return executor;
}
}

这个配置非常重要。

否则就可能出现一种尴尬情况:

HTTP 请求已经优雅停机了,但异步线程池里的任务被直接中断了。

对于普通日志任务,这可能只是少打一条日志。

但对于结算、库存、支付、消息确认这类业务,问题就大了。

生产环境最怕的不是报错,而是“半成功”。

半成功的数据,通常比失败更难处理。

chapter 6:消息消费场景要特别小心

如果应用中有 Kafka、RabbitMQ、RocketMQ 等消息消费逻辑,优雅停机更要谨慎。

理想顺序应该是:

1
2
3
4
5
6
7
8
9
10
11
停止拉取新消息

等待当前正在处理的消息完成

提交 offset 或 ACK

关闭 consumer

关闭线程池

关闭数据库连接池等底层资源

这里最容易犯的错误是:底层资源先关闭,业务线程还在跑。

例如:

1
2
3
4
5
6
7
数据库连接池已经关闭

消息消费线程还在处理业务

业务继续访问数据库

报错

这种问题看起来像数据库异常,本质上却是停机顺序不对。

如果停机顺序比较复杂,可以考虑实现 SmartLifecycle,通过 phase 控制组件的启动和关闭顺序。

示例:

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
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;

@Component
public class MessageConsumerLifecycle implements SmartLifecycle {

private volatile boolean running = false;

@Override
public void start() {
running = true;
// 启动消息消费
}

@Override
public void stop(Runnable callback) {
try {
// 1. 停止拉取新消息
// 2. 等待正在处理的消息完成
// 3. 提交 ACK 或 offset
running = false;
} finally {
callback.run();
}
}

@Override
public void stop() {
stop(() -> {});
}

@Override
public boolean isRunning() {
return running;
}

@Override
public int getPhase() {
return 1000;
}
}

注意,stop(Runnable callback) 中一定要调用:

1
callback.run();

否则 Spring 可能会一直等到超时。

chapter 7:Kubernetes 环境下的优雅停机

如果应用部署在 Kubernetes 中,只配置 Spring Boot 还不够。

因为 Kubernetes 删除 Pod 时,大致会经历下面的过程:

1
2
3
4
5
6
7
8
9
Pod 被标记为 Terminating

执行 preStop hook

发送 SIGTERM 给容器主进程

等待 terminationGracePeriodSeconds

如果还没退出,发送 SIGKILL 强杀

所以,Kubernetes 下要同时考虑:

  • Spring Boot 的 graceful shutdown;
  • Kubernetes 的 preStop
  • Kubernetes 的 terminationGracePeriodSeconds
  • readinessProbe 的流量摘除;
  • 外部负载均衡的刷新延迟。

一个常见配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: your-app:latest
lifecycle:
preStop:
exec:
command: [ "sh", "-c", "sleep 10" ]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080

为什么 preStop 里要 sleep 10

因为 Pod 开始 terminating 后,服务发现和负载均衡摘除流量并不一定是瞬间完成的。

可能会出现一个短暂窗口:

1
2
Pod 已经准备关闭
但仍然有新流量打进来

preStop sleep 的作用,就是给 Kubernetes、Service、Ingress 或外部负载均衡一点时间完成流量摘除。

但是要注意时间对齐。

如果配置是:

1
2
3
spring:
lifecycle:
timeout-per-shutdown-phase: 30s

同时:

1
2
3
preStop:
exec:
command: [ "sh", "-c", "sleep 10" ]

那么 Kubernetes 的:

1
terminationGracePeriodSeconds: 60

就要大于 preStop 时间加上 Spring Boot graceful shutdown 的等待时间。

否则就会出现:

1
2
3
4
preStop sleep 10s
Spring Boot 还想等待 30s
Kubernetes 总共只给 30s
结果 Spring 还没优雅完,K8s 直接 SIGKILL

这就像公司说“你慢慢交接”,但半小时后门禁直接失效。

这不是优雅,是流程互相背刺。

chapter 8:推荐配置模板

Spring Boot 推荐配置:

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
server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 30s

task:
execution:
shutdown:
await-termination: true
await-termination-period: 30s
scheduling:
shutdown:
await-termination: true
await-termination-period: 30s

management:
endpoint:
health:
probes:
enabled: true
endpoints:
web:
exposure:
include: health,info

Kubernetes 推荐配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: your-app:latest
lifecycle:
preStop:
exec:
command: [ "sh", "-c", "sleep 10" ]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080

这套配置的核心思路是:

1
2
3
4
5
6
Kubernetes 负责流量摘除
Spring Boot 负责应用上下文关闭
Web Server 负责请求排水
线程池负责等待任务完成
消息消费者负责停止拉取和完成 ACK
@PreDestroy 负责最后资源清理

不要指望一个配置解决所有问题。

真正的生产稳定性,往往来自多个层面的配合。

chapter 9:如何验证优雅停机是否生效

优雅停机一定要验证,不要只看配置。

可以写一个慢接口:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(20_000);
return "ok";
}
}

启动应用:

1
java -jar app.jar

发起请求:

1
curl http://localhost:8080/slow

在请求还没返回时,执行:

1
kill <pid>

观察结果:

  • 请求是否正常返回;
  • 应用是否在请求完成后退出;
  • 新请求是否不再被接收;
  • @PreDestroy 是否执行;
  • 线程池任务是否执行完成;
  • MQ 消费是否完成 ACK;
  • Kubernetes 中 Pod 是否被 SIGKILL
  • 日志中是否出现连接池已关闭但业务还在执行的异常。

可以加一个停机日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class ShutdownLogger {

private static final Logger log = LoggerFactory.getLogger(ShutdownLogger.class);

@PreDestroy
public void onShutdown() {
log.info("[shutdown] application is shutting down, start cleanup");
}
}

验证时尽量不要只在 IDE 里点停止按钮。

不同 IDE 的停止行为可能不同,有些情况下并不能完整模拟生产环境中的 SIGTERM

更推荐用真实 jar 包运行,然后通过 kill <pid> 验证。

chapter 10:常见误区

误区一:配置了 graceful 就万事大吉

不对。

server.shutdown=graceful 主要解决 Web 请求层面的优雅停机。

线程池、消息消费、定时任务、外部连接、注册中心流量摘除,都需要单独考虑。

误区二:@PreDestroy 等于优雅停机

不对。

@PreDestroy 是 Bean 销毁前的回调。

它适合做资源清理,但不适合承载整个停机流程。

误区三:kill -9 也能触发优雅停机

不能。

kill -9 是强杀,JVM 没有机会执行 shutdown hook,Spring 也没有机会关闭上下文。

误区四:超时时间越长越好

不一定。

时间太短,业务来不及收尾。

时间太长,发布、扩缩容、故障恢复都会变慢。

比较合理的做法是根据接口 P99、最长业务任务、MQ 消费耗时和发布系统超时限制来综合设置。

误区五:Kubernetes 会自动帮我处理好一切

不会。

Kubernetes 只负责容器生命周期管理。

应用内部的请求排水、线程池关闭、消息确认、资源释放,仍然需要应用自己设计。

参考资料

  • Spring Boot Reference Documentation:Graceful Shutdown
  • Spring Framework Reference Documentation:Using @PostConstruct and @PreDestroy
  • Spring Framework Reference Documentation:Customizing the Nature of a Bean
  • Kubernetes Documentation:Pod Lifecycle
  • Kubernetes Documentation:Container Lifecycle Hooks

启示录

优雅停机的本质,不是让应用“慢慢关闭”,而是让系统在退出时保持秩序。

一个可靠的应用,不应该只关注启动速度,也应该关注退出姿势。

上线要稳,下线也要稳。

真正的工程能力,不只体现在服务能跑起来,也体现在服务能不能安全地停下来。

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Spring Boot 优雅停机:别让应用被拔电源式下线
https://allendericdalexander.github.io/2026/06/01/java_shutdown/
作者
AtLuoFu
发布于
2026年6月1日
许可协议