springboot + nacos + k8s 优雅停机

1 概念

优雅停机是什么?网上说的优雅下线、无损下线,都是一个意思。

优雅停机,通常是指在设备、系统或应用程序中止运作前,先执行一定的流程或动作,以确保数据的安全、预防错误并保证系统的整体稳定。

一般来说,优雅停机可以参考以下步骤以实现:

  1. 备份数据 :立即将内存中的所有未保存的修改、缓存等数据保存到数据库或磁盘中。
  2. 停止接收新的请求
  3. 处理未完成的请求
  4. 通知其他依赖组件
  5. 等待所有要素安全退出后,关闭系统

在具体实施时,不同的设备、不同的系统、不同的应用,所需要的优雅停机步骤也不尽相同,甚至需要根据不同的场景来选择不同的方法。

例如,在某些情况下,你可能需要让用户知道,系统即将关闭,并告诉他们应当保存所有的工作并退出系统;而在另一些情况下,你可能需要设计一种策略,能够让系统在无用户介入的情况下,自动保存所有的状态,并在下次启动时恢复之。

但是,无论在哪种情况下,优雅停机的目标都是保护数据,避免错误,并尽量减少到访用户或使用者的不便。

上面的步骤,其实还缺了不少基础的内容,比如,停止请求外,还要停止接收定时任务、停止接收mq消息,等待他们的完成,这2项都是我们微服务中必不可缺的能力。

因此,我希望通过本文,能够更清晰,更详细的讲解,在我已知的真实业务场景下,如何做优雅停机。

文中,很多内容不会讲得太详细,需要大家有一定的搜索能力或者经验!

2 用案例说话

随着微服务的兴起,运维方式由docker -> k8s 变化,优雅停机涉及到的点就越来越多!下面,我们用一个案例,说明优雅停机中的问题和问题解决方案。

案例前:k8s 停机流程

当程序员执行 kubectl delete pod 命令时,两个过程开始:

网络规则即将生效:

  • Kube-apiserver 收到 pod 删除请求,并将 pod 的状态更新为 Extinating at Etcd;
  • 终结点控制器从终结点对象中删除 Pod 的 IP;
  • Kuber-proxy 根据 Endpoint 对象的更改更新 iptables 的规则,并且不再将流量路由到已删除的 pod。

删除容器:

  • Kube-apiserver 收到 pod 删除请求,并将 pod 的状态更新为 Extinating at Etcd;
  • Kubelet 清理节点处的容器相关资源,如存储、网络;
  • 添加 Prestop hook 钩子,等待流量不再发给pod;
  • Kubelet 将SIGTERM发送到容器;
  • 如果容器在默认的 30 秒内没有退出,Kubelet 将发送 SIGKILL 并强制其退出。

k8s + springboot + nacos 案例

PreStopHook 做了2件事情:

  1. nacos反注册
  2. 休眠35秒

通过信号量关闭springboot程序;

其中,k8s的(宽限期)设置为35s。

问题

springBoot程序关闭时间只有2s, 那么该程序就无法处理完一些线程任务、异步消息、定时任务等。为什么呢?

宽限期设置了35s,PreStop休眠了35s + 一个请求的时间,超过了宽限期,那么 kubelet 就会给与 pod 增加一次性2s的宽限时间。Pod 的生命周期,2s不管程序是否正常结束,都会被Kill -9。

为什么反注册之后需要休眠35s?

这里涉及到nacos服务发现原理,nacos服务变更响应时间:实时;ribbon 默认缓存刷新时间30s;因此,一开始是设置30s的,发现还有feign请求失败的情况,所以设置成了35s以解决这个问题!

nacos服务变更响应时间真的是实时吗?

其实并不一定,nacos服务发现是通过http和udp实现的,udp是实时的,http最大等待时间是10s,但是,udp端口生产环境可能没有开放!所以,案例中的nacos服务发现仅通过http定时轮询实现。

案例优化

上面的案例可以优化的点

  • nacos 反注册后休眠35s,是否可以减少;
  • terminationGracePeriodSeconds 设置多少合理?

优化点1

反注册后休眠的35s时候受到nacos服务发现 + ribbon 缓存刷新时间影响,正常应该是 服务发现时间 + 缓存刷新时间 40s才能在极端情况下保证服务停机时,不会再有feign 请求进入。

如果想要缩短这个时间

  • 启用udp,这个需要和运维同学商量,否则10s等待少不了;
  • 监听nacos服务变更通知,发现服务下线后,及时刷新ribbon缓存;
/**
 * 订阅 nacos 实例变更通知
 * 手动刷新 ribbon 服务实例缓存
 * nacos client 1.4.6 【1.4.1有重大缺陷,要注意】
 */
@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();
      // service: DEFAULT_GROUP@@demo         ribbonService: demo
      String ribbonService = service.substring(service.indexOf("@@") + 2);
      log.info("#### 接收到微服务nacos实例变更事件:{} ribbonServiceName: {}", event.getServiceName(), ribbonService);
      ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer(ribbonService);
      if(loadBalancer != null){
         ((ZoneAwareLoadBalancer<?>) loadBalancer).updateListOfServers();
         log.info("刷新 ribbon 服务实例:{} 缓存成功", ribbonService);
      }
   }

   @Override
   public Class<? extends com.alibaba.nacos.common.notify.Event> subscribeType() {
      return InstancesChangeEvent.class;
   }
   

    /**
     * nacos 1.4.4 ~ 1.4.6 需要加这个方法的实现, 2.1.2以后版本修复了该问题
     * 多注册中心时,变更事件没有隔离,因此需要实现该方法来判断事件是否需要处理
     * @see <a href="https://github.com/alibaba/nacos/issues/8428">ISSUE #8428 - Nacos InstancesChange Event Scope</a>
     * **/
    @Override
    public boolean scopeMatches(InstancesChangeEvent event) {
       returntrue;
    }

}

