微服务滚动发布方案 | 优雅且平滑

业务背景

众所周知,在我们迭代业务需求和Fix Bug时往往需要重启我们的服务,在高并发场景下,如果还像N年前一样,通过上机操作敲Linux命令执行Kill操作,在分布式环境下显然会让你敲断手指,并且这种方式重启还会影响用户的操作,导致一些线上事故,所以,在微服务、分布式系统中,平滑滚动发布无疑是非常重要的(主打的就是一个丝滑)。

SpringCloud微服务架构导致发布的一些弊端

1.Nacos组件导致的一些弊端

众所周知,Nacos利用心跳机制每隔一段时间(可配置,defalut:30s)服务端与客户端都会互相发送心跳包,当服务端接收到的客户端心跳包有异常情况时,Nacos服务端会剔除客户端在服务端中的注册信息,意味着这个服务是不可用状态,这种方式其实是有弊端的,这其实也是心跳机制的弊端,比如我服务A重启了,Nacos服务端并没有马上感知服务A的异常情况,这个时候还认为其是可用状态,这个时候如果有流量打进来,通过负载均衡很明显会有流量进入到服务A,无法即时感知客户端异常,就是它的最大弊端。

2.SpringCloud Gateway组件导致的一些弊端

SpringCloud Gateway组件对比以前的Zuul网关,在性能和吞吐提升了很多,主要是因为它是基于Spring5.0+SpringBoot2.0+Spring Reactor,没错看到Reactor你就会想到性能强悍的Netty,SpringCloud Gateway正是用了这一高性能通信框架。

提到网关,第一我们要想到的就是负载均衡机制,之前面试过不少后端的兄弟,你们的系统怎么做的负载均衡,他们中多数人的回答是使用了Nginx的负载均衡机制,我...,咱不废话,接着说SpringCloud Gateway的负载均衡机制,当使用Ribbon作为负载中间件时会使用定时线程从Nacos注册中心拉取服务列表然后放到Gateway服务的本地缓存中,拉取时间默认也是30s,同样的也是无法及时感知Nacos注册中心的服务异常情况,所以其实很多人在使用SpringCloud Gateway中会经常出现一些500,503等问题,下面我将针对这些组件的弊端做一个完善解决方案。

针对Nacos与GateWay弊端的解决方案

Nacos与Gateway之间即时感知弊端的优化切入思路

  1. 在程序停止前通过Nacos上下线事件监听回调操作中主动调用Nacos服务下线操作,让Nacos服务端能即时剔除重启的客户端,避免流量流入
  2. SpringCloud GateWay组件中可以重写其负载均衡策略,也就是在服务上下线回调监听事件中刷新在网关服务本地缓存中的Nacos服务信息,就好比我们业务中更新了数据库要刷一遍缓存一般,操作起来没啥难度。

线上优化实战

  • 问题简介

