首页
学习
活动
专区
圈层
工具
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

K8s + SpringBoot 怎么优雅关门?这才叫干净利落!

优雅停机,听起来像是给程序一个体面的离场方式。可是在 SpringBoot + Nacos + Kubernetes 这个组合拳里,这个“体面”真不是说说就行,操作不好就变成“仓皇下线”现场。

我是真见过那种程序还在忙着处理 MQ 消息,Kubernetes 一把 kill 掉,堆栈一地鸡毛,数据丢失、调用失败,报警像下雨一样哗啦啦往群里砸。那时候你别说优雅了,连个吭都没来得及打。今天这篇文章,就是来把这件事讲清楚,怎么让 SpringBoot 程序在 K8s 中真正做到优雅停机。

Kubernetes 的套路:不是说关就能关的

在 K8s 里,当你执行kubectl delete pod xxx这个命令,Pod 的死亡倒计时就开始了,但并不是立即“啪”一下消失,而是走一个流程。K8s 先会告诉 API Server:“我要删这个 Pod”,然后状态会变成Terminating。

接下来 kubelet 给容器发一个 SIGTERM 信号,告诉你,“我准备关你了,自己收拾收拾”。这时候程序有一段宽限时间,默认是 30 秒,叫terminationGracePeriodSeconds。你得在这个时间里自己优雅地退出,比如把 MQ 消息处理完、线程池任务执行完、缓存刷盘等等。如果你超时还没搞完,抱歉,K8s 会给你来个 SIGKILL,强制下线,谁也救不了你。

所以问题来了:SpringBoot 应用真的能在这个时间内把所有事情都处理完吗?

SpringBoot 真能做到“优雅”吗?

别看 SpringBoot 启动一个 main 方法就搞定,真要优雅停机,里面门道多着呢。首先,从 SpringBoot 2.3 开始,引入了Lifecycle和SmartLifecycle接口,可以注册 shutdown hook,控制停机流程。

但要实现真正的优雅停机,下面这两句配置必不可少:

threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);

threadPoolTaskExecutor.setAwaitTerminationSeconds(30);

什么意思呢?简单说就是,“我等你把线程池的任务执行完再关机”,而不是不管三七二十一就停掉。否则,你那还在处理的异步请求、定时任务直接一刀切,全挂。

还有一点,SpringBoot 的 actuator 提供了shutdown端点,表面上看可以用它来主动触发停机,看起来很优雅,实际操作的时候你会发现,它只是让 Spring 容器开始退出,并没有等你所有任务都处理完,所以依旧有可能在任务还没跑完的时候,被 K8s 杀掉。

nacos 的“假反应”:反注册 ≠ 没有流量

讲到这,咱得把 Nacos 拉出来讲一讲。K8s 的 PreStop Hook 很好用,可以在 Pod 被杀之前做些操作,比如发个 HTTP 请求告诉别的服务“我准备下线了”,然后 deregister 掉 Nacos 注册中心里的实例,理论上别的服务就不会再访问它了。

但是你知道吗?Nacos 反注册的延迟其实是个坑。

Nacos 使用的是 HTTP 和 UDP 来做服务发现

UDP 是即时通知,但是很多线上环境压根儿没开 UDP

那么就只能走 HTTP 轮询,而默认的刷新时间是 10 秒

再加上 Ribbon 默认的缓存时间是 30 秒

也就是说,从你反注册开始,别的服务最多可能还要 30-40 秒才知道你“死了”。这期间还有请求打进来咋办?靠自己顶着呗。

所以,一些程序员在 PreStop 里加了一个大招:反注册完后 sleep 35 秒,等大家都发现自己已经死透了再关闭服务。

听起来好像靠谱?但是这时候又踩坑了。

你的 terminationGracePeriodSeconds 是 30 秒,sleep 35 秒,等你睡醒了,K8s 已经不耐烦给你一刀了,Pod 直接 SIGKILL。SpringBoot 应用啥都没干完就挂掉了,一地鸡毛又回来了。

