如何实现优雅停机?
总结
优雅停机(Graceful Shutdown)= ** “拒接新客 + 善待老客 + 妥善交接” **。
它要求 JVM 进程在接收到外部关闭指令时,不再接收任何新的外部请求,
但必须保证当前正在线程池中处理的业务逻辑执行完毕,
并安全释放底层资源(如数据库长连接、文件句柄、自定义线程池、MQ 消费者等)后,再平滑退出进程,从而彻底避免业务数据丢失或事务意外中断。
详细原理
优雅停机的本质,是一套从操作系统底层一路贯穿到应用业务框架的自上而下的联动机制:
1. OS 信号层(触发源点):
运维脚本或容器编排系统(如 Kubernetes)下发终止指令时,必须使用 kill -15 (SIGTERM),这代表操作系统礼貌地请求进程自行终止。绝对不能使用 kill -9 (SIGKILL),后者是内核级的强制抹杀,JVM 会瞬间暴毙,任何善后代码都无法执行。
2. JVM 钩子层(桥梁):
JVM 底层提供了一个核心 API:Runtime.getRuntime().addShutdownHook(Thread hook)。当 JVM 捕获到 OS 发来的 SIGTERM 信号时,它会挂起默认的退出流程,转而并发启动所有提前注册好的 ShutdownHook(关闭钩子)线程。
3. Web 容器层(流量阻断,如 Tomcat):
自 Spring Boot 2.3 开始,原生支持了 Web 容器的优雅停机。当钩子被触发时,内嵌的 Tomcat 会立即切断源头——停止在操作系统网络层(Socket)接收新的连接。同时,它会给内部的 Worker 线程池设定一个“缓冲期”(默认 30 秒),耐心等待那些正在执行中的 HTTP 请求处理完毕并返回响应。
4. Spring 容器层(资源销毁):
等 Web 层的流量完全处理完(或超时)后,Spring Boot 开始自毁 IoC 容器(调用 AbstractApplicationContext.doClose())。它会按依赖关系的逆序,依次调用所有单例 Bean 的销毁逻辑(即执行 @PreDestroy 注解的方法,或触发 DisposableBean 接口的 destroy() 方法),在此阶段切断数据库连接池(如 HikariCP)并注销注册中心(如 Nacos)的实例节点。
实际的例子
假设你负责研发一个处理核心支付交易的 Spring Boot 订单服务。
场景 1:原生流量的优雅停机(只需配置)
在 application.yml 中开启 Spring Boot 的原生机制:
server:
shutdown: graceful # 开启 Web 容器优雅停机(替代默认的 immediate 直接断开)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 设定最大等待缓冲期为 30 秒
有了这两行配置,当你执行 kill -15 重启服务时,如果某个支付请求刚执行到一半,Tomcat 会保住这个线程,直到支付成功返回前端,才会放任 JVM 退出。
场景 2:自定义后台资源的优雅停机(必须手写)
如果你在支付服务里自己 new 了一个异步线程池用来异步发送发票邮件,Spring 的 Web 优雅停机是管不到这个自定义线程池的。你必须在对应的 Bean 中手动收尾:
@Service
public class InvoiceService implements DisposableBean {
// 自定义异步发票处理线程池
private final ExecutorService executor = Executors.newFixedThreadPool(10);
@Override
public void destroy() throws Exception {
// 1. 停止接收新的发票任务
executor.shutdown();
// 2. 阻塞主线程,最多等 20 秒,让队列里积压的老邮件发送完毕
if (!executor.awaitTermination(20, TimeUnit.SECONDS)) {
// 3. 实在处理不完(可能外部邮件服务器卡死了),强制终止,并记录严重告警日志以便人工补偿
executor.shutdownNow();
log.error("Invoice thread pool forced to shutdown, potential data loss!");
}
}
}
场景 3:基础设施的硬性约束(面试避坑点)
如果你将这个服务部署在 Kubernetes 中,K8s 的 Pod 有一个 terminationGracePeriodSeconds 属性(默认也是 30 秒)。K8s 发送 kill -15 后开始倒计时,如果 30 秒到了你的 JVM 还没退出,K8s 就会无情地下发 kill -9 强杀。因此,**K8s 的宽限期必须大于 Spring Boot 配置的 timeout-per-shutdown-phase**,否则你的优雅停机逻辑就会在半路被强制打断。

浙公网安备 33010602011771号