微服务架构Spring Boot 2.2.x优雅停机的完整方案需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。
在微服务架构中,优雅停机是保障服务可靠性的重要一环。本文分享在 Spring Boot 2.2.x 版本中实现优雅停机的完整方案,涵盖 Nacos 反注册、Kafka 消费者停止、线程池等待等核心场景。

当服务收到停止信号(如 K8s Pod 滚动更新、手动发布等)时,如果直接终止,可能导致:
Spring Boot 2.3.0 才内置 server.shutdown=graceful 配置,自动启用 Web 容器优雅停机。
但很多存量项目仍运行在 2.2.x 版本,需要自行实现优雅停机逻辑。
| 原则 | 说明 |
|---|---|
| 零侵入 | 不修改现有业务代码,通过新增组件实现 |
| 有开关 | 新功能可通过配置控制,默认关闭 |
| 有默认值 | 配置项有合理默认值,不配置时行为与之前一致 |
| 统一入口 | 所有停机逻辑收敛到一个 Handler,便于维护 |
kill -15 <pid> │ ▼JVM 触发 SpringContextShutdownHook │ ▼发布 ContextClosedEvent(早于 Bean 销毁) │ ├─ [GracefulShutdownHandler 监听 ★] │ ① 设置运行标志为 false(通知消费者停止 poll) │ ② 主动调用注册中心反注册(下游秒级感知) │ ③ sleep(N秒)(等待上游服务感知下线) │ 期间 HTTP 请求、异步任务、远程调用可继续完成 │ ▼Spring 依次销毁 Bean ├─ KafkaConsumer.close() ├─ ThreadPoolExecutor.shutdown()(等待任务完成) ├─ DataSource 连接池关闭 └─ Redis 连接池关闭 │ ▼Web 容器 stopping │ ▼JVM 退出(exit code 0)
监听 ContextClosedEvent,在 Spring 销毁 Bean 之前执行停机前处理:
@Slf4j@Componentpublic class GracefulShutdownHandler implements ApplicationListener<ContextClosedEvent> { private final AtomicBoolean executed = new AtomicBoolean(false); @Autowired(required = false) private NacosAutoServiceRegistration nacosAutoServiceRegistration; @Override public void onApplicationEvent(ContextClosedEvent event) { // 防止 Feign 子上下文事件冒泡导致重复执行 if (!executed.compareAndSet(false, true)) { log.info("[GracefulShutdownHandler] 重复事件,跳过。来源={}", event.getApplicationContext().getDisplayName()); return; } log.info("[GracefulShutdownHandler] 收到 ContextClosedEvent,开始优雅停机..."); // 1. 设置全局运行标志 SystemConfig.RUNNING = false; log.info("设置 RUNNING=false,通知消费者停止"); // 2. 主动 Nacos 反注册 if (nacosAutoServiceRegistration != null) { log.info("Nacos 主动反注册..."); nacosAutoServiceRegistration.destroy(); log.info("Nacos 反注册完成"); } // 3. 等待上游服务感知(Ribbon 缓存刷新周期约 30s) int waitSeconds = 30; log.info("等待 {} 秒,让上游服务感知下线...", waitSeconds); ThreadUtil.sleep(waitSeconds * 1000L); log.info("优雅停机预处理完成,交由 Spring 继续销毁 Bean"); }}| 事件 | 触发时机 | 是否适合 |
|---|---|---|
ContextClosedEvent | Bean 销毁之前 | ✅ 适合 |
PreDestroy | Bean 销毁时 | ❌ 太晚,DataSource 可能已关闭 |
ShutdownHook | JVM 退出时 | ❌ 太晚,Spring 容器已销毁 |
ContextClosedEvent 在 destroyBeans() 之前发布,此时所有 Bean 仍可用,可以安全执行反注册、等待等操作。
问题:Spring Cloud 为每个 Feign Client 创建独立子 ApplicationContext。主上下文销毁子上下文时,每个子上下文都会发布 ContextClosedEvent 并冒泡到父上下文,导致 Handler 被触发 N 次。
JVM SIGTERM └─ 主 context.doClose() ├─ publishEvent(ContextClosedEvent) ← Handler 第 1 次触发 └─ destroyBeans() └─ FeignContext.destroy() ├─ 关闭子 ctx A → 事件冒泡 → Handler 第 2 次 ├─ 关闭子 ctx B → 事件冒泡 → Handler 第 3 次 └─ ...
解决:使用 AtomicBoolean 兜底,只执行第一次(主上下文事件最先到达)。
@Async 任务、定时任务可继续执行| 模式 | 实现方式 | 停止方法 |
|---|---|---|
| 注解式 | @KafkaListener | Spring 自动管理,无需额外处理 |
| 手动式 | new KafkaConsumer() + while 循环 | 需要检查 RUNNING 标志 |
public class ManualKafkaConsumer implements Runnable { private final KafkaConsumer<String, String> consumer; private volatile boolean running = true; @Override public void run() { try { while (running && SystemConfig.RUNNING) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1)); // 处理消息... consumer.commitSync(); } } finally { consumer.close(); log.info("KafkaConsumer 已关闭"); } } public void shutdown() { this.running = false; }}关键点:
poll() 超时设为 1 秒,RUNNING=false 后最多延迟 1 秒退出finally 块确保 consumer.close() 一定执行commitSync() 在关闭前提交 offset,避免消息重复推荐使用 Spring 的 ThreadPoolTaskExecutor,自动管理生命周期:
@Configurationpublic class ThreadPoolConfig { @Bean("businessExecutor") public ThreadPoolTaskExecutor businessExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(100); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("business-"); // 关键配置:等待任务完成 executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(20); // 最多等待 20 秒 executor.initialize(); return executor; }}| 配置项 | 说明 |
|---|---|
setWaitForTasksToCompleteOnShutdown(true) | 停机时等待已提交任务完成 |
setAwaitTerminationSeconds(20) | 最多等待 20 秒,超时强制中断 |
如果必须手动创建,需要在停机时显式调用 shutdown():
ExecutorService executor = new ThreadPoolExecutor(...);// 停机时executor.shutdown();try { if (!executor.awaitTermination(20, TimeUnit.SECONDS)) { executor.shutdownNow(); }} catch (InterruptedException e) { executor.shutdownNow();}如果服务有较高并发 HTTP 流量,可以通过 Filter 精确计数在途请求:
@Componentpublic class GracefulShutdownFilter implements Filter { private final AtomicInteger activeRequests = new AtomicInteger(0); private volatile boolean shuttingDown = false; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (shuttingDown) { // 可选:拒绝新请求,返回 503 response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Service is shutting down"); return; } activeRequests.incrementAndGet(); try { chain.doFilter(request, response); } finally { activeRequests.decrementAndGet(); } } public void awaitRequestsCompletion(long timeoutMs) { this.shuttingDown = true; long deadline = System.currentTimeMillis() + timeoutMs; while (activeRequests.get() > 0 && System.currentTimeMillis() < deadline) { ThreadUtil.sleep(100); } }}// 在 GracefulShutdownHandler 中@Autowired(required = false)private GracefulShutdownFilter shutdownFilter;// 停机时if (shutdownFilter != null) { shutdownFilter.awaitRequestsCompletion(5000); // 等待在途请求完成}# 检查停机日志grep "GracefulShutdownHandler" server.log# 期望输出:# [GracefulShutdownHandler] 收到 ContextClosedEvent,开始优雅停机...# 设置 RUNNING=false,通知消费者停止# Nacos 主动反注册...# Nacos 反注册完成# 等待 30 秒,让上游服务感知下线...# 优雅停机预处理完成,交由 Spring 继续销毁 Bean
# 确保只触发一次(无 Feign 冒泡)grep -c "GracefulShutdownHandler.*收到" server.log# 期望:1
grep -E "收到 ContextClosedEvent|all closed success" server.log# 期望:两行时间差 ≈ 30s(sleep 时长)+ Bean 销毁时间
| 场景 | 行为 | 建议 |
|---|---|---|
kill -9 | 跳过所有 JVM 钩子,立即终止 | ❌ 禁止使用 |
| HTTP 在途请求 | sleep 后 DataSource 关闭,可能返回 500 | 启用 Filter 计数保护 |
| Feign 子上下文冒泡 | Handler 执行 N 次,停机卡住 | 加 AtomicBoolean 防护 |
| 消费者任务超时 | 线程池强制中断,可能丢失消息 | 评估业务耗时,调大 awaitTerminationSeconds |
| 需要立即关闭 | - | kill -15 正常触发,不要用 kill -9 |
优雅停机的核心要点:
ContextClosedEvent:在 Bean 销毁前执行预处理通过以上方案,可以在 Spring Boot 2.2.x 版本实现完整的优雅停机,保障服务在滚动更新、手动发布等场景下的数据安全和业务连续性。