优雅停机

优雅停机(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/shutdown

3. 编程式关闭

@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 按创建的逆序销毁。若需精确控制顺序,使用 @DependsOnSmartLifecycle

@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 消费者

相关链接