优雅停机
优雅停机(Graceful Shutdown)指应用收到停止信号后,不立即中断,而是先停止接收新请求,等待进行中的请求处理完毕,再清理资源、关闭连接,最终退出进程。
Spring Boot 内置优雅停机(2.3+)
Spring Boot 2.3 开始对 Tomcat、Undertow、Jetty、Netty 均内置了优雅停机支持:
server:
shutdown: graceful # 开启优雅停机(默认 immediate:立即关闭)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 每个关闭阶段最长等待时间(默认 30s)开启后的关闭流程:
收到 SIGTERM
│
▼
停止接收新 HTTP 请求(拒绝新连接)
│
▼
等待进行中的请求处理完毕(最长 timeout-per-shutdown-phase)
│
▼
执行 Spring 容器关闭流程(@PreDestroy、DisposableBean、ContextClosedEvent)
│
▼
JVM 退出
触发停机的方式
1. SIGTERM 信号(Linux / Docker / K8s 标准方式)
kill -15 <pid> # 发送 SIGTERM,触发优雅停机
kill -2 <pid> # SIGINT(Ctrl+C),同样触发优雅停机
kill -9 <pid> # SIGKILL,强制杀进程(跳过所有清理,避免使用)2. Actuator /actuator/shutdown(HTTP 触发,需手动开启)
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown, health# 发送 POST 请求触发关闭(生产环境注意安全,建议仅内网可访问)
curl -X POST http://localhost:8080/actuator/shutdown3. 编程式关闭
@Component
@RequiredArgsConstructor
public class ShutdownController {
private final ApplicationContext context;
public void shutdown() {
// 触发 Spring 容器关闭,进而触发优雅停机流程
((ConfigurableApplicationContext) context).close();
}
}关闭回调:执行清理逻辑
@PreDestroy(推荐,标准 JSR-250)
@Service
@Slf4j
public class CacheService {
@PreDestroy
public void cleanup() {
log.info("关闭前刷新缓存...");
flushAllToRedis();
log.info("缓存已刷新");
}
}DisposableBean 接口
@Component
@Slf4j
public class ConnectionPool implements DisposableBean {
@Override
public void destroy() throws Exception {
log.info("关闭数据库连接池...");
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
}
}ApplicationListener<ContextClosedEvent>
@Component
@Slf4j
public class GracefulShutdownListener
implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("Spring 容器开始关闭,执行收尾工作...");
// 通知外部服务、取消注册服务发现...
}
}@Bean destroyMethod
@Bean(destroyMethod = "close")
public HeavyResource heavyResource() {
return new HeavyResource(); // 关闭时自动调用 close()
}等待异步任务完成
应用关闭时,线程池中未完成的异步任务默认会被中断。配置等待:
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(500);
// 关键:关闭时等待任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}YAML 方式(Spring Boot 2.2+):
spring:
task:
execution:
shutdown:
await-termination: true
await-termination-period: 30s异步线程池详细配置参见 异步与线程池。
消息队列消费者的优雅停机
消费者停机时,应先暂停拉取新消息,等当前批次处理完毕再关闭。
RabbitMQ
@Component
@RequiredArgsConstructor
@Slf4j
public class RabbitShutdownListener
implements ApplicationListener<ContextClosedEvent> {
private final RabbitListenerEndpointRegistry registry;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("停止 RabbitMQ 消费者...");
// 停止所有监听容器(不再拉取新消息)
registry.getListenerContainers()
.forEach(container -> {
container.stop();
log.info("容器已停止: {}", container.getListenerId());
});
}
}Kafka
@Component
@RequiredArgsConstructor
@Slf4j
public class KafkaShutdownListener
implements ApplicationListener<ContextClosedEvent> {
private final KafkaListenerEndpointRegistry registry;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("停止 Kafka 消费者...");
registry.getListenerContainers()
.forEach(container -> {
// pause 不立即停止,等当前 poll 批次处理完再停
container.pause();
});
}
}消息队列集成详见 消息队列。
定时任务的优雅停机
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(4);
scheduler.setThreadNamePrefix("scheduler-");
// 关闭时等待正在执行的任务完成
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
scheduler.initialize();
registrar.setTaskScheduler(scheduler);
}
}定时任务详见 定时任务。
健康检查与 Readiness 探针
优雅停机期间,应提前将服务从负载均衡/服务注册中心摘除,避免新请求打到正在关闭的实例。
management:
endpoint:
health:
show-details: always
health:
livenessstate:
enabled: true
readinessstate:
enabled: true// 开始关闭时,手动将 Readiness 标记为 OUT_OF_SERVICE
@Component
@RequiredArgsConstructor
public class ReadinessShutdownListener
implements ApplicationListener<ContextClosedEvent> {
private final ApplicationAvailability availability;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 让 K8s / 负载均衡感知到该实例已不可用
AvailabilityChangeEvent.publish(event.getApplicationContext(),
ReadinessState.REFUSING_TRAFFIC);
}
}Kubernetes 优雅停机
K8s 删除 Pod 时的流程:
1. Pod 状态设为 Terminating
2. 从 Service Endpoints 中摘除(停止路由新流量)
3. 发送 SIGTERM 给容器(两步可能有短暂并行,需 preStop 补偿)
4. 等待 terminationGracePeriodSeconds(默认 30s)
5. 若超时,发送 SIGKILL 强制终止
推荐配置:
# deployment.yaml
spec:
template:
spec:
terminationGracePeriodSeconds: 60 # 给应用足够时间
containers:
- name: app
lifecycle:
preStop:
exec:
# preStop 在 SIGTERM 之前执行,补偿 Endpoints 摘除延迟
command: ["sh", "-c", "sleep 5"]# application.yaml(Spring Boot 端配置要小于 terminationGracePeriodSeconds)
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 50s # 留 10s 给 JVM 本身关闭K8s 部署详见 K8s部署,健康探针详见 Actuator监控。
关闭顺序控制
Spring 容器关闭时,Bean 按创建的逆序销毁。若需精确控制顺序,使用 @DependsOn 或 SmartLifecycle:
@Component
public class DatabaseShutdownBean implements SmartLifecycle {
private volatile boolean running = false;
@Override
public void start() { running = true; }
@Override
public void stop(Runnable callback) {
log.info("优先关闭数据库连接...");
dataSource.close();
running = false;
callback.run(); // 通知 Spring 该 Bean 已关闭完毕
}
@Override
public boolean isRunning() { return running; }
@Override
public int getPhase() {
// 数值越大越先启动,越后关闭;数值越小越后启动,越先关闭
return Integer.MAX_VALUE; // 最后启动、最先关闭(用于基础设施 Bean)
}
}验证优雅停机效果
# 启动应用
java -jar app.jar &
APP_PID=$!
# 发起持续请求
for i in $(seq 1 100); do
curl -s http://localhost:8080/api/slow-task & # 慢接口(2s)
done
# 发送停止信号
kill -15 $APP_PID
# 观察:进行中的请求是否正常返回,新请求是否被拒绝常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 停机后仍有请求被截断 | timeout 配置过短 | 增大 timeout-per-shutdown-phase |
| K8s Pod 一直处于 Terminating | 进程未响应 SIGTERM | 确保 JVM 进程为 PID 1,或用 exec java ... 启动 |
| 异步任务未完成就退出 | 未配置 await-termination | 线程池设置 setWaitForTasksToCompleteOnShutdown(true) |
| 消息被重复消费 | 消费者未等处理完成就关闭 | 监听 ContextClosedEvent 先 pause 消费者 |