大家好,我是苏三,又跟大家见面了。前言最近星球中有位小伙伴问了我一个问题:如何优雅的停机?
我觉得这个问题挺有代表性的。
今天这篇文章跟大家一下优雅停机的一些常见方案,希望对你会有所帮助。
1.什么是优雅停机?优雅停机(Graceful Shutdown) 指在服务终止前,系统能:
拒绝新请求进入完成存量请求处理释放所有资源通知上下游服务非优雅停机的惨痛代价:
真实案例:支付回调丢失。
代码语言:javascript代码运行次数:0运行复制// 支付回调处理
@PostMapping("/callback")
public void handleCallback(Payment payment) {
// 1. 更新订单状态
orderService.updateStatus(payment.getOrderId(), PAID);
// 2. 发放权益(kill发生时此处未执行)
benefitService.grantVip(payment.getUserId());
}
当kill发生在步骤1和2之间时,导致订单状态已更新但权益未发放,引发用户投诉。
2.优雅停机三大核心流程2.1 信号捕获层2.2 流量控制层2.3 资源释放层3.Spring Boot优雅停机的实现3.1 基础配置在SpringBoot项目的application.yml文件中增加如下配置:
代码语言:javascript代码运行次数:0运行复制server:
shutdown: graceful # 开启优雅停机
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最长等待时间
3.2 线程池优雅关闭在线程池中实现优雅关闭功能:
代码语言:javascript代码运行次数:0运行复制@Bean
public ExecutorService threadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setWaitForTasksToCompleteOnShutdown(true); // 等待任务完成
executor.setAwaitTerminationSeconds(60); // 最大等待时间
return executor.getThreadPoolExecutor();
}
在shutdown之前,先等待任务完成。
3.3 分布式锁释放拦截器代码语言:javascript代码运行次数:0运行复制@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object handleRequest(ProceedingJoinPoint pjp) {
Lock lock = redisson.getLock("order_lock");
try {
lock.lock();
return pjp.proceed();
} finally {
if (!isShuttingDown()) {
lock.unlock(); // 非停机时正常释放
}
// 停机时由锁管理器统一释放
}
}
使用统一的拦截器释放分布式锁,防止出现异常有释放遗漏的地方。
4.Kubernetes环境下的优雅停机4.1 关键配置代码语言:javascript代码运行次数:0运行复制STOPSIGNAL SIGTERM # 使用SIGTERM替代SIGKILL
代码语言:javascript代码运行次数:0运行复制# Deployment配置
spec:
terminationGracePeriodSeconds:60# 宽限期
containers:
-lifecycle:
preStop:
exec:
command:["/bin/sh","-c","sleep 20;"]# 预留缓冲时间
在部署配置中增加预留缓冲时间。
4.2 就绪探针自动摘流5.中间件连接优雅关闭5.1 数据库连接池代码语言:javascript代码运行次数:0运行复制@PreDestroy
public void close() {
HikariPool pool = dataSource.getHikariPoolMXBean();
pool.suspendPool(); // 停止借出连接
pool.softEvictConnections(); // 驱逐空闲连接
while (pool.getActiveConnections() > 0) {
Thread.sleep(500); // 等待活动连接完成
}
pool.shutdown(); // 彻底关闭
}
使用@PreDestroy在服务销毁之前关闭数据库连接池。
5.2 RabbitMQ消费者代码语言:javascript代码运行次数:0运行复制@PreDestroy
public void stop() {
channel.basicCancel(consumerTag); // 取消订阅
while (unackedMessages.get() > 0) {
Thread.sleep(100); // 等待ACK完成
}
connection.close();
}
@PreDestroy在服务销毁之前取消订阅,需要先等待ACK完成。
3. Redis分布式锁代码语言:javascript代码运行次数:0运行复制public class LockManager implements DisposableBean {
@Override
public void destroy() {
lockMap.forEach((key, lock) -> {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 强制释放未解锁的锁
}
});
}
}
实现DisposableBean接口,在服务销毁之前强制释放未解锁的锁。
6.全链路优雅停机6.1 停机事件传播机制6.2 状态机管理代码语言:javascript代码运行次数:0运行复制public enum ShutdownState {
RUNNING, // 正常运行
PRE_SHUTDOWN, // 拒绝新请求
DRAINING, // 排空存量请求
TERMINATED // 完全终止
}
6.3 停机监控面板7.生产环境避坑指南7.1 必须避免的四大陷阱陷阱
后果
解决方案
死锁等待
无法完成停机
设置锁超时时间
第三方服务不可用
资源无法释放
添加熔断机制
长周期任务
超过宽限期被强杀
拆分任务+保存中间状态
文件写入未完成
数据损坏
使用原子文件替换
7.2 停机检查清单代码语言:javascript代码运行次数:0运行复制# 停机前执行
curl -X POST http://localhost:8080/actuator/shutdown-prepare
# 验证项:
1. 新请求返回503
2. 活动线程数持续下降
3. 数据库连接数归零
4. MQ无未ACK消息
7.3 黄金法则:二段式停机总结基础层:处理HTTP请求
Spring Boot Graceful Shutdown + 线程池等待进阶层:管理中间件连接
数据库连接池排空 + MQ消费者取消订阅高级层:分布式协同
停机事件广播 + 分布式锁释放终极层:全链路状态管理
停机状态机 + 智能超时控制停机策略对比表
策略
实现难度
停机时间
数据安全
适用场景
直接kill -9
☆
秒级
极低
开发环境
Spring Boot
☆☆
10-30s
中
常规Web应用
容器化方案
☆☆☆
可配置
高
K8S环境
全链路管理
☆☆☆☆
分钟级
极高
金融核心系统