一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

微服务架构Spring Boot 2.2.x优雅停机的完整方案

时间:2026-08-07 09:54:57 编辑:袖梨 来源:一聚教程网

微服务架构Spring Boot 2.2.x优雅停机的完整方案需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。

前言

在微服务架构中,优雅停机是保障服务可靠性的重要一环。本文分享在 Spring Boot 2.2.x 版本中实现优雅停机的完整方案,涵盖 Nacos 反注册、Kafka 消费者停止、线程池等待等核心场景。

微服务架构Spring Boot 2.2.x优雅停机的完整方案

一、背景与问题

1.1 为什么需要优雅停机?

当服务收到停止信号(如 K8s Pod 滚动更新、手动发布等)时,如果直接终止,可能导致:

  1. 在途请求丢失:HTTP 请求处理到一半,连接被强制关闭
  2. 消息重复消费:Kafka 消费者正在处理消息,offset 未提交就被终止
  3. 服务发现延迟:注册中心未及时摘除实例,流量仍路由到已停止节点
  4. 线程池任务中断:异步任务执行到一半被强制终止

1.2 Spring Boot 版本的限制

Spring Boot 2.3.0 才内置 server.shutdown=graceful 配置,自动启用 Web 容器优雅停机。

但很多存量项目仍运行在 2.2.x 版本,需要自行实现优雅停机逻辑。

二、整体方案设计

2.1 设计原则

原则说明
零侵入不修改现有业务代码,通过新增组件实现
有开关新功能可通过配置控制,默认关闭
有默认值配置项有合理默认值,不配置时行为与之前一致
统一入口所有停机逻辑收敛到一个 Handler,便于维护

2.2 停机顺序设计

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)

三、核心实现

3.1 GracefulShutdownHandler

监听 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");    }}

3.2 关键点解析

3.2.1 为什么监听 ContextClosedEvent?

事件触发时机是否适合
ContextClosedEventBean 销毁之前✅ 适合
PreDestroyBean 销毁时❌ 太晚,DataSource 可能已关闭
ShutdownHookJVM 退出时❌ 太晚,Spring 容器已销毁

ContextClosedEventdestroyBeans() 之前发布,此时所有 Bean 仍可用,可以安全执行反注册、等待等操作。

3.2.2 为什么需要 AtomicBoolean?

问题: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 兜底,只执行第一次(主上下文事件最先到达)。

3.2.3 为什么 sleep 30 秒?

  1. Ribbon 缓存刷新周期:默认 30 秒,上游服务最多 30 秒后感知下线
  2. 在途请求完成:给正在处理的 HTTP 请求留出完成时间
  3. 异步任务收尾@Async 任务、定时任务可继续执行

四、Kafka 消费者优雅停止

4.1 两种消费者模式

模式实现方式停止方法
注解式@KafkaListenerSpring 自动管理,无需额外处理
手动式new KafkaConsumer() + while 循环需要检查 RUNNING 标志

4.2 手动消费者实现

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;    }}

关键点

  1. poll() 超时设为 1 秒,RUNNING=false 后最多延迟 1 秒退出
  2. finally 块确保 consumer.close() 一定执行
  3. commitSync() 在关闭前提交 offset,避免消息重复

五、线程池优雅关闭

5.1 Spring 托管线程池

推荐使用 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 秒,超时强制中断

5.2 手动创建线程池

如果必须手动创建,需要在停机时显式调用 shutdown()

ExecutorService executor = new ThreadPoolExecutor(...);// 停机时executor.shutdown();try {    if (!executor.awaitTermination(20, TimeUnit.SECONDS)) {        executor.shutdownNow();    }} catch (InterruptedException e) {    executor.shutdownNow();}

六、HTTP 请求保护(可选)

6.1 Servlet Filter 计数

如果服务有较高并发 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);        }    }}

6.2 与 Handler 配合

// 在 GracefulShutdownHandler 中@Autowired(required = false)private GracefulShutdownFilter shutdownFilter;// 停机时if (shutdownFilter != null) {    shutdownFilter.awaitRequestsCompletion(5000);  // 等待在途请求完成}

七、验证方式

7.1 日志校验

# 检查停机日志grep "GracefulShutdownHandler" server.log# 期望输出:# [GracefulShutdownHandler] 收到 ContextClosedEvent,开始优雅停机...# 设置 RUNNING=false,通知消费者停止# Nacos 主动反注册...# Nacos 反注册完成# 等待 30 秒,让上游服务感知下线...# 优雅停机预处理完成,交由 Spring 继续销毁 Bean

7.2 触发次数校验

# 确保只触发一次(无 Feign 冒泡)grep -c "GracefulShutdownHandler.*收到" server.log# 期望:1

7.3 整体停机时长

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

九、总结

优雅停机的核心要点:

  1. 监听 ContextClosedEvent:在 Bean 销毁前执行预处理
  2. 主动反注册:让注册中心立即推送下线通知
  3. 设置运行标志:通知消费者停止 poll 循环
  4. 等待时间窗口:给上游服务感知下线的时间
  5. 线程池托管:让 Spring 自动管理线程池生命周期
  6. AtomicBoolean 防护:防止 Feign 子上下文事件冒泡

通过以上方案,可以在 Spring Boot 2.2.x 版本实现完整的优雅停机,保障服务在滚动更新、手动发布等场景下的数据安全和业务连续性。

参考资料

  1. Spring Boot Graceful Shutdown
  2. Spring Cloud Feign Context 生命周期
  3. Kafka Consumer 关闭最佳实践

热门栏目