优化点2

terminationGracePeriodSeconds 的值应该略大于 PreStop耗时 + springBoot 停机时间,springBoot 停机时间是由程序业务决定的(mq消息、定时任务、线程池任务、以及备份数据),网上的推荐做法是启用springBoot的优雅停机功能,并实现自定义的关闭逻辑。

springBoot优雅停机的默认缓冲时间是30s,因此,terminationGracePeriodSeconds的时间个人建议10 + 30s即可。

经过优化后

使用 actuator shutdown 方案

有些网贴推荐使用 actuator shutdown 进行优雅停机,那么看下其流程图:

其实,真正的情况并非如上图所示,因为调用shutdown后,springBoot就会进入优雅停机流程,但是这个流程没有结束,然后就会被kill -15 中断,如果线程池没有做好配置,线程池任务没有结束,服务就会关闭。

// 没有设置下面参数,在kill -15时,线程池没有执行结束,会被强制关闭
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.setAwaitTerminationSeconds(30);

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

3 再次优化

mq 和 定时任务

上面的方案中,提到nacos反注册时,其他服务监听反注册事件,进行ribbon缓存刷新,那么,反注册的服务(停机服务)自身,是否可以也监听该事件呢?答案是可以的。

停机的服务监听nacos反注册事件,判断是自己反注册了,表示准备关机,那么就可以停止对mq消息的监听,停止定时任务,这样就比在优雅停机时,进行mq 和 定时任务的停止更完美。

流量控制

如果没有使用k8s进行pod节点的流量控制,那么大概率会使用 springCloud gateway作为服务网关,因此,gateway 服务也应该监听nacos的反注册事件,从而及时刷新ribbon的缓存,关闭停机服务的流量。

4 小结

经过大量的资料参考、学习,最终得到的一份自己认为合格的优雅停机方案,里面可能有较多的不专业表述,敬请谅解和指正,谢谢。

在本文的最后,还要说下,优雅停机最大的挑战并不是来源于这个优雅停机流程,机械化的流程前人都帮忙躺过了,剩下的是业务服务自身的逻辑:

  • 有没有包含超过30s的业务逻辑,如执行超过30s的请求,定时任务、线程池任务或mq消息;
  • 服务关闭时,如何保存未完成的任务、数据,实现自定义的关闭逻辑;
  • 接口逻辑是否做了幂等;
### 集成概述 在构建微服务架构时,Spring Boot与Dubbo和Nacos的集成能够提供强大的服务治理能力。通过这种组合,不仅可以简化开发流程,还能提高系统的可维护性和灵活性[^1]。 ### 使用Nacos作为注册中心和服务发现工具 Nacos作为一个动态服务发现、配置管理和服务中心,非常适合用于微服务体系中的服务注册与发现工作。其主要特点在于支持多种协议的服务发现方式(如DNS和RPC),并且具备良好的跨语言兼容性。 对于希望利用Nacos来完成这一任务的应用程序来说,在pom.xml文件中加入相应的依赖项是必不可少的第一步: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo-nacos-discovery</artifactId> </dependency> ``` 接着需要修改`application.properties`或`yaml`配置文件以指定Nacos服务器的位置以及其他必要的参数设置: ```properties spring.application.name=demo-service-provider spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 dubbo.protocol.name=dubbo dubbo.protocol.port=-1 dubbo.registry.address=nacos://127.0.0.1:8848 ``` 上述配置指定了应用程序名称、Nacos服务器地址以及Dubbo的相关属性。其中特别需要注意的是`dubbo.protocol.port`被设为-1表示由框架自动分配端口号;而`dubbo.registry.address`则表明使用Nacos作为默认注册中心。 ### 利用Dubbo实现远程过程调用(RPC) 为了让两个不同节点间可以通过网络互相通信并执行对方的方法调用,还需要引入另一个重要的组件——Dubbo。该框架提供了高效可靠的RPC机制,允许开发者轻松定义接口并通过简单的注解形式暴露给其他服务消费[^2]。 假设有一个名为`DemoService`的服务接口及其具体实现类,则可以在Provider端按照如下方式进行编码: #### Provider端代码示例 创建一个普通的Java接口作为对外提供的API入口: ```java public interface DemoService { String sayHello(String name); } ``` 随后编写其实现逻辑,并加上适当标注使其成为可供发布的服务实例之一: ```java @Service(version = "1.0.0") // 这里的@Service来自org.apache.dubbo.config.annotation.Service public class DemoServiceImpl implements DemoService { @Override public String sayHello(String name) { return "Hello, " + name; } } ``` 最后一步是在Consumer端声明对该服务的需求即可发起请求获取响应数据: #### Consumer端代码示例 同样地先定义好所需访问的目标接口签名: ```java @Reference(version = "1.0.0", check=false) private DemoService demoService; // 调用方法处... System.out.println(demoService.sayHello("world")); ``` 这里使用的`@Reference`注解来自于`org.apache.dubbo.config.annotation.Reference`包下,用来指示当前成员变量所代表的就是远端某个已发布出来的对象引用。 ### 结合Seata处理分布式事务 当涉及到多个数据库操作或者其他资源协调场景时,确保整个业务流程的一致性就显得尤为重要了。此时可以考虑借助于像Seata这样的分布式事务解决方案来进行全局控制[^3]。 不过这部分内容相对独立一些,因此如果读者感兴趣的话建议单独查阅相关资料深入了解具体的实施细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值