解决方法:睡觉不如做点事

怎么解?别盲目 sleep,把时间浪费在等别人刷新缓存上,不如主动出击。你可以在服务内部监听 Nacos 的实例变更事件,比如这样:

@Component

@Slf4j

publicclass NacosInstancesChangeEventListener extends Subscriber<InstancesChangeEvent> {

  @Resource

  private SpringClientFactory springClientFactory;

  @PostConstruct

  public void registerToNotifyCenter(){

      NotifyCenter.registerSubscriber(this);

  }

  @Override

  public void onEvent(InstancesChangeEvent event) {

      String service = event.getServiceName();

      String ribbonService = service.substring(service.indexOf("@@") + 2);

      log.info("Nacos 服务变更:{}", ribbonService);

      ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer(ribbonService);

      if (loadBalancer != null) {

          ((ZoneAwareLoadBalancer<?>) loadBalancer).updateListOfServers();

      }

  }

  @Override

  public Class<? extends Event> subscribeType() {

      return InstancesChangeEvent.class;

  }

  @Override

  public boolean scopeMatches(InstancesChangeEvent event) {

      returntrue;

  }

}

这一段代码的核心是:一旦某个服务实例变更(比如某个实例下线),就主动刷新 Ribbon 的缓存,不用再等 30 秒。这样一来,你 Pod 反注册之后,别的服务几秒钟内就能更新缓存,不会再有请求误打进来。

如果 UDP 开得通,那更好,刷新更快。如果不行,也比原地等死强。

定时任务和 MQ 的“死角”:你以为停了,其实没停

再说一个坑,优雅停机不只是关闭 Web 请求,你还有 MQ 消息监听、定时任务,这些都是后台线程,一不留神就让服务在停机过程中继续干活。

有些人可能会在@PreDestroy方法里手动关掉 MQ 监听器,停掉定时任务。但有没有更自动的办法?当然有。

你也可以监听 Nacos 的反注册事件,一旦发现自己“即将下线”,就关掉 MQ 消息和定时任务,比如这样:

@EventListener

public void onNacosDeregistered(InstancesChangeEvent event) {

  if (event.getServiceName().contains("你的服务名") && 是当前实例) {

      // 停止 MQ 消费

      consumer.stop();

      // 停止定时任务

      scheduler.shutdown();

  }

}

这样你就能在停机前,干净地退出所有“后台活”,不给别人添麻烦,也不给自己留炸弹。

terminationGracePeriodSeconds 到底该设多少?

这个值设多少,不是凭感觉。你得做一个加法:

SpringBoot 自己优雅停机时间:默认是 30 秒

PreStop 中你的处理时间:比如反注册 + Ribbon 缓存刷新,假如是 10 秒

那terminationGracePeriodSeconds至少得 40 秒。再多留一点 buffer,设成 45 或 50 秒都行。别设置得比你 sleep 的时间还短,白忙活。

SpringCloud Gateway 的那一票也不能漏

最后还有个细节,如果你流量是通过 SpringCloud Gateway 进来的,那 Gateway 也要做一件事:监听服务反注册事件,刷新自己的缓存,不要再把请求转发到快死的服务上。

否则就算你服务已经准备“咽气”了,Gateway 还在猛塞请求,真成了“临终被折腾”。

写在最后

说到底,优雅停机的真正挑战,不是这些配置或者流程,而是你有没有认真考虑过你的程序“离开舞台”时要做的每一步。

有没有超过 30 秒的大任务?是同步还是异步的?

MQ、定时任务、线程池有没有收尾逻辑?

你是不是把所有“开着的门”都关好了?

很多时候,服务挂了不是因为 Kubernetes 杀得快,而是我们自己收拾得慢。

程序终有一死,优雅与否,全看生前修行。希望今天这篇,能帮你少走点坑,让你的服务能体面下线,不留后患。

最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OAG-tWEHzscuTWWLQbgUQdZQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券