Java 微服务优雅停机:从踩坑到最佳实践
服务在接收到停止信号后,不立即强制退出,而是先完成正在处理的请求/任务,再有序释放资源,最后退出进程。方式命令行为风险强制停机内核直接终止进程,JVM 无感知数据丢失、状态不一致优雅停机JVM 捕获 SIGTERM,触发 ShutdownHook可控,推荐kill -9是 SIGKILL,操作系统层面强杀,任何代码钩子都无法拦截。生产环境禁止使用。优雅停机的核心是三层协同容器层,拒绝新请求,等待存
Java 微服务优雅停机:从踩坑到最佳实践
博客标签:
JavaSpring Boot微服务优雅停机Kafka
阅读时长:约 10 分钟
一、从一次半夜告警说起
凌晨两点,运维发来消息:
“刚刚发布了一个版本,线上有几十条订单状态异常,用户投诉了。”
排查之后发现,问题出在服务重启的那 3 秒——Kafka 消费者正在处理一批消息,kill 命令一下去,JVM 直接退出,消息处理到一半,数据库写了一半,状态就永远停在了中间态。
这就是没有优雅停机的代价。
二、什么是优雅停机?
优雅停机(Graceful Shutdown)是指:服务在接收到停止信号后,不立即强制退出,而是先完成正在处理的请求/任务,再有序释放资源,最后退出进程。
对比两种停机方式:
| 方式 | 命令 | 行为 | 风险 |
|---|---|---|---|
| 强制停机 | kill -9 <pid> |
内核直接终止进程,JVM 无感知 | 数据丢失、状态不一致 |
| 优雅停机 | kill -15 <pid> |
JVM 捕获 SIGTERM,触发 ShutdownHook | 可控,推荐 |
kill -9是 SIGKILL,操作系统层面强杀,任何代码钩子都无法拦截。生产环境禁止使用。
三、Spring Boot 优雅停机原理
Spring Boot 2.3+ 内置了优雅停机支持,核心是两个配置项:
server:
shutdown: graceful # 启用优雅停机(默认 immediate)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最大等待时间
触发流程如下:
kill -15
│
▼
JVM ShutdownHook
│
▼
Spring 发布 ContextClosedEvent
│
▼
各组件有序销毁(HTTP容器 → Bean → 连接池)
│
▼
JVM 退出
启用 server.shutdown=graceful 后,Tomcat/Undertow 会拒绝新请求,等待已有请求处理完毕,再关闭。
四、实战:三层资源的优雅关闭
生产环境中,一个微服务通常有三类需要优雅处理的资源:
4.1 HTTP 请求层
只需 YAML 配置,零代码:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
Spring Boot 会自动为 Tomcat/Undertow 注入 GracefulShutdown 回调,停机时不再接收新请求,等待存量请求完成。
4.2 线程池层
业务线程池需要主动等待任务完成,否则线程池一关,正在跑的任务直接中断:
@Configuration
public class ThreadPoolConfig {
@Bean(name = "bizExecutor")
public ThreadPoolTaskExecutor bizExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("biz-thread-");
// 关键:停机时等待任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(20); // 最多等 20s
return executor;
}
}
参数约束:
awaitTerminationSeconds(20s)<timeout-per-shutdown-phase(30s),保证线程池有足够时间收尾。
4.3 Kafka 消费者层
Kafka 消费者是最容易踩坑的地方。以 @KafkaListener 为例:
问题:Spring Kafka 的消费者在收到停机信号后,会调用 consumer.wakeup() 中断 poll(),但正在业务线程池里处理的消息不会等待。
解决方案:消费者业务线程池同样使用 Spring Bean 管理,让 Spring 容器负责生命周期:
@Configuration
public class ConsumerThreadPoolConfig {
@Bean(name = "kafkaBizExecutor")
public ThreadPoolTaskExecutor kafkaBizExecutor(
@Value("${kafka.consumer.thread-pool-size:50}") int poolSize) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(poolSize);
executor.setMaxPoolSize(poolSize);
executor.setQueueCapacity(0); // SynchronousQueue 语义,防止积压
executor.setThreadNamePrefix("kafka-biz-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// Spring 停机时自动 shutdown 并等待
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(20);
return executor;
}
}
消费者注入并使用:
@Component
@RequiredArgsConstructor
public class OrderConsumer {
private final ThreadPoolTaskExecutor kafkaBizExecutor;
@KafkaListener(topics = "order-topic", groupId = "order-group")
public void onMessage(ConsumerRecord<String, String> record) {
// 提交到业务线程池异步处理
kafkaBizExecutor.submit(() -> handleOrder(record));
}
private void handleOrder(ConsumerRecord<String, String> record) {
// 真正的业务逻辑
log.info("处理订单消息: {}", record.value());
}
}
五、微服务注册中心配合:Nacos 反注册
在微服务体系中,停机还需要配合注册中心下线,否则上游服务还会把请求打过来:
@Component
@Slf4j
public class GracefulShutdownHandler implements ApplicationListener<ContextClosedEvent> {
private final NacosAutoServiceRegistration nacosRegistration;
private final AtomicBoolean stopped = new AtomicBoolean(false);
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 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 30s?
Ribbon 客户端的服务列表缓存默认 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 反注册 | 见 §5 |
Spring Boot 2.2 不支持 server.shutdown=graceful |
该配置 2.3 才引入 | 自实现 ApplicationListener<ContextClosedEvent> |
八、总结
优雅停机的核心是三层协同:
- 容器层(HTTP):
server.shutdown=graceful,拒绝新请求,等待存量完成 - 线程池层:
setWaitForTasksToCompleteOnShutdown=true,等待异步任务完成 - 注册中心层:主动 Nacos 反注册 + sleep,给上游感知时间
做到这三点,就能把停机风险从"数据损坏"降到"业务无感知"。
本文示例基于 Spring Boot 2.3+ + Spring Cloud Alibaba + Nacos,完整代码已在生产环境验证。
如果本文对你有帮助,欢迎点赞收藏!有问题欢迎评论区交流。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)