1. 为什么我们需要优雅停机
在传统的应用停机过程中,直接kill进程的方式就像突然拔掉电器的电源插头——所有正在进行的操作都会戛然而止。想象一下数据库事务执行到一半、文件上传到99%、支付流程正在验证时的强制中断,这会导致数据不一致、资源泄漏等一系列问题。
SpringBoot优雅停机的核心目标就是让应用像飞机降落一样平稳:先停止接收新请求(关闭舱门),完成已有请求处理(让乘客有序下机),释放资源(清理机舱),最后安全关闭引擎。整个过程需要解决三个关键问题:
- 如何感知停机信号(飞行员收到塔台指令)
- 如何阻止新请求进入(关闭值机柜台)
- 如何等待进行中的请求完成(确保所有乘客下机)
在Kubernetes等容器化环境中,这个问题尤为突出。当Pod需要终止时,Kubernetes会先发送SIGTERM信号,等待30秒(默认值)后强制杀死进程。如果应用没有正确处理这个信号,就会导致请求中断。
2. SpringBoot优雅停机实现原理
2.1 信号处理机制
当JVM接收到SIGTERM信号时,会触发Shutdown Hook线程执行。SpringBoot通过注册SmartLifecycle接口的实现类,在这些Hook中按顺序执行关闭逻辑。关键组件包括:
java复制// 简化的生命周期管理逻辑
public class GracefulShutdown implements SmartLifecycle {
private volatile boolean running;
@Override
public void stop(Runnable callback) {
// 1. 停止接收新请求
server.stopAcceptingRequests();
// 2. 等待现有请求完成
server.awaitTermination(30, TimeUnit.SECONDS);
// 3. 执行回调通知Spring继续后续关闭
callback.run();
running = false;
}
}
2.2 嵌入式服务器适配
不同Web服务器需要不同的处理策略:
| 服务器类型 | 优雅停机实现类 | 核心方法 |
|---|---|---|
| Tomcat | GracefulShutdown | pauseEndpoint + setGraceful |
| Jetty | GracefulShutdownHandler | setStopTimeout + setShutdown |
| Undertow | GracefulShutdownHandler | shutdown + awaitShutdown |
以Tomcat为例,其核心配置参数包括:
properties复制server.shutdown=graceful # 启用优雅停机
spring.lifecycle.timeout-per-shutdown-phase=30s # 等待超时时间
2.3 请求拦截与资源释放
优雅停机期间需要特别处理的资源类型:
- HTTP长连接:通过Connection: close头主动关闭
- WebSocket会话:发送关闭帧并等待确认
- 数据库连接池:归还连接并终止长时间查询
- 线程池任务:记录未完成任务以便恢复
重要提示:Spring Boot 2.3+版本内置了优雅停机支持,旧版本需要自行实现SmartLifecycle
3. 生产环境最佳实践
3.1 配置优化方案
在application.yml中推荐这样配置:
yaml复制server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
task:
execution:
shutdown:
await-termination: true
await-termination-period: 10s
关键参数说明:
timeout-per-shutdown-phase:必须小于K8s的terminationGracePeriodSecondsawait-termination-period:应该大于平均请求处理时间
3.2 Kubernetes集成策略
当部署在K8s中时,需要配置这些参数:
yaml复制apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 40 # 比Spring超时多10秒缓冲
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"] # 给服务发现更新留时间
3.3 监控与验证方案
验证优雅停机是否生效的方法:
- 发送请求到慢接口(如sleep 20s的接口)
- 在请求处理期间发送kill命令
- 观察日志是否显示等待请求完成
关键日志检查点:
code复制2023-07-20 14:00:00 | Received shutdown signal
2023-07-20 14:00:00 | Pausing request processing
2023-07-20 14:00:20 | Completed in-flight requests: 3/3
2023-07-20 14:00:20 | Closed all resources
4. 常见问题与解决方案
4.1 停机超时问题
现象:日志显示"Timeout during shutdown phase"
解决方案:
- 分析线程转储找出卡住的线程:
bash复制jstack <pid> > thread.dump
- 检查是否有以下情况:
- 数据库死锁
- 同步锁未释放
- 外部服务调用无超时
4.2 请求丢失问题
现象:负载均衡器在停机期间仍转发请求
解决方案:
- 添加preStop钩子延迟注册中心下线:
yaml复制lifecycle:
preStop:
exec:
command: ["sleep", "10"]
- 配置健康检查快速失效:
properties复制management.endpoint.health.probes.enabled=true
management.health.livenessState.enabled=true
4.3 资源泄漏问题
排查清单:
- 检查连接池状态:
sql复制SELECT * FROM pg_stat_activity WHERE usename='appuser';
- 检查文件描述符:
bash复制lsof -p <pid>
- 检查线程泄漏:
java复制ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
threadMXBean.dumpAllThreads(true, true);
5. 高级定制方案
对于特殊场景,可以扩展默认行为:
5.1 自定义停机处理器
java复制@Component
public class CustomShutdownHandler implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 1. 保存未完成任务状态
TaskRepository.savePendingTasks();
// 2. 通知上下游服务
EventPublisher.publishShutdownEvent();
// 3. 等待异步处理完成
AsyncUtil.awaitTermination(10, TimeUnit.SECONDS);
}
}
5.2 分批关闭微服务
在分布式系统中建议采用的分阶段关闭策略:
- 先关闭读服务(允许5分钟下线窗口)
- 再关闭写服务(确保数据一致性)
- 最后关闭基础设施(如消息队列消费者)
5.3 停机时状态保存
对于需要恢复的场景,可以实现状态快照:
java复制public class StateSnapshotter implements SmartLifecycle {
@Override
public void stop() {
// 将内存状态持久化到数据库
StateExporter.exportToDatabase();
// 记录最后处理的ID
LastProcessedId.saveToFile();
}
}
在实际生产环境中,我们遇到过因未正确处理JMS连接关闭导致的资源泄漏。后来通过在停机阶段主动关闭所有ConnectionFactory解决了问题。关键是要确保每个外部资源都有明确的关闭路径,并为每个关闭操作设置合理的超时时间。