今天来聊一个我最近刚搞过、也确实很多小伙伴踩坑的东西——Spring Boot + Nacos + Kubernetes 怎么优雅停机。说白了就是:服务怎么关得体面、关得不掉链子、关得不影响用户体验。

我跟你讲,这里面的坑啊,可不是你加一行 @PreDestroy 就能搞定的事。尤其你一旦上了 K8s,这个“优雅”两个字背后的含义直接加倍放大!我之前项目里就遇到 Pod 频繁重启,把我数据库连接、MQ消费者、线程池全搞炸了🤯……所以今天我就从程序员实战角度,给你整明白这个事!

先说结论:要想在 Spring Boot + Nacos + K8s 场景下做到真正的“优雅停机”,你得搞定下面这几件事:

1)Nacos里的服务要提前下线 2)K8s探针要合理配置,不然就会出现还没完全关,Pod已经被砍了 3)业务线程池、连接池、消息队列等资源得主动回收 4)一些长连接(WebSocket、Grpc、TCP)要主动关闭 5)如果有MQ消费者,还要搞定幂等性 + 消息回补机制

下面我就一点点来剖一下,附代码,保你看完能立马上手!

第一步,服务停之前得告诉Nacos我“要走了”

不然你服务还在Nacos注册列表里,别的服务还是往你这发请求,结果你都准备撤了还硬接,这不是找事嘛!

Spring Boot里,Nacos Client 有个 NamingService,可以手动把实例从注册中心摘除:

@Autowired
private NamingService namingService;

@Value("${spring.cloud.nacos.discovery.service}")
private String serviceName;

@Value("${spring.cloud.nacos.discovery.ip}")
private String ip;

@Value("${server.port}")
private int port;

@PreDestroy
public void deregister() throws NacosException {
    namingService.deregisterInstance(serviceName, ip, port);
    System.out.println("手动摘除nacos注册实例完成!");
}

注意,这段代码要在优雅停机流程的早期执行,否则就晚了,人家流量已经进来了👋

第二步,K8s 的探针配置很关键!

这里是我之前踩的一个大坑:K8s默认在你Pod收到 SIGTERM 的时候会立刻把它从Service里摘掉,然后马上开始发请求给别的Pod,而你的这个Pod其实还没关完,可能还有线程在跑。

解决办法就是——

1)加一个 preStop hook,给你服务一些“缓冲时间” 2)设置 terminationGracePeriodSeconds 比如说30秒 3)再配合 Spring Boot 的 @PreDestroy 来做资源释放

K8s配置示例👇:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "echo 等30秒再停 && sleep 30"]
terminationGracePeriodSeconds: 35

这个 preStop 就是你Pod快关前的一次“临终遗言时间”,给你30秒窗口干净撤退😎

第三步,线程池要自己收,不然你任务就死在半路上

比如你项目里用了线程池去处理异步任务,那优雅停机时,一定要调用 shutdown() 然后 awaitTermination() 等待任务跑完再结束:

@Autowired
private ThreadPoolTaskExecutor executor;

@PreDestroy
public void shutdownExecutor() {
    executor.shutdown();
    try {
        if (!executor.getThreadPoolExecutor().awaitTermination(30, TimeUnit.SECONDS)) {
            System.out.println("线程池任务还没跑完,强制关闭");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

别以为线程池会自动关,不主动清理,你一停机全是异常堆栈💣

第四步,MQ的消费者得停干净,消息不能丢也不能重复处理

比如你用了 RabbitMQ 或 Kafka 做异步消费,那你服务停机时千万别让它还在消费!否则出现“消息来了但处理了一半”就尴尬了。

以 RabbitMQ 为例,你可以在 @PreDestroy 里手动 stop 掉 listener:

@Autowired
private SimpleMessageListenerContainer listenerContainer;

@PreDestroy
public void shutdownMQConsumer() {
    listenerContainer.stop();
    System.out.println("RabbitMQ 消费者已停止");
}

另外,消息要么做到幂等,要么能补偿(重试、死信、告警等都行),不然出错就不好排查了。

第五步,注意那些容易忘掉的“粘人连接”

我见过很多人忘记关 WebSocket / TCP / Grpc 的连接,结果客户端一直以为“还能用”,但服务早挂了……

你如果用的是 Netty / Spring WebSocket,就得在停机前通知客户端主动关闭,或者服务端统一踢掉连接:

@PreDestroy
public void closeWebSocketSessions() {
    for (Session session : sessionList) {
        if (session.isOpen()) {
            session.close();
        }
    }
}

不然你等客户端一个个 timeout,那用户体验就像掉进黑洞一样🌑

对了,还有一个小Tip!

Spring Boot 从 2.3 开始支持 ApplicationListener<ApplicationReadyEvent> 和 ApplicationListener<ContextClosedEvent> 了,前者适合做一些“启动后注册”,后者适合做“停机前清理”。

你完全可以这么写:

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("监听到Spring容器关闭,开始优雅停机...");
        // 这里调用前面那几个清理动作
    }
}

比单靠 @PreDestroy 更灵活,也能让整个流程更有条理,代码也更清晰。

SpringBoot + Nacos + K8s 想做到“真·优雅停机”,不能只靠“系统自动”,而是要你作为开发者主动插手,去安排服务撤退的每一步。只要你做到:

✅ 提前下线Nacos服务实例 ✅ 配置合理的探针与 preStop✅ 回收所有资源(线程池、MQ、连接) ✅ 幂等性保障+消息补偿机制 ✅ 客户端连接主动关闭

那你这服务就能优雅地告别,不留一丝痕迹!✌️

更多推荐