当前位置: 代码网 > it编程>编程语言>Java > 微服务架构Spring Boot 2.2.x优雅停机的完整方案

微服务架构Spring Boot 2.2.x优雅停机的完整方案

2026年08月06日 Java 我要评论
前言在微服务架构中,优雅停机是保障服务可靠性的重要一环。本文分享在 spring boot 2.2.x 版本中实现优雅停机的完整方案,涵盖 nacos 反注册、kafka 消费者停止、线程池等待等核心

前言

在微服务架构中,优雅停机是保障服务可靠性的重要一环。本文分享在 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?

事件触发时机是否适合
contextclosedeventbean 销毁之前✅ 适合
predestroybean 销毁时❌ 太晚,datasource 可能已关闭
shutdownhookjvm 退出时❌ 太晚,spring 容器已销毁

contextclosedeventdestroybeans() 之前发布,此时所有 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 两种消费者模式

模式实现方式停止方法
注解式@kafkalistenerspring 自动管理,无需额外处理
手动式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

九、总结

优雅停机的核心要点:

  1. 监听 contextclosedevent:在 bean 销毁前执行预处理
  2. 主动反注册:让注册中心立即推送下线通知
  3. 设置运行标志:通知消费者停止 poll 循环
  4. 等待时间窗口:给上游服务感知下线的时间
  5. 线程池托管:让 spring 自动管理线程池生命周期
  6. atomicboolean 防护:防止 feign 子上下文事件冒泡

通过以上方案,可以在 spring boot 2.2.x 版本实现完整的优雅停机,保障服务在滚动更新、手动发布等场景下的数据安全和业务连续性。

参考资料

到此这篇关于微服务架构spring boot2.2.x优雅停机的文章就介绍到这了,更多相关spring boot优雅停机内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com