根据线上日志发现,gateway网关在服务重启时会有调用失败的现象,调用失败可能会导致一些数据的丢失甚至引发一些金钱、充值相关的数据有误,尤其在并发越高的情况下,这种现象表现得越发严重,鄙人曾经的项目中有因为此等原因导致了服务雪崩的情况,所以针对此网关进行一个优化。

  • 优化思路可行性分析
  1. 第一种方式 重写gateway的负载均衡器,从可行性来看问题不大,但是我们的主要问题是针对服务上下线无法及时感知而优化,并不是针对其负载均衡器进行深度优化,所以此方式虽可实现,但开发、结果成本可能稍大,不优先采取
  2. 第二种方式 分析Nacos与Gateway之间的关联关系可知,我们可以通过Nacos上下线的事件监听回调来操作Gateway 令其刷新。
  • 代码
   @Slf4j
   @Component
   public class ApplicationEventListener implements ApplicationListener {

       @Value("${spring.application.name}")
       private String applicationName;

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

       @Autowired
       private DiscoveryClient discoveryClient;
       @Autowired
       private NacosAutoServiceRegistration nacosAutoServiceRegistration;


       @Override
       public void onApplicationEvent(ApplicationEvent applicationEvent) {
           if (applicationEvent instanceof ApplicationStartedEvent) {
               log.info("【{}】【{}】应用启动", IpUtil.getIntranetIp(), applicationName);

               Executors.newSingleThreadExecutor().execute(() -> checkDiscoveryClient());

           } else if (applicationEvent instanceof ContextClosedEvent) {
               log.info("【{}】【{}】程序已停止...", IpUtil.getIntranetIp(), applicationName);
               ApplicationCheckUtil.setSystemIsNormal(false);
               //优雅停机
               nacosAutoServiceRegistration.stop();
               SpringApplication.exit(SpringContextUtil.getApplicationContext());
               ((ConfigurableApplicationContext) SpringContextUtil.getApplicationContext()).close();
           }
       }

       private void checkDiscoveryClient() {
           try {
               List<ServiceInstance> serviceInstanceList;
               AtomicBoolean currentInstanceHasRegister = new AtomicBoolean(false);
               while (true) {
                   serviceInstanceList = discoveryClient.getInstances(applicationName);
                   serviceInstanceList.forEach(serviceInstance -> {
                       log.info("host:{} | port:{} | serviceId:{}", serviceInstance.getHost(), serviceInstance.getPort(), serviceInstance.getServiceId());
                       if (IpUtil.getIntranetIp().equals(serviceInstance.getHost()) && serviceInstance.getPort() == port) {
                           currentInstanceHasRegister.set(true);
                       }
                   });

                   if (currentInstanceHasRegister.get()) {
                       log.info("当前服务实例已成功注册到Nacos中...");
                       break;
                   }

                   TimeUnit.SECONDS.sleep(1);
               }
           } catch (Exception e) {
               log.error("检测服务注册异常...");
           } finally {
               DingTalkUtil.send(DingTalkType.PUBLISH_NOTICE, String.format("【%s】【%s】应用启动成功", IpUtil.getIntranetIp(), applicationName));
               ApplicationCheckUtil.setSystemIsNormal(true);
           }
       }


   }

主要核心代码就一行:nacosAutoServiceRegistration.stop();

线上优化结果指标

通过此优化 频繁重启了几次服务,并且用测试工具一直并发调用重启的服务,并未发现有调用异常的情况,至此 服务端真正意义上的平滑重启稍微提升了一个层次,但还不够,请接着往下看。

K8S滚动发布更新方案

用户请求服务过程

服务重启预想过程

滚动发布流程

k8s参数配置

服务在滚动更新时,deployment控制器的目的是:给旧版本(old_rs)副本数减少至0、给新版本(new_rs)副本数量增至期望值(replicas)。大家在使用时,通常容易忽视控制速率的特性,以下是kubernetes提供的两个参数:

  1. maxUnavailable:和期望ready的副本数比,不可用副本数最大比例(或最大值),这个值越小,越能保证服务稳定,更新越平滑;
  2. maxSurge:和期望ready的副本数比,超过期望副本数最大比例(或最大值),这个值调的越大,副本更新速度越快。
spec:
  ---副本数量
  replicas: 5
  selector:
    matchLabels:
      app: user-service
  minReadySeconds: 120
  strategy:
    ---滚动更新方式
    type: RollingUpdate
    rollingUpdate:
      ---超过期望副本数最大比例(或最大值)
      maxSurge: 1
      ---不可用副本数最大比例(或最大值)
      maxUnavailable: 3

至此,一套完整高可用、稳定的微服务滚动更新方案已完成,基本已经能够满足目前的使用情况。

后续优化展望

  1. 增加灰度发布方案
  2. 增加长连接服务的平滑发布方案(涉及到的知识点很干,大家敬请期待)
#从0到1千万级直播项目#
从0-1开发千万级直播项目 文章被收录于专栏

文章内容源自本人所在互联网社交企业实战项目,分享、记录从0-1做一个千万级直播项目,内容包括高并发场景下技术选型、架构设计、业务解决方案等。

全部评论

相关推荐

点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务