Java 微服务优雅停机:从踩坑到最佳实践
Java 微服务优雅停机从踩坑到最佳实践博客标签JavaSpring Boot微服务优雅停机Kafka阅读时长约 10 分钟一、从一次半夜告警说起凌晨两点运维发来消息“刚刚发布了一个版本线上有几十条订单状态异常用户投诉了。”排查之后发现问题出在服务重启的那 3 秒——Kafka 消费者正在处理一批消息kill命令一下去JVM 直接退出消息处理到一半数据库写了一半状态就永远停在了中间态。这就是没有优雅停机的代价。二、什么是优雅停机优雅停机Graceful Shutdown是指服务在接收到停止信号后不立即强制退出而是先完成正在处理的请求/任务再有序释放资源最后退出进程。对比两种停机方式方式命令行为风险强制停机kill -9 pid内核直接终止进程JVM 无感知数据丢失、状态不一致优雅停机kill -15 pidJVM 捕获 SIGTERM触发 ShutdownHook可控推荐kill -9是 SIGKILL操作系统层面强杀任何代码钩子都无法拦截。生产环境禁止使用。三、Spring Boot 优雅停机原理Spring Boot 2.3 内置了优雅停机支持核心是两个配置项server:shutdown:graceful# 启用优雅停机默认 immediatespring:lifecycle:timeout-per-shutdown-phase:30s# 最大等待时间触发流程如下kill -15 │ ▼ JVM ShutdownHook │ ▼ Spring 发布 ContextClosedEvent │ ▼ 各组件有序销毁HTTP容器 → Bean → 连接池 │ ▼ JVM 退出启用server.shutdowngraceful后Tomcat/Undertow 会拒绝新请求等待已有请求处理完毕再关闭。四、实战三层资源的优雅关闭生产环境中一个微服务通常有三类需要优雅处理的资源4.1 HTTP 请求层只需 YAML 配置零代码server:shutdown:gracefulspring:lifecycle:timeout-per-shutdown-phase:30sSpring Boot 会自动为 Tomcat/Undertow 注入GracefulShutdown回调停机时不再接收新请求等待存量请求完成。4.2 线程池层业务线程池需要主动等待任务完成否则线程池一关正在跑的任务直接中断ConfigurationpublicclassThreadPoolConfig{Bean(namebizExecutor)publicThreadPoolTaskExecutorbizExecutor(){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(20);executor.setMaxPoolSize(50);executor.setQueueCapacity(200);executor.setThreadNamePrefix(biz-thread-);// 关键停机时等待任务完成executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(20);// 最多等 20sreturnexecutor;}}参数约束awaitTerminationSeconds20stimeout-per-shutdown-phase30s保证线程池有足够时间收尾。4.3 Kafka 消费者层Kafka 消费者是最容易踩坑的地方。以KafkaListener为例问题Spring Kafka 的消费者在收到停机信号后会调用consumer.wakeup()中断poll()但正在业务线程池里处理的消息不会等待。解决方案消费者业务线程池同样使用 Spring Bean 管理让 Spring 容器负责生命周期ConfigurationpublicclassConsumerThreadPoolConfig{Bean(namekafkaBizExecutor)publicThreadPoolTaskExecutorkafkaBizExecutor(Value(${kafka.consumer.thread-pool-size:50})intpoolSize){ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor.setCorePoolSize(poolSize);executor.setMaxPoolSize(poolSize);executor.setQueueCapacity(0);// SynchronousQueue 语义防止积压executor.setThreadNamePrefix(kafka-biz-);executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());// Spring 停机时自动 shutdown 并等待executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(20);returnexecutor;}}消费者注入并使用ComponentRequiredArgsConstructorpublicclassOrderConsumer{privatefinalThreadPoolTaskExecutorkafkaBizExecutor;KafkaListener(topicsorder-topic,groupIdorder-group)publicvoidonMessage(ConsumerRecordString,Stringrecord){// 提交到业务线程池异步处理kafkaBizExecutor.submit(()-handleOrder(record));}privatevoidhandleOrder(ConsumerRecordString,Stringrecord){// 真正的业务逻辑log.info(处理订单消息: {},record.value());}}五、微服务注册中心配合Nacos 反注册在微服务体系中停机还需要配合注册中心下线否则上游服务还会把请求打过来ComponentSlf4jpublicclassGracefulShutdownHandlerimplementsApplicationListenerContextClosedEvent{privatefinalNacosAutoServiceRegistrationnacosRegistration;privatefinalAtomicBooleanstoppednewAtomicBoolean(false);OverridepublicvoidonApplicationEvent(ContextClosedEventevent){// AtomicBoolean 防止 Feign 子上下文事件冒泡重复触发if(!stopped.compareAndSet(false,true)){return;}log.info([优雅停机] 开始执行...);// 第一步Nacos 取消注册上游立即感知下线if(nacosRegistration.isRunning()){nacosRegistration.destroy();log.info([优雅停机] Nacos 反注册完成);}// 第二步等待上游 Ribbon 缓存刷新通常 30s 周期log.info([优雅停机] 等待 30s让上游感知下线...);ThreadUtil.sleep(30_000);log.info([优雅停机] 等待结束进入 Bean 销毁阶段);}}为什么要 sleep 30sRibbon 客户端的服务列表缓存默认 30s 刷新一次。Nacos 反注册后上游不会立刻知道你下线了这 30s 的等待是给上游缓存过期的时间窗口。六、完整停机时序kill -15 │ ▼ [GracefulShutdownHandler] 监听 ContextClosedEvent ├─ ① Nacos 反注册上游秒级感知 └─ ② sleep(30s)等待上游缓存刷新 │ ▼ [Spring 自动销毁 Bean] ├─ kafkaBizExecutor.shutdown()等待 ≤ 20s ├─ bizExecutor.shutdown()等待 ≤ 20s ├─ DataSource 连接池关闭 └─ Redis 连接池关闭 │ ▼ JVM 退出全程约 30s Bean 销毁时间七、常见踩坑汇总坑原因解决方案kill -9无效SIGKILL 绕过 JVM改用kill -15停机日志打印 N 遍Feign 子上下文ContextClosedEvent冒泡用AtomicBoolean.compareAndSet防重线程池不等待忘记设置setWaitForTasksToCompleteOnShutdown(true)见 §4.2上游仍打流量未做 Nacos 反注册见 §5Spring Boot 2.2 不支持server.shutdowngraceful该配置 2.3 才引入自实现ApplicationListenerContextClosedEvent八、总结优雅停机的核心是三层协同容器层HTTPserver.shutdowngraceful拒绝新请求等待存量完成线程池层setWaitForTasksToCompleteOnShutdowntrue等待异步任务完成注册中心层主动 Nacos 反注册 sleep给上游感知时间做到这三点就能把停机风险从数据损坏降到业务无感知。本文示例基于 Spring Boot 2.3 Spring Cloud Alibaba Nacos完整代码已在生产环境验证。如果本文对你有帮助欢迎点赞收藏有问题欢迎评论区交流。