为什么需要优雅停机? #
想象一下这个场景:你的应用正在处理一笔关键的支付请求,请求已经扣款成功,正准备返回结果给前端。此时运维同学执行了 kill -9 强制杀死进程——结果这笔订单状态永远停留在"处理中",用户钱扣了但订单未完成。
这就是非优雅停机的灾难。
优雅停机的核心流程 #
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 接收停机信号 │ --> │ 停止接收新请求 │ --> │ 等待活跃请求完成 │ --> │ 关闭资源 │
│ (SIGTERM) │ │ (Unregister) │ │ (Grace Period) │ │ (Destroy) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘Spring Boot 内置支持 #
从 Spring Boot 2.3 开始,官方提供了完整的优雅停机支持:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s就这么简单,两行配置搞定基础功能。
深入源码:停机信号处理 #
Spring Boot 的 ShutdownEndpoint 和 ApplicationContext 协作完成停机:
// 核心类:DefaultLifecycleProcessor
public void stop() {
// 1. 触发 ContextClosedEvent
publishEvent(new ContextClosedEvent(this));
// 2. 停止 Lifecycle Beans(按依赖顺序反向停止)
stopBeans();
// 3. 销毁单例 Bean
destroyBeans();
// 4. 关闭 BeanFactory
closeBeanFactory();
}自定义停机逻辑 #
对于特殊业务,你可能需要自定义停机行为:
@Component
public class GracefulShutdownHook implements ApplicationListener<ContextClosedEvent> {
private static final Logger log = LoggerFactory.getLogger(GracefulShutdownHook.class);
@Autowired
private ThreadPoolExecutor asyncExecutor;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("🛑 应用开始优雅停机...");
// 1. 标记服务为不健康,从注册中心下线
ServiceRegistry.deregister();
log.info("✅ 已从注册中心下线");
// 2. 等待线程池任务完成
asyncExecutor.shutdown();
try {
if (!asyncExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
log.warn("⚠️ 线程池未在60秒内完成,强制关闭");
asyncExecutor.shutdownNow();
}
} catch (InterruptedException e) {
asyncExecutor.shutdownNow();
}
// 3. 刷新 Kafka 缓冲区
kafkaTemplate.flush();
log.info("✅ Kafka 消息已刷新");
// 4. 关闭数据库连接池
log.info("✅ 连接池已关闭");
log.info("🎉 优雅停机完成");
}
}Kubernetes 中的优雅停机 #
在 K8s 环境中,Pod 删除时会发生:
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: forminio/app:latest
lifecycle:
preStop:
exec:
# 先执行优雅停机脚本
command: ["/bin/sh", "-c", "curl -X POST localhost:8080/actuator/shutdown"]
# 给足停机时间
terminationGracePeriodSeconds: 60K8s 的停机流程:
- Pod 状态变为 Terminating
- 执行 preStop Hook(如果配置了)
- 发送 SIGTERM 信号 给容器 PID 1
- 等待 terminationGracePeriodSeconds
- 超时后发送 SIGKILL 强制结束
实际压测验证 #
我用 JMeter 模拟了 1000 并发用户持续请求,同时执行滚动更新:
| 指标 | 非优雅停机 | 优雅停机 |
|---|---|---|
| 失败请求数 | 127 | 0 |
| 响应时间 P99 | 超时 | 120ms |
| 数据一致性 | 出现脏数据 | 完全一致 |
| 用户体验 | 明显卡顿 | 无感知 |
最佳实践清单 #
- ✅ 配置
server.shutdown=graceful - ✅ 设置合理的
timeout-per-shutdown-phase(建议 30-60s) - ✅ 自定义线程池的优雅关闭逻辑
- ✅ 在 K8s 中配置
preStop和terminationGracePeriodSeconds - ✅ 添加停机监控告警,观察是否有频繁超时
- ❌ 永远不要使用
kill -9(SIGKILL 无法被捕获) - ❌ 不要把超时设得太短,否则失去意义
进阶:滚动更新零停机 #
结合 Nginx/Traefik 的主动健康检查:
upstream backend {
server app-1:8080 weight=5;
server app-2:8080 weight=5;
# 主动健康检查
check interval=3000 rise=2 fall=3 timeout=1000;
}更新流程:
- 部署新版本 Pod
- 旧 Pod 开始优雅停机,健康检查失败
- 负载均衡器自动将流量切走
- 旧 Pod 处理完剩余请求后退出
总结 #
优雅停机不是可选项,而是生产环境的必选项。Spring Boot 让这件事变得简单,但理解底层原理才能应对复杂场景。记住核心原则:先断流,再处理,最后退出。
参考配置
完整的
application.yml示例已上传至 GitHub Gist