前言
在微服务架构中,优雅停机是保障服务可靠性的重要一环。本文分享在 spring boot 2.2.x 版本中实现优雅停机的完整方案,涵盖 nacos 反注册、kafka 消费者停止、线程池等待等核心场景。
一、背景与问题
1.1 为什么需要优雅停机?
当服务收到停止信号(如 k8s pod 滚动更新、手动发布等)时,如果直接终止,可能导致:
- 在途请求丢失:http 请求处理到一半,连接被强制关闭
- 消息重复消费:kafka 消费者正在处理消息,offset 未提交就被终止
- 服务发现延迟:注册中心未及时摘除实例,流量仍路由到已停止节点
- 线程池任务中断:异步任务执行到一半被强制终止
1.2 spring boot 版本的限制
spring boot 2.3.0 才内置 server.shutdown=graceful 配置,自动启用 web 容器优雅停机。
但很多存量项目仍运行在 2.2.x 版本,需要自行实现优雅停机逻辑。
二、整体方案设计
2.1 设计原则
| 原则 | 说明 |
|---|---|
| 零侵入 | 不修改现有业务代码,通过新增组件实现 |
| 有开关 | 新功能可通过配置控制,默认关闭 |
| 有默认值 | 配置项有合理默认值,不配置时行为与之前一致 |
| 统一入口 | 所有停机逻辑收敛到一个 handler,便于维护 |
2.2 停机顺序设计
kill -15 <pid> │ ▼ jvm 触发 springcontextshutdownhook │ ▼ 发布 contextclosedevent(早于 bean 销毁) │ ├─ [gracefulshutdownhandler 监听 ★] │ ① 设置运行标志为 false(通知消费者停止 poll) │ ② 主动调用注册中心反注册(下游秒级感知) │ ③ sleep(n秒)(等待上游服务感知下线) │ 期间 http 请求、异步任务、远程调用可继续完成 │ ▼ spring 依次销毁 bean ├─ kafkaconsumer.close() ├─ threadpoolexecutor.shutdown()(等待任务完成) ├─ datasource 连接池关闭 └─ redis 连接池关闭 │ ▼ web 容器 stopping │ ▼ jvm 退出(exit code 0)
三、核心实现
3.1 gracefulshutdownhandler
监听 contextclosedevent,在 spring 销毁 bean 之前执行停机前处理:
@slf4j
@component
public class gracefulshutdownhandler implements applicationlistener<contextclosedevent> {
private final atomicboolean executed = new atomicboolean(false);
@autowired(required = false)
private nacosautoserviceregistration nacosautoserviceregistration;
@override
public void onapplicationevent(contextclosedevent event) {
// 防止 feign 子上下文事件冒泡导致重复执行
if (!executed.compareandset(false, true)) {
log.info("[gracefulshutdownhandler] 重复事件,跳过。来源={}",
event.getapplicationcontext().getdisplayname());
return;
}
log.info("[gracefulshutdownhandler] 收到 contextclosedevent,开始优雅停机...");
// 1. 设置全局运行标志
systemconfig.running = false;
log.info("设置 running=false,通知消费者停止");
// 2. 主动 nacos 反注册
if (nacosautoserviceregistration != null) {
log.info("nacos 主动反注册...");
nacosautoserviceregistration.destroy();
log.info("nacos 反注册完成");
}
// 3. 等待上游服务感知(ribbon 缓存刷新周期约 30s)
int waitseconds = 30;
log.info("等待 {} 秒,让上游服务感知下线...", waitseconds);
threadutil.sleep(waitseconds * 1000l);
log.info("优雅停机预处理完成,交由 spring 继续销毁 bean");
}
}
3.2 关键点解析
3.2.1 为什么监听 contextclosedevent?
| 事件 | 触发时机 | 是否适合 |
|---|---|---|
contextclosedevent | bean 销毁之前 | ✅ 适合 |
predestroy | bean 销毁时 | ❌ 太晚,datasource 可能已关闭 |
shutdownhook | jvm 退出时 | ❌ 太晚,spring 容器已销毁 |
contextclosedevent 在 destroybeans() 之前发布,此时所有 bean 仍可用,可以安全执行反注册、等待等操作。
3.2.2 为什么需要 atomicboolean?
问题:spring cloud 为每个 feign client 创建独立子 applicationcontext。主上下文销毁子上下文时,每个子上下文都会发布 contextclosedevent 并冒泡到父上下文,导致 handler 被触发 n 次。
jvm sigterm
└─ 主 context.doclose()
├─ publishevent(contextclosedevent) ← handler 第 1 次触发
└─ destroybeans()
└─ feigncontext.destroy()
├─ 关闭子 ctx a → 事件冒泡 → handler 第 2 次
├─ 关闭子 ctx b → 事件冒泡 → handler 第 3 次
└─ ...
解决:使用 atomicboolean 兜底,只执行第一次(主上下文事件最先到达)。
3.2.3 为什么 sleep 30 秒?
- ribbon 缓存刷新周期:默认 30 秒,上游服务最多 30 秒后感知下线
- 在途请求完成:给正在处理的 http 请求留出完成时间
- 异步任务收尾:
@async任务、定时任务可继续执行
四、kafka 消费者优雅停止
4.1 两种消费者模式
| 模式 | 实现方式 | 停止方法 |
|---|---|---|
| 注解式 | @kafkalistener | spring 自动管理,无需额外处理 |
| 手动式 | new kafkaconsumer() + while 循环 | 需要检查 running 标志 |
4.2 手动消费者实现
public class manualkafkaconsumer implements runnable {
private final kafkaconsumer<string, string> consumer;
private volatile boolean running = true;
@override
public void run() {
try {
while (running && systemconfig.running) {
consumerrecords<string, string> records = consumer.poll(duration.ofseconds(1));
// 处理消息...
consumer.commitsync();
}
} finally {
consumer.close();
log.info("kafkaconsumer 已关闭");
}
}
public void shutdown() {
this.running = false;
}
}
关键点:
poll()超时设为 1 秒,running=false后最多延迟 1 秒退出finally块确保consumer.close()一定执行commitsync()在关闭前提交 offset,避免消息重复
五、线程池优雅关闭
5.1 spring 托管线程池
推荐使用 spring 的 threadpooltaskexecutor,自动管理生命周期:
@configuration
public class threadpoolconfig {
@bean("businessexecutor")
public threadpooltaskexecutor businessexecutor() {
threadpooltaskexecutor executor = new threadpooltaskexecutor();
executor.setcorepoolsize(10);
executor.setmaxpoolsize(100);
executor.setqueuecapacity(1000);
executor.setthreadnameprefix("business-");
// 关键配置:等待任务完成
executor.setwaitfortaskstocompleteonshutdown(true);
executor.setawaitterminationseconds(20); // 最多等待 20 秒
executor.initialize();
return executor;
}
}
| 配置项 | 说明 |
|---|---|
setwaitfortaskstocompleteonshutdown(true) | 停机时等待已提交任务完成 |
setawaitterminationseconds(20) | 最多等待 20 秒,超时强制中断 |
5.2 手动创建线程池
如果必须手动创建,需要在停机时显式调用 shutdown():
executorservice executor = new threadpoolexecutor(...);
// 停机时
executor.shutdown();
try {
if (!executor.awaittermination(20, timeunit.seconds)) {
executor.shutdownnow();
}
} catch (interruptedexception e) {
executor.shutdownnow();
}
六、http 请求保护(可选)
6.1 servlet filter 计数
如果服务有较高并发 http 流量,可以通过 filter 精确计数在途请求:
@component
public class gracefulshutdownfilter implements filter {
private final atomicinteger activerequests = new atomicinteger(0);
private volatile boolean shuttingdown = false;
@override
public void dofilter(servletrequest request, servletresponse response, filterchain chain)
throws ioexception, servletexception {
if (shuttingdown) {
// 可选:拒绝新请求,返回 503
response.senderror(httpservletresponse.sc_service_unavailable, "service is shutting down");
return;
}
activerequests.incrementandget();
try {
chain.dofilter(request, response);
} finally {
activerequests.decrementandget();
}
}
public void awaitrequestscompletion(long timeoutms) {
this.shuttingdown = true;
long deadline = system.currenttimemillis() + timeoutms;
while (activerequests.get() > 0 && system.currenttimemillis() < deadline) {
threadutil.sleep(100);
}
}
}
6.2 与 handler 配合
// 在 gracefulshutdownhandler 中
@autowired(required = false)
private gracefulshutdownfilter shutdownfilter;
// 停机时
if (shutdownfilter != null) {
shutdownfilter.awaitrequestscompletion(5000); // 等待在途请求完成
}
七、验证方式
7.1 日志校验
# 检查停机日志 grep "gracefulshutdownhandler" server.log # 期望输出: # [gracefulshutdownhandler] 收到 contextclosedevent,开始优雅停机... # 设置 running=false,通知消费者停止 # nacos 主动反注册... # nacos 反注册完成 # 等待 30 秒,让上游服务感知下线... # 优雅停机预处理完成,交由 spring 继续销毁 bean
7.2 触发次数校验
# 确保只触发一次(无 feign 冒泡) grep -c "gracefulshutdownhandler.*收到" server.log # 期望:1
7.3 整体停机时长
grep -e "收到 contextclosedevent|all closed success" server.log # 期望:两行时间差 ≈ 30s(sleep 时长)+ bean 销毁时间
八、风险与注意事项
| 场景 | 行为 | 建议 |
|---|---|---|
kill -9 | 跳过所有 jvm 钩子,立即终止 | ❌ 禁止使用 |
| http 在途请求 | sleep 后 datasource 关闭,可能返回 500 | 启用 filter 计数保护 |
| feign 子上下文冒泡 | handler 执行 n 次,停机卡住 | 加 atomicboolean 防护 |
| 消费者任务超时 | 线程池强制中断,可能丢失消息 | 评估业务耗时,调大 awaitterminationseconds |
| 需要立即关闭 | - | kill -15 正常触发,不要用 kill -9 |
九、总结
优雅停机的核心要点:
- 监听
contextclosedevent:在 bean 销毁前执行预处理 - 主动反注册:让注册中心立即推送下线通知
- 设置运行标志:通知消费者停止 poll 循环
- 等待时间窗口:给上游服务感知下线的时间
- 线程池托管:让 spring 自动管理线程池生命周期
- atomicboolean 防护:防止 feign 子上下文事件冒泡
通过以上方案,可以在 spring boot 2.2.x 版本实现完整的优雅停机,保障服务在滚动更新、手动发布等场景下的数据安全和业务连续性。
参考资料
到此这篇关于微服务架构spring boot2.2.x优雅停机的文章就介绍到这了,更多相关spring boot优雅停机内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论